Tutorial

Tutorial

Tutorial

Building Video Semantic Recommendation with TwelveLabs Embedding and Qdrant Search

Hrishikesh Yadav

Hrishikesh Yadav

Hrishikesh Yadav

In this tutorial, we'll build a recommendation system that understands video content at a semantic level. By combining TwelveLabs' video embedding capabilities with Qdrant vector similarity search, we'll create an engine that finds relevant videos based on their actual meaning—not just keyword matches.

In this tutorial, we'll build a recommendation system that understands video content at a semantic level. By combining TwelveLabs' video embedding capabilities with Qdrant vector similarity search, we'll create an engine that finds relevant videos based on their actual meaning—not just keyword matches.

Join our newsletter

Receive the latest advancements, tutorials, and industry insights in video understanding

Apr 11, 2025

Apr 11, 2025

Apr 11, 2025

10 Min

10 Min

10 Min

Copy link to article

Copy link to article

Copy link to article

Introduction

What if your content recommendation system could truly understand what's happening inside videos, instead of just relying on tags, transcriptions, and keywords? 🔍

In this tutorial, we'll build a recommendation system that understands video content at a semantic level. By combining TwelveLabs' video embedding capabilities with Qdrant vector similarity search, we'll create an engine that finds relevant videos based on their actual meaning—not just keyword matches.

Let's explore how this application works and how you can build similar solutions using the TwelveLabs Python SDK and Qdrant Cloud Quickstart.

You can explore the demo of the application here: TwelveLabs Content Recommendation


Prerequisites

  • Generate an API key by signing up at the TwelveLabs Playground.

  • Set up Qdrant Cloud by following the Setup Guide.

  • Find the repository for this application on Github.

  • You should be familiar with Python, Flask, and Next.js


Demo Application

This demo application showcases the power of semantic video content recommendations. Users can select an intent-based category, and the system suggests relevant content through semantic similarity matching. Users can also specify their mood to receive content that aligns with their emotional state.

The system stores embeddings from a large collection of animated videos across different categories in Qdrant Cloud.

Working of the Application

The application operates in two main stages:

  • Embedding Generation & Storage – The system creates video embeddings using Marengo 2.7 and generates a public URL via an S3 bucket. These embeddings and their metadata are stored in a Qdrant Cloud Vector database collection.

  • Search & Retrieval - When users enter their preferences and search query, the system converts the text into embeddings using Marengo 2.7 and performs a semantic search for relevant videos. Users can refine results by adjusting their mood or selecting new preferences, and the system fetches updated recommendations from the Qdrant Cloud collection.

To make the application work as expected, you must insert the embedding data into the collection, or you can refer to the sample data provided here used in this application.


Preparation Steps

  1. Obtain your API key from the TwelveLabs Playground and set up your environment variable.

  2. Clone the project from Github.

  3. Create a .env file containing your TwelveLabs and Qdrant credentials.

  4. Set up a Qdrant Cloud instance. Follow the setup guide for the Qdrant Cloud Cluster.

Once you've completed these steps, you're ready to start developing!


Walkthrough for Content Recommendation App

This tutorial shows you how to build a video content recommendation application. The app uses Next.js for the frontend and Flask API with CORS enabled for the backend. We'll focus on implementing the core backend functionality for content recommendations and setting up the application.

You'll learn how to set up the Qdrant Cloud client, generate and insert embeddings, and interact with the collection to retrieve relevant videos. For detailed code structure and setup instructions, check the README.md on GitHub.


1 - Guide for Embedding Generation and Insertion into Qdrant


Step 1 - Setup and Dependencies

Let's start by installing the necessary dependencies.


Now let's import the libraries we'll need:

import os
import uuid
import boto3
from botocore.exceptions import ClientError
import requests
from IPython.display import display, HTML
import shutil
import pandas as pd
from twelvelabs import TwelveLabs
from qdrant_client import QdrantClient, models
from qdrant_client.models import PointStruct
import time

Step 2 - Configuring Services

Let's configure AWS S3, TwelveLabs, and Qdrant. First, set up an AWS S3 bucket to generate public video URLs for streaming. Then, initialize the Qdrant and TwelveLabs clients to enable efficient video embedding and search.

# AWS S3 Configuration
AWS_ACCESS_KEY = "YOUR_AWS_ACCESS_KEY"
AWS_SECRET_KEY = "YOUR_AWS_SECRET_KEY"
AWS_BUCKET_NAME = "YOUR_BUCKET_NAME"
AWS_REGION = "us-east-1"  # Change to your region

# Initialize S3 client
s3_client = boto3.client(
    's3',
    aws_access_key_id=AWS_ACCESS_KEY,
    aws_secret_access_key=AWS_SECRET_KEY,
    region_name=AWS_REGION
)

# Twelve Labs Configuration
TWELVE_LABS_API_KEY = "YOUR_TWELVE_LABS_API_KEY"
twelvelabs_client = TwelveLabs(api_key=TWELVE_LABS_API_KEY)

# Qdrant Configuration
QDRANT_HOST = "YOUR_QDRANT_HOST"
QDRANT_API_KEY = "YOUR_QDRANT_API_KEY"
COLLECTION_NAME = "content_collection"
VECTOR_SIZE = 1024  # Size of embeddings from Twelve Labs

# Initialize Qdrant client
qdrant_client = QdrantClient(
    url=f"https://{QDRANT_HOST}",
    api_key=QDRANT_API_KEY,
    timeout=20,
    prefer_grpc=False
)

When configuring the Qdrant client, set timeout=20 to prevent indefinite waiting and avoid delays. Set prefer_grpc=False since Flask natively supports HTTP REST APIs rather than gRPC. This ensures smooth communication between Flask and Qdrant using standard HTTP.

Define the video directory and access the MP4 folder within it. This setup is essential for the subsequent processing steps.

# Get a list of video files
video_dir = "downloads/video_content"
video_files = [f for f in os.listdir(video_dir) if f.endswith('.mp4')]


Step 3 - Uploading Videos to AWS S3

