Hybrid Retrieval: Combining Metadata and Vector Search

Introduction to Hybrid Retrieval

Welcome back! In the previous lesson, we explored the concept of similarity search using cosine similarity to measure the similarity between text embeddings. This foundational knowledge is crucial as we delve into more advanced techniques. Now, we will focus on hybrid retrieval, a powerful approach that combines metadata and vector search to enhance search results. This technique allows us to leverage both the semantic meaning captured in vector embeddings and the structured information available in metadata. By the end of this lesson, you will understand how to implement hybrid retrieval using Qdrant, a vector database that excels in handling such tasks.

Hybrid retrieval leverages the strengths of both metadata and vector search. Metadata provides structured information that can refine search queries, while vector search uses embeddings to understand the semantic meaning of text. By combining these approaches, we can achieve more precise and relevant search outcomes. Let's explore how this works in practice.

Understanding Metadata and Vector Search

Before we dive into the implementation, let's briefly revisit the concepts of metadata and vector search. Metadata refers to structured information that describes the content of a document, such as categories, tags, or author names. It allows us to filter and refine search queries based on specific attributes.

In this lesson, we use “hybrid retrieval” to mean combining vector similarity with payload metadata filters. In other systems, hybrid search may also refer to dense+sparse or keyword+vector score fusion.

Vector search, on the other hand, uses embeddings to capture the semantic meaning of text. By representing text as vectors, we can measure the similarity between different pieces of text, enabling us to perform semantic searches that go beyond simple keyword matching.

Combining metadata and vector search allows us to leverage the strengths of both approaches. Metadata helps us narrow down the search space, while vector search ensures that the results are semantically relevant. This synergy is what makes hybrid retrieval a powerful tool in semantic search systems.

Pipeline overview:

query text -> embedding -> Qdrant vector search -> payload metadata filter -> ranked filtered results

Setting Up Data and Qdrant Collection

Let's set up some sample data using Qdrant. This will help us understand how the data is structured and how it can be queried.

In a real project, move this repeated setup into a helper such as initialize_collection(...) so later examples can focus on the query and filtering logic. We show the setup once here for clarity.

from sentence_transformers import SentenceTransformer
from qdrant_client import QdrantClient
from qdrant_client.http import models
import json
import itertools

# Config
collection_name = "hybrid-demo"
file_path = "./data/corpus.json"
batch_size = 32  # adjust as needed

# Load the embedding model
model = SentenceTransformer("sentence-transformers/all-MiniLM-L6-v2")

# Load documents from JSON file
with open(file_path, "r", encoding="utf-8") as f:
    documents = json.load(f)

# Helper: chunk generator
def chunks(iterable, size):
    it = iter(iterable)
    while True:
        batch = list(itertools.islice(it, size))
        if not batch:
            break
        yield batch

# Initialize Qdrant client (in-memory for demo; replace with real endpoint in production)
client = QdrantClient(":memory:")
vector_dim = model.get_sentence_embedding_dimension()

# Recreate collection if it exists
if client.collection_exists(collection_name):
    client.delete_collection(collection_name)

client.create_collection(
    collection_name=collection_name,
    vectors_config=models.VectorParams(size=vector_dim, distance=models.Distance.COSINE)
)

# Batch encode + upsert
for batch_idx, batch_docs in enumerate(chunks(documents, batch_size)):
    texts = [doc.get("content", "") for doc in batch_docs]
    embeddings = model.encode(texts, show_progress_bar=False).tolist()

    points = [
        models.PointStruct(
            id=batch_idx * batch_size + i,
            vector=emb,
            payload={
                "title": doc.get("title", ""),
                "content": doc.get("content", ""),
                "category": doc.get("category", "unknown"),
                "tags": doc.get("tags", []),
                "date": doc.get("date", "")
            }
        )
        for i, (doc, emb) in enumerate(zip(batch_docs, embeddings))
    ]

    client.upsert(collection_name=collection_name, points=points)
    print(f"Upserted batch {batch_idx+1}, {len(points)} points")

Output:

Upserted batch 1, 32 points
Upserted batch 2, 32 points
Upserted batch 3, 32 points
Upserted batch 4, 32 points
Upserted batch 5, 22 points

In this setup, we load documents from a JSON file and initialize a Qdrant collection. The helper function batches the data, generates embeddings, and upserts the data into the collection, including metadata such as title, category, tags, and date. The expression batch_idx * batch_size + i ensures that each document gets a unique ID across all batches, avoiding any ID collisions. This data will be used in our hybrid retrieval examples.

Implementing Hybrid Retrieval with Qdrant

Let's walk through the process of implementing hybrid retrieval using Qdrant. We'll use a code snippet to demonstrate how to perform a hybrid search by combining metadata and vector retrieval, especially with ambiguous queries that could belong to multiple categories.

For example, consider the query "Python". In your dataset, "Python" can refer to either the programming language (in the "Technology" category) or the snake (in the "Travel" category).

By applying different metadata filters, hybrid retrieval helps disambiguate the results and return contextually relevant documents for each category.