First, we need a function to upload videos to AWS S3 Bucket and generate public URLs. The public URL is returned and stored as metadata for each video.

def upload_to_s3(file_path, filename):
    try:
        # Upload the file
        s3_client.upload_file(
            file_path,
            AWS_BUCKET_NAME,
            f"videos-embed/{filename}",
            ExtraArgs={
                'ACL': 'public-read',
                'ContentType': 'video/mp4'
            }
        )

        # Generate the public URL
        url = f"https://{AWS_BUCKET_NAME}.s3.{AWS_REGION}.amazonaws.com/videos-embed/{filename}"
        print(f"Uploaded to S3: {url}")
        return url

    except ClientError as e:
        print(f"Error uploading to S3: {str(e)}")
        raise


Step 4 -  Generating Video Embedding with Marengo 2.7

Now we'll create a function to generate video embeddings using TwelveLabs Marengo-retrieval-2.7 Engine. You can find the code for the video embedding generation and insertion here.

def create_video_embedding(video_path, max_retries=3, retry_delay=5):
    if not twelvelabs_client:
        raise ValueError("Twelve Labs API key not configured")

    retries = 0
    while retries < max_retries:
        try:
            print(f"Creating whole video embedding for {video_path}... (Attempt {retries+1}/{max_retries})")

            # Use video_embedding_scopes parameter set to ["clip", "video"] to get whole video embedding
            task = twelvelabs_client.embed.task.create(
                model_name="Marengo-retrieval-2.7",
                video_file=video_path,
                video_embedding_scopes=["clip", "video"]
            )

            print(f"Created task: id={task.id}, status={task.status}")
            task.wait_for_done(sleep_interval=3)
            task_result = twelvelabs_client.embed.task.retrieve(task.id)

            if task_result.status != 'ready':
                raise ValueError(f"Task failed with status: {task_result.status}")

            return task_result

        except Exception as e:
            print(f"Error creating embedding (attempt {retries+1}): {str(e)}")
            retries += 1
            if retries < max_retries:
                print(f"Retrying in {retry_delay} seconds...")
                time.sleep(retry_delay)
                retry_delay *= 2
            else:
                print("Max retries reached, giving up.")
                raise

The video_embedding_scopes is set to both clip and video to generate embeddings for the entire video clip.


Step 5 - Insertion of Embeddings into Qdrant with metadata

Let's create a function to store the generated embeddings in our Qdrant vector database. The video embedding is stored as a point with metadata payload, improving searchability. The video URL in the metadata enables streaming.

def store_in_qdrant(task_result, video_id, s3_url, original_filename):
    if not qdrant_client:
        raise ValueError("Qdrant client not configured")

    try:
        print(f"Processing video embedding for {video_id}...")

        # The embedding will be in the segments with embedding_scope="video"
        if task_result.video_embedding and task_result.video_embedding.segments:
            video_segments = [s for s in task_result.video_embedding.segments
                             if hasattr(s, 'embedding_scope') and s.embedding_scope == 'video']

            if video_segments:
                print(f"Found video-scope embedding")
                embedding_vector = video_segments[0].embeddings_float
            else:
                # If no video scope segment is found, use the first segment as fallback
                print(f"No video-scope embedding found, using first available segment")
                embedding_vector = task_result.video_embedding.segments[0].embeddings_float
        else:
            raise ValueError("No embeddings found in the response")

        # Create a unique point structure for Qdrant storage
        point = PointStruct(
            id=uuid.uuid4().int & ((1<<64)-1), # Generate a unique 64-bit integer ID
            vector=embedding_vector, # Store the extracted embedding vector
            payload={
                'video_id': video_id,
                'video_url': s3_url,  # Store the public S3 URL of the video
                'is_url': True,
                'original_filename': original_filename # Save the original filename
            }
        )

	 # Insert the generated embedding point into the Qdrant collection
        qdrant_client.upsert(collection_name=COLLECTION_NAME, points=[point])
        print(f"Stored whole video embedding in Qdrant")
        return 1
    except Exception as e:
        print(f"Error storing in Qdrant: {str(e)}")
        raise


Step 6 - Video Processing Pipeline

Now we'll define a streamlined flow that connects all components, processing videos through our pipeline.

Process all videos in a directory through the full pipeline:

  1. Upload to AWS S3 Bucket

  2. Generate embeddings using TwelveLabs

  3. Store embeddings in Qdrant

# Process each video
for filename in video_files[:5]:  # Process first 5 videos or you can setup as per convenience
    try:
        print(f"\nProcessing {filename}...")
        video_path = os.path.join(video_dir, filename)
        video_id = f"{str(uuid.uuid4())[:8]}_{filename}"
        
        # Upload to S3
        s3_url = upload_to_s3(video_path, video_id)
        
        # Generate embeddings
        task_result = create_video_embedding(video_path)
        
        # Store in Qdrant
        store_in_qdrant(task_result, video_id, s3_url, filename)
        
        print(f"Successfully processed {filename}")
    except Exception as e:
        print(f"Error processing {filename}: {str(e)}")

The video embeddings and metadata are now stored in the Qdrant Cloud. The next step is to connect our application to the collection for retrieval.


2 - Building Search API with Flask


Step 1 - Setup CORS Origin

To enable cross-origin requests, we need to configure CORS (Cross-Origin Resource Sharing) in our Flask application. This allows web clients hosted on different domains to access our API. You can find the complete backend implementation in app.py.

app = Flask(__name__) 
CORS(app, resources={r"/*": {"origins": "*"}})


Step 2  - Initializing Qdrant Collection

We need to initialize our Qdrant collection if it doesn't exist. This setup ensures our vector database can properly retrieve video embeddings. The TwelveLabs client is initialized to generate search query embeddings for semantic search capability.

# Get credentials from environment variables
API_KEY = os.getenv('API_KEY')
QDRANT_HOST = os.getenv('QDRANT_HOST')
QDRANT_API_KEY = os.getenv('QDRANT_API_KEY')

# Qdrant Configuration
COLLECTION_NAME = "content_collection"
VECTOR_SIZE = 1024 # Dimension of vector embeddings

# Initialize clients
try:
    client = TwelveLabs(api_key=API_KEY)
    qdrant_client = QdrantClient(
        url=f"https://{QDRANT_HOST}",
        api_key=QDRANT_API_KEY,
        timeout=20
    )
    logger.info("Successfully initialized API clients")
except Exception as e:
    logger.error(f"Failed to initialize clients: {str(e)}")
    raise

def init_qdrant():
    try:
	 # Fetch all existing collections
        collections = qdrant_client.get_collections().collections
        collection_exists = any(col.name == COLLECTION_NAME for col in collections)
        if not collection_exists:
            # Create the collection with specified vector configuration if it doesn't exist
            qdrant_client.recreate_collection(
                collection_name=COLLECTION_NAME,
                vectors_config=VectorParams(
                    size=VECTOR_SIZE,
                    distance=Distance.COSINE # Use cosine similarity for retrieval
                )
            )
            logger.info(f"Created collection: {COLLECTION_NAME}")
    except Exception as e:
        logger.error(f"Qdrant initialization error: {str(e)}")
        raise


Step 3 - Creating Simple Search Functionality

This section implements a search endpoint that enables users to search for videos. The endpoint handles search queries, generates embeddings via TwelveLabs, retrieves similar vectors from Qdrant, and returns matching results.

Let's create a search endpoint for video searches.

@app.route('/search', methods=['POST'])
def search():
    # Ensure the request contains JSON data
    if not request.is_json:
        logger.warning("Missing JSON data")
        return jsonify({'error': 'Request must be JSON format'}), 400
        
    # Get and validate query
    data = request.get_json()
    query = data.get('query')
    if not query:
        logger.warning("Empty query parameter")
        return jsonify({'error': 'Missing query parameter'}), 400
    
    logger.info(f"Processing search: '{query}'")
    
    try:
        # Generate embedding for the search query
        formatted_query = f"Recommend - {query}"
        embedding_response = client.embed.create(
            model_name="Marengo-retrieval-2.7",
            text=formatted_query
        )

	 # Get the embedding vector
        vector = embedding_response.text_embedding.segments[0].embeddings_float
        
        # Similarity search from the Qdrant collection
        query_response = qdrant_client.query_points(
            collection_name=COLLECTION_NAME,
            query=vector,
            limit=10,
            with_payload=True
        )
        
        # Extract and format results
        search_results = query_response.points
        logger.info(f"Found {len(search_results)} matching results")
        
        # If no results, return empty list
        if not search_results:
            return jsonify([])
        
        # Build formatted response
        formatted_results = []
        for result in search_results:
            point_id = result.id
            score = float(result.score)
            payload = result.payload
            
            formatted_results.append({
                'video_id': payload.get('video_id', f"video_{point_id}"),
                'filename': payload.get('original_filename', payload.get('filename', 'video.mp4')),
                'start_time': float(payload.get('start_time', 0)),
                'end_time': float(payload.get('end_time', 30)),
                'score': score,
                'confidence': 'high' if score > 0.7 else 'medium',
                'url': payload.get('video_url')
            })
        
        logger.info(f"Returning {len(formatted_results)} results")
        return jsonify(formatted_results)
        
    except Exception as e:
        logger.exception(f"Search error: {str(e)}")
        return jsonify({'error': 'Search failed', 'details': str(e)}), 500

Here's how it works:

  1. Receives a search query from the request

  2. Generates an embedding vector using TwelveLabs

  3. Searches for similar vectors in Qdrant

  4. Returns the matching videos in a structured format


More Ideas to Experiment with the Tutorial

Understanding how applications work helps you create innovative products that meet user needs. Here are some potential use cases for video content embeddings:

🎯 Personalized Ad Insertion — Dynamically insert context-relevant ads into videos.

⚙️ Real-Time Similarity Matching — Instantly find similar videos as new content is uploaded.

📊 Trend Analysis & Insights — Cluster and analyze video trends based on embedding patterns.


Conclusion

This tutorial shows how video understanding creates smarter, more accurate content recommendations. Using TwelveLabs for video embeddings and Qdrant for fast vector search, we've built a system that understands video content beyond manual transcription, tags, or keywords. This approach delivers better recommendations, keeps users engaged, and scales easily with large video collections. As an open-source solution, it can be customized for various industries—from education to entertainment and beyond.


Additional Resources

Learn more about the embedding generation engine—Marengo-retrieval-2.7. To explore TwelveLabs further and enhance your understanding of video content analysis, check out these resources:

We encourage you to use these resources to expand your knowledge and create innovative applications using TwelveLabs video understanding technology.

Introduction

What if your content recommendation system could truly understand what's happening inside videos, instead of just relying on tags, transcriptions, and keywords? 🔍

In this tutorial, we'll build a recommendation system that understands video content at a semantic level. By combining TwelveLabs' video embedding capabilities with Qdrant vector similarity search, we'll create an engine that finds relevant videos based on their actual meaning—not just keyword matches.

Let's explore how this application works and how you can build similar solutions using the TwelveLabs Python SDK and Qdrant Cloud Quickstart.

You can explore the demo of the application here: TwelveLabs Content Recommendation


Prerequisites

  • Generate an API key by signing up at the TwelveLabs Playground.

  • Set up Qdrant Cloud by following the Setup Guide.

  • Find the repository for this application on Github.

  • You should be familiar with Python, Flask, and Next.js


Demo Application

This demo application showcases the power of semantic video content recommendations. Users can select an intent-based category, and the system suggests relevant content through semantic similarity matching. Users can also specify their mood to receive content that aligns with their emotional state.

The system stores embeddings from a large collection of animated videos across different categories in Qdrant Cloud.

Working of the Application

The application operates in two main stages:

  • Embedding Generation & Storage – The system creates video embeddings using Marengo 2.7 and generates a public URL via an S3 bucket. These embeddings and their metadata are stored in a Qdrant Cloud Vector database collection.

  • Search & Retrieval - When users enter their preferences and search query, the system converts the text into embeddings using Marengo 2.7 and performs a semantic search for relevant videos. Users can refine results by adjusting their mood or selecting new preferences, and the system fetches updated recommendations from the Qdrant Cloud collection.