# Example of hybrid search with an ambiguous query
query_text = "Python"
query_embedding = model.encode(query_text).tolist()

# Hybrid search: metadata + vector retrieval for Technology
search_response_tech = client.query_points(
    collection_name=collection_name,
    query=query_embedding,
    limit=3,
    with_payload=True,
    query_filter=models.Filter(
        must=[models.FieldCondition(
            key="category",
            match=models.MatchValue(value="Technology")
        )]
    )
)
print("\nHybrid retrieval results for category 'Technology':")
for result in search_response_tech.points:
    print(f"- {result.payload.get('title', 'Untitled')}")

# Hybrid search: metadata + vector retrieval for Travel
search_response_travel = client.query_points(
    collection_name=collection_name,
    query=query_embedding,
    limit=3,
    with_payload=True,
    query_filter=models.Filter(
        must=[models.FieldCondition(
            key="category",
            match=models.MatchValue(value="Travel")
        )]
    )
)
print("\nHybrid retrieval results for category 'Travel':")
for result in search_response_travel.points:
    print(f"- {result.payload.get('title', 'Untitled')}")

Output:

Hybrid retrieval results for category 'Technology':
- Python: The Versatile Programming Language
- Revolutionizing Computing with AI
- Large Language Models: Expanding the Horizons of AI

Hybrid retrieval results for category 'Travel':
- Spotting Pythons in the Wild
- Adventures in Backpacking and Road Trips
- Adventure Tourism: Pushing Boundaries

In this example, we use the ambiguous query "Python". By applying the metadata filter for category: Technology, the search returns documents about the Python programming language. When we switch the filter to category: Travel, the search returns documents about pythons as animals in the context of wildlife and travel. This demonstrates how hybrid retrieval can resolve ambiguity and provide results that are relevant to the user's intent based on context.

The search_response_tech.points object contains a list of results, where each result includes the document's payload (such as title, content, category, tags, and date), the vector ID, and the similarity score. You can access the metadata fields from the payload to display or further process the search results.

Example: Performing a Hybrid Search

Let's look at another example using a different ambiguous query, "Coach", which in your dataset can refer to either a business coach or a mode of travel (bus).

# Example of hybrid search with an ambiguous query
query_text = "Coach"
query_embedding = model.encode(query_text).tolist()

# Hybrid search: metadata + vector retrieval for Business
search_response_business = client.query_points(
    collection_name=collection_name,
    query=query_embedding,
    limit=3,
    with_payload=True,
    query_filter=models.Filter(
        must=[models.FieldCondition(
            key="category",
            match=models.MatchValue(value="Business")
        )]
    )
)
print("\nHybrid retrieval results for category 'Business':")
for result in search_response_business.points:
    print(f"- {result.payload.get('title', 'Untitled')}")

# Hybrid search: metadata + vector retrieval for Travel
search_response_travel = client.query_points(
    collection_name=collection_name,
    query=query_embedding,
    limit=3,
    with_payload=True,
    query_filter=models.Filter(
        must=[models.FieldCondition(
            key="category",
            match=models.MatchValue(value="Travel")
        )]
    )
)
print("\nHybrid retrieval results for category 'Travel':")
for result in search_response_travel.points:
    print(f"- {result.payload.get('title', 'Untitled')}")

Output:

Hybrid retrieval results for category 'Business':
- Coach: Leadership in Modern Business
- Corporate Leadership in the 21st Century
- Innovative Strategies in Modern Business

Hybrid retrieval results for category 'Travel':
- Coach Travel Across Europe
- Sports Tourism: Exploring Destinations Through Games
- Adventure Tourism: Pushing Boundaries

By running the same query with different metadata filters, you can observe how hybrid retrieval surfaces the most relevant documents for each context—either a business coach or coach travel—demonstrating the power and flexibility of combining metadata and vector search.

Common Challenges and Troubleshooting

As you implement hybrid retrieval, you may encounter some common challenges. One potential issue is ensuring that the metadata filters are correctly defined and match the structure of your dataset. It's important to verify that the metadata fields used in the filter parameter exist in your data.

Another challenge is understanding how the two parts interact. In the examples above, metadata filters are hard constraints: Qdrant restricts results to points whose payload matches the filter, then vector similarity ranks the candidates within that filtered set. If you need weighted score fusion, implement that as a separate reranking or scoring step.

If you encounter any issues, double-check your code for syntax errors and ensure that your environment is set up correctly. With practice, you'll become more comfortable troubleshooting and resolving these challenges.

Summary and Next Steps

In this lesson, we explored the concept of hybrid retrieval and its role in enhancing search accuracy by combining metadata and vector search. We implemented a hybrid search using Qdrant and demonstrated how to execute queries that leverage both metadata and semantic understanding.

As you move on to the practice exercises, focus on applying what you've learned about hybrid retrieval. Experiment with different queries and metadata filters to see how they affect the search results. Mastering hybrid retrieval will provide a strong foundation for more advanced search techniques in future lessons.

Sign up

Join the 1M+ learners on CodeSignal

Be a part of our community of 1M+ users who develop and demonstrate their skills on CodeSignal