To make the application work as expected, you must insert the embedding data into the collection, or you can refer to the sample data provided here used in this application.


Preparation Steps

  1. Obtain your API key from the TwelveLabs Playground and set up your environment variable.

  2. Clone the project from Github.

  3. Create a .env file containing your TwelveLabs and Qdrant credentials.

  4. Set up a Qdrant Cloud instance. Follow the setup guide for the Qdrant Cloud Cluster.

Once you've completed these steps, you're ready to start developing!


Walkthrough for Content Recommendation App

This tutorial shows you how to build a video content recommendation application. The app uses Next.js for the frontend and Flask API with CORS enabled for the backend. We'll focus on implementing the core backend functionality for content recommendations and setting up the application.

You'll learn how to set up the Qdrant Cloud client, generate and insert embeddings, and interact with the collection to retrieve relevant videos. For detailed code structure and setup instructions, check the README.md on GitHub.


1 - Guide for Embedding Generation and Insertion into Qdrant


Step 1 - Setup and Dependencies

Let's start by installing the necessary dependencies.


Now let's import the libraries we'll need:

import os
import uuid
import boto3
from botocore.exceptions import ClientError
import requests
from IPython.display import display, HTML
import shutil
import pandas as pd
from twelvelabs import TwelveLabs
from qdrant_client import QdrantClient, models
from qdrant_client.models import PointStruct
import time

Step 2 - Configuring Services

Let's configure AWS S3, TwelveLabs, and Qdrant. First, set up an AWS S3 bucket to generate public video URLs for streaming. Then, initialize the Qdrant and TwelveLabs clients to enable efficient video embedding and search.

# AWS S3 Configuration
AWS_ACCESS_KEY = "YOUR_AWS_ACCESS_KEY"
AWS_SECRET_KEY = "YOUR_AWS_SECRET_KEY"
AWS_BUCKET_NAME = "YOUR_BUCKET_NAME"
AWS_REGION = "us-east-1"  # Change to your region

# Initialize S3 client
s3_client = boto3.client(
    's3',
    aws_access_key_id=AWS_ACCESS_KEY,
    aws_secret_access_key=AWS_SECRET_KEY,
    region_name=AWS_REGION
)

# Twelve Labs Configuration
TWELVE_LABS_API_KEY = "YOUR_TWELVE_LABS_API_KEY"
twelvelabs_client = TwelveLabs(api_key=TWELVE_LABS_API_KEY)

# Qdrant Configuration
QDRANT_HOST = "YOUR_QDRANT_HOST"
QDRANT_API_KEY = "YOUR_QDRANT_API_KEY"
COLLECTION_NAME = "content_collection"
VECTOR_SIZE = 1024  # Size of embeddings from Twelve Labs

# Initialize Qdrant client
qdrant_client = QdrantClient(
    url=f"https://{QDRANT_HOST}",
    api_key=QDRANT_API_KEY,
    timeout=20,
    prefer_grpc=False
)

When configuring the Qdrant client, set timeout=20 to prevent indefinite waiting and avoid delays. Set prefer_grpc=False since Flask natively supports HTTP REST APIs rather than gRPC. This ensures smooth communication between Flask and Qdrant using standard HTTP.

Define the video directory and access the MP4 folder within it. This setup is essential for the subsequent processing steps.

# Get a list of video files
video_dir = "downloads/video_content"
video_files = [f for f in os.listdir(video_dir) if f.endswith('.mp4')]


Step 3 - Uploading Videos to AWS S3

First, we need a function to upload videos to AWS S3 Bucket and generate public URLs. The public URL is returned and stored as metadata for each video.

def upload_to_s3(file_path, filename):
    try:
        # Upload the file
        s3_client.upload_file(
            file_path,
            AWS_BUCKET_NAME,
            f"videos-embed/{filename}",
            ExtraArgs={
                'ACL': 'public-read',
                'ContentType': 'video/mp4'
            }
        )

        # Generate the public URL
        url = f"https://{AWS_BUCKET_NAME}.s3.{AWS_REGION}.amazonaws.com/videos-embed/{filename}"
        print(f"Uploaded to S3: {url}")
        return url

    except ClientError as e:
        print(f"Error uploading to S3: {str(e)}")
        raise


Step 4 -  Generating Video Embedding with Marengo 2.7

Now we'll create a function to generate video embeddings using TwelveLabs Marengo-retrieval-2.7 Engine. You can find the code for the video embedding generation and insertion here.

def create_video_embedding(video_path, max_retries=3, retry_delay=5):
    if not twelvelabs_client:
        raise ValueError("Twelve Labs API key not configured")

    retries = 0
    while retries < max_retries:
        try:
            print(f"Creating whole video embedding for {video_path}... (Attempt {retries+1}/{max_retries})")

            # Use video_embedding_scopes parameter set to ["clip", "video"] to get whole video embedding
            task = twelvelabs_client.embed.task.create(
                model_name="Marengo-retrieval-2.7",
                video_file=video_path,
                video_embedding_scopes=["clip", "video"]
            )

            print(f"Created task: id={task.id}, status={task.status}")
            task.wait_for_done(sleep_interval=3)
            task_result = twelvelabs_client.embed.task.retrieve(task.id)

            if task_result.status != 'ready':
                raise ValueError(f"Task failed with status: {task_result.status}")

            return task_result

        except Exception as e:
            print(f"Error creating embedding (attempt {retries+1}): {str(e)}")
            retries += 1
            if retries < max_retries:
                print(f"Retrying in {retry_delay} seconds...")
                time.sleep(retry_delay)
                retry_delay *= 2
            else:
                print("Max retries reached, giving up.")
                raise

The video_embedding_scopes is set to both clip and video to generate embeddings for the entire video clip.


Step 5 - Insertion of Embeddings into Qdrant with metadata

Let's create a function to store the generated embeddings in our Qdrant vector database. The video embedding is stored as a point with metadata payload, improving searchability. The video URL in the metadata enables streaming.

def store_in_qdrant(task_result, video_id, s3_url, original_filename):
    if not qdrant_client:
        raise ValueError("Qdrant client not configured")

    try:
        print(f"Processing video embedding for {video_id}...")

        # The embedding will be in the segments with embedding_scope="video"
        if task_result.video_embedding and task_result.video_embedding.segments:
            video_segments = [s for s in task_result.video_embedding.segments
                             if hasattr(s, 'embedding_scope') and s.embedding_scope == 'video']

            if video_segments:
                print(f"Found video-scope embedding")
                embedding_vector = video_segments[0].embeddings_float
            else:
                # If no video scope segment is found, use the first segment as fallback
                print(f"No video-scope embedding found, using first available segment")
                embedding_vector = task_result.video_embedding.segments[0].embeddings_float
        else:
            raise ValueError("No embeddings found in the response")

        # Create a unique point structure for Qdrant storage
        point = PointStruct(
            id=uuid.uuid4().int & ((1<<64)-1), # Generate a unique 64-bit integer ID
            vector=embedding_vector, # Store the extracted embedding vector
            payload={
                'video_id': video_id,
                'video_url': s3_url,  # Store the public S3 URL of the video
                'is_url': True,
                'original_filename': original_filename # Save the original filename
            }
        )

	 # Insert the generated embedding point into the Qdrant collection
        qdrant_client.upsert(collection_name=COLLECTION_NAME, points=[point])
        print(f"Stored whole video embedding in Qdrant")
        return 1
    except Exception as e:
        print(f"Error storing in Qdrant: {str(e)}")
        raise


Step 6 - Video Processing Pipeline

Now we'll define a streamlined flow that connects all components, processing videos through our pipeline.

Process all videos in a directory through the full pipeline:

  1. Upload to AWS S3 Bucket

  2. Generate embeddings using TwelveLabs

  3. Store embeddings in Qdrant

# Process each video
for filename in video_files[:5]:  # Process first 5 videos or you can setup as per convenience
    try:
        print(f"\nProcessing {filename}...")
        video_path = os.path.join(video_dir, filename)
        video_id = f"{str(uuid.uuid4())[:8]}_{filename}"
        
        # Upload to S3
        s3_url = upload_to_s3(video_path, video_id)
        
        # Generate embeddings
        task_result = create_video_embedding(video_path)
        
        # Store in Qdrant
        store_in_qdrant(task_result, video_id, s3_url, filename)
        
        print(f"Successfully processed {filename}")
    except Exception as e:
        print(f"Error processing {filename}: {str(e)}")

The video embeddings and metadata are now stored in the Qdrant Cloud. The next step is to connect our application to the collection for retrieval.


2 - Building Search API with Flask


Step 1 - Setup CORS Origin

To enable cross-origin requests, we need to configure CORS (Cross-Origin Resource Sharing) in our Flask application. This allows web clients hosted on different domains to access our API. You can find the complete backend implementation in app.py.

app = Flask(__name__) 
CORS(app, resources={r"/*": {"origins": "*"}})


Step 2  - Initializing Qdrant Collection

We need to initialize our Qdrant collection if it doesn't exist. This setup ensures our vector database can properly retrieve video embeddings. The TwelveLabs client is initialized to generate search query embeddings for semantic search capability.

# Get credentials from environment variables
API_KEY = os.getenv('API_KEY')
QDRANT_HOST = os.getenv('QDRANT_HOST')
QDRANT_API_KEY = os.getenv('QDRANT_API_KEY')

# Qdrant Configuration
COLLECTION_NAME = "content_collection"
VECTOR_SIZE = 1024 # Dimension of vector embeddings

# Initialize clients
try:
    client = TwelveLabs(api_key=API_KEY)
    qdrant_client = QdrantClient(
        url=f"https://{QDRANT_HOST}",
        api_key=QDRANT_API_KEY,
        timeout=20
    )
    logger.info("Successfully initialized API clients")
except Exception as e:
    logger.error(f"Failed to initialize clients: {str(e)}")
    raise

def init_qdrant():
    try:
	 # Fetch all existing collections
        collections = qdrant_client.get_collections().collections
        collection_exists = any(col.name == COLLECTION_NAME for col in collections)
        if not collection_exists:
            # Create the collection with specified vector configuration if it doesn't exist
            qdrant_client.recreate_collection(
                collection_name=COLLECTION_NAME,
                vectors_config=VectorParams(
                    size=VECTOR_SIZE,
                    distance=Distance.COSINE # Use cosine similarity for retrieval
                )
            )
            logger.info(f"Created collection: {COLLECTION_NAME}")
    except Exception as e:
        logger.error(f"Qdrant initialization error: {str(e)}")
        raise


Step 3 - Creating Simple Search Functionality

This section implements a search endpoint that enables users to search for videos. The endpoint handles search queries, generates embeddings via TwelveLabs, retrieves similar vectors from Qdrant, and returns matching results.

Let's create a search endpoint for video searches.

@app.route('/search', methods=['POST'])
def search():
    # Ensure the request contains JSON data
    if not request.is_json:
        logger.warning("Missing JSON data")
        return jsonify({'error': 'Request must be JSON format'}), 400
        
    # Get and validate query
    data = request.get_json()
    query = data.get('query')
    if not query:
        logger.warning("Empty query parameter")
        return jsonify({'error': 'Missing query parameter'}), 400
    
    logger.info(f"Processing search: '{query}'")
    
    try:
        # Generate embedding for the search query
        formatted_query = f"Recommend - {query}"
        embedding_response = client.embed.create(
            model_name="Marengo-retrieval-2.7",
            text=formatted_query
        )

	 # Get the embedding vector
        vector = embedding_response.text_embedding.segments[0].embeddings_float
        
        # Similarity search from the Qdrant collection
        query_response = qdrant_client.query_points(
            collection_name=COLLECTION_NAME,
            query=vector,
            limit=10,
            with_payload=True
        )
        
        # Extract and format results
        search_results = query_response.points
        logger.info(f"Found {len(search_results)} matching results")
        
        # If no results, return empty list
        if not search_results:
            return jsonify([])
        
        # Build formatted response
        formatted_results = []
        for result in search_results:
            point_id = result.id
            score = float(result.score)
            payload = result.payload
            
            formatted_results.append({
                'video_id': payload.get('video_id', f"video_{point_id}"),
                'filename': payload.get('original_filename', payload.get('filename', 'video.mp4')),
                'start_time': float(payload.get('start_time', 0)),
                'end_time': float(payload.get('end_time', 30)),
                'score': score,
                'confidence': 'high' if score > 0.7 else 'medium',
                'url': payload.get('video_url')
            })
        
        logger.info(f"Returning {len(formatted_results)} results")
        return jsonify(formatted_results)
        
    except Exception as e:
        logger.exception(f"Search error: {str(e)}")
        return jsonify({'error': 'Search failed', 'details': str(e)}), 500

Here's how it works:

  1. Receives a search query from the request

  2. Generates an embedding vector using TwelveLabs

  3. Searches for similar vectors in Qdrant

  4. Returns the matching videos in a structured format


More Ideas to Experiment with the Tutorial

Understanding how applications work helps you create innovative products that meet user needs. Here are some potential use cases for video content embeddings:

🎯 Personalized Ad Insertion — Dynamically insert context-relevant ads into videos.

⚙️ Real-Time Similarity Matching — Instantly find similar videos as new content is uploaded.

📊 Trend Analysis & Insights — Cluster and analyze video trends based on embedding patterns.


Conclusion

This tutorial shows how video understanding creates smarter, more accurate content recommendations. Using TwelveLabs for video embeddings and Qdrant for fast vector search, we've built a system that understands video content beyond manual transcription, tags, or keywords. This approach delivers better recommendations, keeps users engaged, and scales easily with large video collections. As an open-source solution, it can be customized for various industries—from education to entertainment and beyond.


Additional Resources

Learn more about the embedding generation engine—Marengo-retrieval-2.7. To explore TwelveLabs further and enhance your understanding of video content analysis, check out these resources:

We encourage you to use these resources to expand your knowledge and create innovative applications using TwelveLabs video understanding technology.

Introduction

What if your content recommendation system could truly understand what's happening inside videos, instead of just relying on tags, transcriptions, and keywords? 🔍

In this tutorial, we'll build a recommendation system that understands video content at a semantic level. By combining TwelveLabs' video embedding capabilities with Qdrant vector similarity search, we'll create an engine that finds relevant videos based on their actual meaning—not just keyword matches.

Let's explore how this application works and how you can build similar solutions using the TwelveLabs Python SDK and Qdrant Cloud Quickstart.

You can explore the demo of the application here: TwelveLabs Content Recommendation


Prerequisites

  • Generate an API key by signing up at the TwelveLabs Playground.

  • Set up Qdrant Cloud by following the Setup Guide.

  • Find the repository for this application on Github.

  • You should be familiar with Python, Flask, and Next.js


Demo Application

This demo application showcases the power of semantic video content recommendations. Users can select an intent-based category, and the system suggests relevant content through semantic similarity matching. Users can also specify their mood to receive content that aligns with their emotional state.

The system stores embeddings from a large collection of animated videos across different categories in Qdrant Cloud.

Working of the Application

The application operates in two main stages:

  • Embedding Generation & Storage – The system creates video embeddings using Marengo 2.7 and generates a public URL via an S3 bucket. These embeddings and their metadata are stored in a Qdrant Cloud Vector database collection.

  • Search & Retrieval - When users enter their preferences and search query, the system converts the text into embeddings using Marengo 2.7 and performs a semantic search for relevant videos. Users can refine results by adjusting their mood or selecting new preferences, and the system fetches updated recommendations from the Qdrant Cloud collection.

To make the application work as expected, you must insert the embedding data into the collection, or you can refer to the sample data provided here used in this application.


Preparation Steps

  1. Obtain your API key from the TwelveLabs Playground and set up your environment variable.

  2. Clone the project from Github.

  3. Create a .env file containing your TwelveLabs and Qdrant credentials.

  4. Set up a Qdrant Cloud instance. Follow the setup guide for the Qdrant Cloud Cluster.

Once you've completed these steps, you're ready to start developing!


Walkthrough for Content Recommendation App

This tutorial shows you how to build a video content recommendation application. The app uses Next.js for the frontend and Flask API with CORS enabled for the backend. We'll focus on implementing the core backend functionality for content recommendations and setting up the application.

You'll learn how to set up the Qdrant Cloud client, generate and insert embeddings, and interact with the collection to retrieve relevant videos. For detailed code structure and setup instructions, check the README.md on GitHub.


1 - Guide for Embedding Generation and Insertion into Qdrant


Step 1 - Setup and Dependencies

Let's start by installing the necessary dependencies.


Now let's import the libraries we'll need:

import os
import uuid
import boto3
from botocore.exceptions import ClientError
import requests
from IPython.display import display, HTML
import shutil
import pandas as pd
from twelvelabs import TwelveLabs
from qdrant_client import QdrantClient, models
from qdrant_client.models import PointStruct
import time

Step 2 - Configuring Services

Let's configure AWS S3, TwelveLabs, and Qdrant. First, set up an AWS S3 bucket to generate public video URLs for streaming. Then, initialize the Qdrant and TwelveLabs clients to enable efficient video embedding and search.

# AWS S3 Configuration
AWS_ACCESS_KEY = "YOUR_AWS_ACCESS_KEY"
AWS_SECRET_KEY = "YOUR_AWS_SECRET_KEY"
AWS_BUCKET_NAME = "YOUR_BUCKET_NAME"
AWS_REGION = "us-east-1"  # Change to your region

# Initialize S3 client
s3_client = boto3.client(
    's3',
    aws_access_key_id=AWS_ACCESS_KEY,
    aws_secret_access_key=AWS_SECRET_KEY,
    region_name=AWS_REGION
)

# Twelve Labs Configuration
TWELVE_LABS_API_KEY = "YOUR_TWELVE_LABS_API_KEY"
twelvelabs_client = TwelveLabs(api_key=TWELVE_LABS_API_KEY)

# Qdrant Configuration
QDRANT_HOST = "YOUR_QDRANT_HOST"
QDRANT_API_KEY = "YOUR_QDRANT_API_KEY"
COLLECTION_NAME = "content_collection"
VECTOR_SIZE = 1024  # Size of embeddings from Twelve Labs

# Initialize Qdrant client
qdrant_client = QdrantClient(
    url=f"https://{QDRANT_HOST}",
    api_key=QDRANT_API_KEY,
    timeout=20,
    prefer_grpc=False
)

When configuring the Qdrant client, set timeout=20 to prevent indefinite waiting and avoid delays. Set prefer_grpc=False since Flask natively supports HTTP REST APIs rather than gRPC. This ensures smooth communication between Flask and Qdrant using standard HTTP.

Define the video directory and access the MP4 folder within it. This setup is essential for the subsequent processing steps.

# Get a list of video files
video_dir = "downloads/video_content"
video_files = [f for f in os.listdir(video_dir) if f.endswith('.mp4')]


Step 3 - Uploading Videos to AWS S3

First, we need a function to upload videos to AWS S3 Bucket and generate public URLs. The public URL is returned and stored as metadata for each video.

def upload_to_s3(file_path, filename):
    try:
        # Upload the file
        s3_client.upload_file(
            file_path,
            AWS_BUCKET_NAME,
            f"videos-embed/{filename}",
            ExtraArgs={
                'ACL': 'public-read',
                'ContentType': 'video/mp4'
            }
        )

        # Generate the public URL
        url = f"https://{AWS_BUCKET_NAME}.s3.{AWS_REGION}.amazonaws.com/videos-embed/{filename}"
        print(f"Uploaded to S3: {url}")
        return url

    except ClientError as e:
        print(f"Error uploading to S3: {str(e)}")
        raise


Step 4 -  Generating Video Embedding with Marengo 2.7

Now we'll create a function to generate video embeddings using TwelveLabs Marengo-retrieval-2.7 Engine. You can find the code for the video embedding generation and insertion here.

def create_video_embedding(video_path, max_retries=3, retry_delay=5):
    if not twelvelabs_client:
        raise ValueError("Twelve Labs API key not configured")

    retries = 0
    while retries < max_retries:
        try:
            print(f"Creating whole video embedding for {video_path}... (Attempt {retries+1}/{max_retries})")

            # Use video_embedding_scopes parameter set to ["clip", "video"] to get whole video embedding
            task = twelvelabs_client.embed.task.create(
                model_name="Marengo-retrieval-2.7",
                video_file=video_path,
                video_embedding_scopes=["clip", "video"]
            )

            print(f"Created task: id={task.id}, status={task.status}")
            task.wait_for_done(sleep_interval=3)
            task_result = twelvelabs_client.embed.task.retrieve(task.id)

            if task_result.status != 'ready':
                raise ValueError(f"Task failed with status: {task_result.status}")

            return task_result

        except Exception as e:
            print(f"Error creating embedding (attempt {retries+1}): {str(e)}")
            retries += 1
            if retries < max_retries:
                print(f"Retrying in {retry_delay} seconds...")
                time.sleep(retry_delay)
                retry_delay *= 2
            else:
                print("Max retries reached, giving up.")
                raise

The video_embedding_scopes is set to both clip and video to generate embeddings for the entire video clip.


Step 5 - Insertion of Embeddings into Qdrant with metadata

Let's create a function to store the generated embeddings in our Qdrant vector database. The video embedding is stored as a point with metadata payload, improving searchability. The video URL in the metadata enables streaming.

def store_in_qdrant(task_result, video_id, s3_url, original_filename):
    if not qdrant_client:
        raise ValueError("Qdrant client not configured")

    try:
        print(f"Processing video embedding for {video_id}...")

        # The embedding will be in the segments with embedding_scope="video"
        if task_result.video_embedding and task_result.video_embedding.segments:
            video_segments = [s for s in task_result.video_embedding.segments
                             if hasattr(s, 'embedding_scope') and s.embedding_scope == 'video']

            if video_segments:
                print(f"Found video-scope embedding")
                embedding_vector = video_segments[0].embeddings_float
            else:
                # If no video scope segment is found, use the first segment as fallback
                print(f"No video-scope embedding found, using first available segment")
                embedding_vector = task_result.video_embedding.segments[0].embeddings_float
        else:
            raise ValueError("No embeddings found in the response")

        # Create a unique point structure for Qdrant storage
        point = PointStruct(
            id=uuid.uuid4().int & ((1<<64)-1), # Generate a unique 64-bit integer ID
            vector=embedding_vector, # Store the extracted embedding vector
            payload={
                'video_id': video_id,
                'video_url': s3_url,  # Store the public S3 URL of the video
                'is_url': True,
                'original_filename': original_filename # Save the original filename
            }
        )

	 # Insert the generated embedding point into the Qdrant collection
        qdrant_client.upsert(collection_name=COLLECTION_NAME, points=[point])
        print(f"Stored whole video embedding in Qdrant")
        return 1
    except Exception as e:
        print(f"Error storing in Qdrant: {str(e)}")
        raise


Step 6 - Video Processing Pipeline

Now we'll define a streamlined flow that connects all components, processing videos through our pipeline.

Process all videos in a directory through the full pipeline:

  1. Upload to AWS S3 Bucket

  2. Generate embeddings using TwelveLabs

  3. Store embeddings in Qdrant

# Process each video
for filename in video_files[:5]:  # Process first 5 videos or you can setup as per convenience
    try:
        print(f"\nProcessing {filename}...")
        video_path = os.path.join(video_dir, filename)
        video_id = f"{str(uuid.uuid4())[:8]}_{filename}"
        
        # Upload to S3
        s3_url = upload_to_s3(video_path, video_id)
        
        # Generate embeddings
        task_result = create_video_embedding(video_path)
        
        # Store in Qdrant
        store_in_qdrant(task_result, video_id, s3_url, filename)
        
        print(f"Successfully processed {filename}")
    except Exception as e:
        print(f"Error processing {filename}: {str(e)}")

The video embeddings and metadata are now stored in the Qdrant Cloud. The next step is to connect our application to the collection for retrieval.


2 - Building Search API with Flask


Step 1 - Setup CORS Origin

To enable cross-origin requests, we need to configure CORS (Cross-Origin Resource Sharing) in our Flask application. This allows web clients hosted on different domains to access our API. You can find the complete backend implementation in app.py.

app = Flask(__name__) 
CORS(app, resources={r"/*": {"origins": "*"}})


Step 2  - Initializing Qdrant Collection

We need to initialize our Qdrant collection if it doesn't exist. This setup ensures our vector database can properly retrieve video embeddings. The TwelveLabs client is initialized to generate search query embeddings for semantic search capability.

# Get credentials from environment variables
API_KEY = os.getenv('API_KEY')
QDRANT_HOST = os.getenv('QDRANT_HOST')
QDRANT_API_KEY = os.getenv('QDRANT_API_KEY')

# Qdrant Configuration
COLLECTION_NAME = "content_collection"
VECTOR_SIZE = 1024 # Dimension of vector embeddings

# Initialize clients
try:
    client = TwelveLabs(api_key=API_KEY)
    qdrant_client = QdrantClient(
        url=f"https://{QDRANT_HOST}",
        api_key=QDRANT_API_KEY,
        timeout=20
    )
    logger.info("Successfully initialized API clients")
except Exception as e:
    logger.error(f"Failed to initialize clients: {str(e)}")
    raise

def init_qdrant():
    try:
	 # Fetch all existing collections
        collections = qdrant_client.get_collections().collections
        collection_exists = any(col.name == COLLECTION_NAME for col in collections)
        if not collection_exists:
            # Create the collection with specified vector configuration if it doesn't exist
            qdrant_client.recreate_collection(
                collection_name=COLLECTION_NAME,
                vectors_config=VectorParams(
                    size=VECTOR_SIZE,
                    distance=Distance.COSINE # Use cosine similarity for retrieval
                )
            )
            logger.info(f"Created collection: {COLLECTION_NAME}")
    except Exception as e:
        logger.error(f"Qdrant initialization error: {str(e)}")
        raise


Step 3 - Creating Simple Search Functionality

This section implements a search endpoint that enables users to search for videos. The endpoint handles search queries, generates embeddings via TwelveLabs, retrieves similar vectors from Qdrant, and returns matching results.

Let's create a search endpoint for video searches.

@app.route('/search', methods=['POST'])
def search():
    # Ensure the request contains JSON data
    if not request.is_json:
        logger.warning("Missing JSON data")
        return jsonify({'error': 'Request must be JSON format'}), 400
        
    # Get and validate query
    data = request.get_json()
    query = data.get('query')
    if not query:
        logger.warning("Empty query parameter")
        return jsonify({'error': 'Missing query parameter'}), 400
    
    logger.info(f"Processing search: '{query}'")
    
    try:
        # Generate embedding for the search query
        formatted_query = f"Recommend - {query}"
        embedding_response = client.embed.create(
            model_name="Marengo-retrieval-2.7",
            text=formatted_query
        )

	 # Get the embedding vector
        vector = embedding_response.text_embedding.segments[0].embeddings_float
        
        # Similarity search from the Qdrant collection
        query_response = qdrant_client.query_points(
            collection_name=COLLECTION_NAME,
            query=vector,
            limit=10,
            with_payload=True
        )
        
        # Extract and format results
        search_results = query_response.points
        logger.info(f"Found {len(search_results)} matching results")
        
        # If no results, return empty list
        if not search_results:
            return jsonify([])
        
        # Build formatted response
        formatted_results = []
        for result in search_results:
            point_id = result.id
            score = float(result.score)
            payload = result.payload
            
            formatted_results.append({
                'video_id': payload.get('video_id', f"video_{point_id}"),
                'filename': payload.get('original_filename', payload.get('filename', 'video.mp4')),
                'start_time': float(payload.get('start_time', 0)),
                'end_time': float(payload.get('end_time', 30)),
                'score': score,
                'confidence': 'high' if score > 0.7 else 'medium',
                'url': payload.get('video_url')
            })
        
        logger.info(f"Returning {len(formatted_results)} results")
        return jsonify(formatted_results)
        
    except Exception as e:
        logger.exception(f"Search error: {str(e)}")
        return jsonify({'error': 'Search failed', 'details': str(e)}), 500

Here's how it works:

  1. Receives a search query from the request

  2. Generates an embedding vector using TwelveLabs

  3. Searches for similar vectors in Qdrant

  4. Returns the matching videos in a structured format


More Ideas to Experiment with the Tutorial

Understanding how applications work helps you create innovative products that meet user needs. Here are some potential use cases for video content embeddings:

🎯 Personalized Ad Insertion — Dynamically insert context-relevant ads into videos.

⚙️ Real-Time Similarity Matching — Instantly find similar videos as new content is uploaded.

📊 Trend Analysis & Insights — Cluster and analyze video trends based on embedding patterns.


Conclusion

This tutorial shows how video understanding creates smarter, more accurate content recommendations. Using TwelveLabs for video embeddings and Qdrant for fast vector search, we've built a system that understands video content beyond manual transcription, tags, or keywords. This approach delivers better recommendations, keeps users engaged, and scales easily with large video collections. As an open-source solution, it can be customized for various industries—from education to entertainment and beyond.


Additional Resources

Learn more about the embedding generation engine—Marengo-retrieval-2.7. To explore TwelveLabs further and enhance your understanding of video content analysis, check out these resources:

We encourage you to use these resources to expand your knowledge and create innovative applications using TwelveLabs video understanding technology.