← AI Agents Memory & Context
🤖 AI Agents

Hybrid Retrieval: BM25 and Vector Search Combined

Source: mortalapps.com
TL;DR
  • Hybrid retrieval combines keyword-based (BM25) and semantic (vector) search to improve relevance.
  • It solves the 'recall problem' where pure vector search misses exact keyword matches and pure keyword search misses semantic synonyms.
  • In production, hybrid retrieval enhances RAG system accuracy, reducing hallucinations and improving agent decision-making.
  • Implementations typically use Reciprocal Rank Fusion (RRF) to merge ranked results from both retrieval methods.
  • This approach is critical for complex queries where both lexical precision and conceptual understanding are required.

Why This Matters

Retrieval Augmented Generation (RAG) systems often struggle with a fundamental challenge: accurately fetching relevant documents for diverse user queries. Pure keyword search, like BM25, excels at exact matches but fails with synonyms or conceptual queries. Conversely, pure vector search (semantic search) captures conceptual similarity but can miss precise keyword occurrences, especially for named entities or specific product codes. This limitation leads to suboptimal context for Large Language Models (LLMs), resulting in less accurate responses or hallucinations.

Hybrid retrieval, specifically combining BM25 and vector search, was invented to address this dual challenge. By leveraging the strengths of both methods, it ensures that both lexical and semantic relevance are considered during document retrieval. Ignoring this approach in production can lead to brittle RAG systems that perform inconsistently across query types, increasing operational costs due to poor LLM output quality and requiring more human intervention. For developers, it means building more robust and reliable agent systems that can handle a wider range of user inputs effectively. From an enterprise perspective, this translates to higher user satisfaction, reduced operational overhead, and more trustworthy AI agent deployments. Use hybrid retrieval when your application requires high precision for specific terms alongside a deep understanding of query intent, especially in domains with varied terminology or complex information needs. For simple, highly structured data, a single retrieval method might suffice, but for general-purpose agents, hybrid is often superior.

Core Concepts

Hybrid retrieval combines multiple search techniques to enhance relevance. This approach typically merges lexical search and semantic search results.

  • Lexical Search (e.g., BM25): Focuses on keyword matching between query and document text. It calculates a score based on term frequency (how often a term appears in a document) and inverse document frequency (how rare a term is across the entire corpus). BM25 is effective for precise keyword recall but struggles with synonyms or conceptual queries.
  • Semantic Search (Vector Search): Utilizes embedding models to convert text into high-dimensional numerical vectors. Documents and queries are represented as vectors, and relevance is determined by the cosine similarity (or other distance metrics) between these vectors. This method excels at understanding the meaning and context of a query, even with different phrasing, but can sometimes miss exact keyword matches.
  • Reciprocal Rank Fusion (RRF): An algorithm used to combine ranked lists from multiple retrieval methods into a single, unified ranked list. RRF assigns a score to each document based on its rank in each individual list, giving higher weight to documents that appear high in multiple lists. It is robust to different scoring scales and does not require score normalization from individual rankers.
  • Retrieval Augmented Generation (RAG): An architecture where an LLM retrieves relevant information from an external knowledge base before generating a response. Hybrid retrieval directly improves the 'retrieval' component of RAG, providing richer, more accurate context to the LLM.
  • Document Chunks: Large documents are often split into smaller, semantically coherent chunks before indexing. This practice improves retrieval granularity, ensuring that only the most relevant portions of a document are fetched, reducing context window pressure and noise for the LLM.

Real BM25 Implementation

In production, replace MockLexicalIndex with the rank_bm25 library:

from rank_bm25 import BM25Okapi

tokenized_corpus = [doc.split() for doc in documents]
bm25 = BM25Okapi(tokenized_corpus)
scores = bm25.get_scores(query.split())

How It Works

Hybrid retrieval operates by executing both lexical and semantic search queries in parallel, then merging their results into a single, optimized list. The process ensures comprehensive coverage for diverse query types.

1. Indexing Phase

Documents are first processed for both lexical and semantic indexing. For lexical search, text is tokenized and inverted indices are built, typically for BM25. Simultaneously, document chunks are converted into vector embeddings using a pre-trained embedding model, which are then stored in a vector database. This dual indexing is crucial for enabling parallel retrieval.

2. Query Execution

When a user submits a query, it is simultaneously sent to both the lexical search engine (e.g., Elasticsearch, Solr) and the vector database. The lexical engine processes the query to identify keyword matches and returns a ranked list of document chunks based on BM25 scores. In parallel, the query is embedded into a vector, which is then used to perform a similarity search against the document embeddings in the vector database, yielding another ranked list based on semantic similarity.

3. Result Merging (Reciprocal Rank Fusion)

Each retrieval method produces a ranked list of document chunks. These lists, often with different scoring scales, are then fed into a Reciprocal Rank Fusion (RRF) algorithm. RRF combines these lists by assigning a score to each unique document based on its rank in each individual list. The formula for RRF for a document d across N rankers is typically RRF_score(d) = sum(1 / (k + rank_i(d))) where rank_i(d) is the rank of document d in the i-th ranker's list, and k is a constant (commonly 60) to prevent division by zero and smooth scores. Documents appearing high in multiple lists receive significantly higher RRF scores.

4. Final Ranking and Context Selection

The RRF algorithm outputs a single, unified ranked list of document chunks. This list is then truncated to the top-N most relevant chunks, which are subsequently used as context for the LLM. This final selection ensures that the LLM receives a balanced perspective, incorporating both keyword precision and semantic understanding.

Failure Cases

  • Empty Results: If both lexical and semantic searches return no relevant documents, the RRF will also yield an empty list. Agents should implement fallback mechanisms, such as broadening the search scope, querying a general knowledge base, or directly informing the user of the lack of information.
  • Disparate Relevance: In cases where one search method yields highly relevant results and the other yields very poor ones, RRF can still provide a reasonable combined list. However, if the k parameter is not tuned, or if one ranker consistently provides noise, the overall quality can degrade. Monitoring retrieval metrics is essential to identify such imbalances.
  • Indexing Lag: If the lexical and vector indices are not synchronized, newly added or updated documents might only appear in one index, leading to incomplete retrieval. Robust indexing pipelines with transactional updates or eventual consistency guarantees are necessary.

Architecture

The conceptual architecture for hybrid retrieval involves distinct components for indexing, retrieval, and fusion. At its core, a Document Store holds the raw text content. This store feeds into two parallel indexing pipelines: a Lexical Indexer and a Vector Indexer. The Lexical Indexer processes text for keyword search, creating an inverted index and storing it in a Lexical Search Engine (e.g., Elasticsearch, OpenSearch). The Vector Indexer uses an Embedding Model to transform document chunks into vectors, which are then stored in a Vector Database (e.g., Pinecone, Weaviate, Chroma).

When a user query arrives, it first hits an API Gateway or Query Orchestrator. This orchestrator dispatches the query in parallel to both the Lexical Search Engine and the Vector Database. The Lexical Search Engine returns a ranked list of document IDs and BM25 scores. Concurrently, the Query Orchestrator sends the query to the Embedding Model to generate a query vector, which is then used by the Vector Database to return a ranked list of document IDs and similarity scores. These two ranked lists flow back to the Query Orchestrator.

The Query Orchestrator then invokes a Reciprocal Rank Fusion (RRF) Module. This module takes the two ranked lists as input, applies the RRF algorithm, and produces a single, unified ranked list of document IDs. Finally, the Query Orchestrator retrieves the actual content for the top-ranked documents from the Document Store and delivers this combined context to the downstream AI Agent or LLM for generation. Execution starts with the user query and ends with the LLM receiving the optimized context.

1. Setting Up the Indexing Environment

Implementing hybrid retrieval requires a robust indexing pipeline capable of handling both lexical and semantic representations of your data. For lexical search, open-source solutions like Elasticsearch or Apache Solr are common. For vector search, dedicated vector databases or vector search capabilities within existing databases (e.g., PostgreSQL with pgvector) are used. The choice depends on scale, existing infrastructure, and feature requirements.

First, define your document structure. Each document should have a unique ID, content, and potentially metadata. Documents are typically chunked into smaller, semantically coherent units (e.g., 200-500 tokens with overlap) to improve retrieval granularity and fit within LLM context windows.

import os
import logging
from typing import List, Dict
from collections import defaultdict

# Configure logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')

class DocumentChunk:
    def __init__(self, id: str, text: str, metadata: Dict = None):
        self.id = id
        self.text = text
        self.metadata = metadata if metadata is not None else {}

    def __repr__(self):
        return f"DocumentChunk(id='{self.id}', text='{self.text[:50]}...', metadata={self.metadata})"

# In a real system, this would be a connection to Elasticsearch/Pinecone
class MockLexicalIndex:
    def __init__(self):
        self.documents = []
        self.inverted_index = defaultdict(list)

    def add_document(self, chunk: DocumentChunk):
        self.documents.append(chunk)
        # Simulate simple inverted index for BM25-like behavior
        for word in chunk.text.lower().split():
            self.inverted_index[word].append(chunk.id)
        logging.info(f"Added document {chunk.id} to lexical index.")

    def search(self, query: str) -> List[Dict]:
        # Simulate BM25: simple keyword matching for demonstration
        query_words = query.lower().split()
        scores = defaultdict(float)
        for word in query_words:
            for doc_id in self.inverted_index[word]:
                scores[doc_id] += 1 # Simple term frequency score
        
        ranked_results = sorted(scores.items(), key=lambda item: item[1], reverse=True)
        return [{'id': doc_id, 'score': score} for doc_id, score in ranked_results]

class MockVectorIndex:
    def __init__(self, embedding_model):
        self.documents = []
        self.embeddings = {}
        self.embedding_model = embedding_model

    def add_document(self, chunk: DocumentChunk):
        embedding = self.embedding_model.encode(chunk.text)
        self.documents.append(chunk)
        self.embeddings[chunk.id] = embedding
        logging.info(f"Added document {chunk.id} to vector index.")

    def search(self, query: str) -> List[Dict]:
        query_embedding = self.embedding_model.encode(query)
        similarities = {}
        for doc_id, doc_embedding in self.embeddings.items():
            # Simulate cosine similarity
            similarity = self._cosine_similarity(query_embedding, doc_embedding)
            similarities[doc_id] = similarity
        
        ranked_results = sorted(similarities.items(), key=lambda item: item[1], reverse=True)
        return [{'id': doc_id, 'score': score} for doc_id, score in ranked_results]

    def _cosine_similarity(self, vec1, vec2):
        dot_product = sum(v1 * v2 for v1, v2 in zip(vec1, vec2))
        magnitude1 = sum(v**2 for v in vec1)**0.5
        magnitude2 = sum(v**2 for v in vec2)**0.5
        if magnitude1 == 0 or magnitude2 == 0:
            return 0.0
        return dot_product / (magnitude1 * magnitude2)

# Placeholder for a real embedding model (e.g., Sentence Transformers)
class MockEmbeddingModel:
    def encode(self, text: str) -> List[float]:
        # Return a dummy vector for demonstration
        # In production, use a model like 'all-MiniLM-L6-v2'
        return [hash(c) % 1000 / 1000.0 for c in text[:10]] # Simple hash-based vector

2. Implementing Reciprocal Rank Fusion (RRF)

RRF is a robust method for combining ranked lists without requiring score normalization. It assigns a score based on the rank of an item in each list. The standard formula for RRF is 1 / (k + rank), where k is a constant (typically 60) and rank is the 1-based position of the item in a list. Higher ranks (smaller numbers) contribute more to the final score.

def reciprocal_rank_fusion(ranked_lists: List[List[Dict]], k: int = 60) -> List[Dict]:
    fused_scores = defaultdict(float)
    
    for ranked_list in ranked_lists:
        for rank, item in enumerate(ranked_list, start=1):
            doc_id = item['id']
            # RRF formula: 1 / (k + rank)
            fused_scores[doc_id] += 1.0 / (k + rank)
            
    # Sort documents by their fused scores in descending order
    sorted_fused_scores = sorted(fused_scores.items(), key=lambda item: item[1], reverse=True)
    
    # Reconstruct the list with original document content (assuming we can retrieve it)
    # For this example, we'll just return the IDs and scores
    return [{'id': doc_id, 'score': score} for doc_id, score in sorted_fused_scores]

3. Orchestrating Hybrid Retrieval

The final step involves orchestrating the parallel calls to both search indexes and applying RRF. This component acts as the central point for a hybrid retrieval system.

class HybridRetriever:
    def __init__(self, lexical_index: MockLexicalIndex, vector_index: MockVectorIndex):
        self.lexical_index = lexical_index
        self.vector_index = vector_index

    def retrieve(self, query: str, top_k: int = 5) -> List[DocumentChunk]:
        logging.info(f"Executing hybrid retrieval for query: '{query}'")
        
        # 1. Execute lexical search
        lexical_results = self.lexical_index.search(query)
        logging.debug(f"Lexical results: {lexical_results}")

        # 2. Execute vector search
        vector_results = self.vector_index.search(query)
        logging.debug(f"Vector results: {vector_results}")

        # 3. Apply Reciprocal Rank Fusion
        fused_results = reciprocal_rank_fusion([lexical_results, vector_results])
        logging.debug(f"Fused results: {fused_results}")

        # 4. Retrieve original document chunks for the top_k results
        retrieved_chunks = []
        for item in fused_results[:top_k]:
            doc_id = item['id']
            # In a real system, fetch the full chunk from a document store
            # For this example, we'll find it from our mock index's documents
            chunk = next((d for d in self.lexical_index.documents if d.id == doc_id), None)
            if chunk:
                retrieved_chunks.append(chunk)
            else:
                logging.warning(f"Document chunk with ID {doc_id} not found in store.")

        logging.info(f"Successfully retrieved {len(retrieved_chunks)} chunks.")
        return retrieved_chunks

4. Tuning and Optimization

  • RRF k parameter: The k constant in RRF influences how quickly the contribution of lower-ranked items diminishes. A higher k gives more weight to lower-ranked items, while a lower k emphasizes top ranks. Tuning k requires empirical evaluation against a representative dataset.
  • Number of results from each ranker: While RRF is robust, sending an excessive number of low-relevance results from individual rankers can dilute the quality of the fused list. It's common to limit the number of results from each individual search (e.g., top_100 from lexical, top_100 from vector) before fusion.
  • Embedding Model Selection: The quality of the vector search is directly dependent on the chosen embedding model. Select a model that is well-suited for your domain and language. Consider fine-tuning if domain-specific performance is critical.
  • Chunking Strategy: The size and overlap of document chunks significantly impact retrieval. Overlapping chunks can help capture context that spans chunk boundaries, while smaller chunks can improve precision. Experimentation is key.

Code Example

This example demonstrates a basic hybrid retrieval system combining mock lexical (BM25-like) and vector search results using Reciprocal Rank Fusion (RRF). It showcases the indexing of documents, parallel querying, and the fusion logic.
Python
import os
import logging
from typing import List, Dict
from collections import defaultdict

# Configure logging
logging.basicConfig(level=os.environ.get('LOG_LEVEL', 'INFO').upper(), format='%(asctime)s - %(levelname)s - %(message)s')

class DocumentChunk:
    def __init__(self, id: str, text: str, metadata: Dict = None):
        self.id = id
        self.text = text
        self.metadata = metadata if metadata is not None else {}

    def __repr__(self):
        return f"DocumentChunk(id='{self.id}', text='{self.text[:50]}...', metadata={self.metadata})"

    def to_dict(self):
        return {'id': self.id, 'text': self.text, 'metadata': self.metadata}

class MockLexicalIndex:
    """Simulates a lexical search engine (e.g., BM25) with simple keyword matching."""
    def __init__(self):
        self.documents: List[DocumentChunk] = []
        self.inverted_index = defaultdict(list)
        self.doc_map = {}

    def add_document(self, chunk: DocumentChunk):
        if chunk.id not in self.doc_map:
            self.documents.append(chunk)
            self.doc_map[chunk.id] = chunk
            for word in chunk.text.lower().split():
                self.inverted_index[word].append(chunk.id)
            logging.debug(f"Added document {chunk.id} to lexical index.")

    def search(self, query: str) -> List[Dict]:
        query_words = query.lower().split()
        scores = defaultdict(float)
        for word in query_words:
            for doc_id in self.inverted_index[word]:
                scores[doc_id] += 1 # Simple term frequency score
        
        ranked_results = sorted(scores.items(), key=lambda item: item[1], reverse=True)
        return [{'id': doc_id, 'score': score} for doc_id, score in ranked_results]

class MockEmbeddingModel:
    """Placeholder for a real embedding model (e.g., Sentence Transformers)."""
    def encode(self, text: str) -> List[float]:
        # In production, use a model like 'all-MiniLM-L6-v2' from sentence-transformers
        # This mock returns a simple hash-based vector for demonstration
        if not text:
            return [0.0] * 10 # Return a zero vector for empty text
        return [float(hash(c) % 1000) / 1000.0 for c in text[:10].ljust(10)] # Ensure fixed size

    def _cosine_similarity(self, vec1, vec2):
        if not vec1 or not vec2 or len(vec1) != len(vec2):
            return 0.0
        dot_product = sum(v1 * v2 for v1, v2 in zip(vec1, vec2))
        magnitude1 = sum(v**2 for v in vec1)**0.5
        magnitude2 = sum(v**2 for v in vec2)**0.5
        if magnitude1 == 0 or magnitude2 == 0:
            return 0.0
        return dot_product / (magnitude1 * magnitude2)

class MockVectorIndex:
    """Simulates a vector database with cosine similarity search."""
    def __init__(self, embedding_model: MockEmbeddingModel):
        self.documents: List[DocumentChunk] = []
        self.embeddings = {}
        self.embedding_model = embedding_model
        self.doc_map = {}

    def add_document(self, chunk: DocumentChunk):
        if chunk.id not in self.doc_map:
            embedding = self.embedding_model.encode(chunk.text)
            self.documents.append(chunk)
            self.embeddings[chunk.id] = embedding
            self.doc_map[chunk.id] = chunk
            logging.debug(f"Added document {chunk.id} to vector index.")

    def search(self, query: str) -> List[Dict]:
        query_embedding = self.embedding_model.encode(query)
        similarities = {}
        for doc_id, doc_embedding in self.embeddings.items():
            similarity = self.embedding_model._cosine_similarity(query_embedding, doc_embedding)
            similarities[doc_id] = similarity
        
        ranked_results = sorted(similarities.items(), key=lambda item: item[1], reverse=True)
        return [{'id': doc_id, 'score': score} for doc_id, score in ranked_results]

def reciprocal_rank_fusion(ranked_lists: List[List[Dict]], k: int = 60) -> List[Dict]:
    """Applies Reciprocal Rank Fusion to a list of ranked lists."""
    fused_scores = defaultdict(float)
    
    for ranked_list in ranked_lists:
        for rank, item in enumerate(ranked_list, start=1):
            doc_id = item['id']
            fused_scores[doc_id] += 1.0 / (k + rank)
            
    sorted_fused_scores = sorted(fused_scores.items(), key=lambda item: item[1], reverse=True)
    return [{'id': doc_id, 'score': score} for doc_id, score in sorted_fused_scores]

class HybridRetriever:
    """Orchestrates hybrid retrieval using lexical and vector indexes."""
    def __init__(self, lexical_index: MockLexicalIndex, vector_index: MockVectorIndex):
        self.lexical_index = lexical_index
        self.vector_index = vector_index

    def retrieve(self, query: str, top_k: int = 5) -> List[DocumentChunk]:
        logging.info(f"Executing hybrid retrieval for query: '{query}'")
        
        # Execute lexical search
        lexical_results = self.lexical_index.search(query)
        logging.debug(f"Lexical results: {lexical_results}")

        # Execute vector search
        vector_results = self.vector_index.search(query)
        logging.debug(f"Vector results: {vector_results}")

        # Apply Reciprocal Rank Fusion
        fused_results = reciprocal_rank_fusion([lexical_results, vector_results])
        logging.debug(f"Fused results: {fused_results}")

        # Retrieve original document chunks for the top_k results
        retrieved_chunks = []
        for item in fused_results[:top_k]:
            doc_id = item['id']
            chunk = self.lexical_index.doc_map.get(doc_id) # Assuming doc_map is consistent
            if chunk:
                retrieved_chunks.append(chunk)
            else:
                logging.warning(f"Document chunk with ID {doc_id} not found in store.")

        logging.info(f"Successfully retrieved {len(retrieved_chunks)} chunks.")
        return retrieved_chunks

if __name__ == "__main__":
    # Set LOG_LEVEL to DEBUG in environment for more verbose output
    # os.environ['LOG_LEVEL'] = 'DEBUG'
    
    # Initialize mock components
    embedding_model = MockEmbeddingModel()
    lexical_index = MockLexicalIndex()
    vector_index = MockVectorIndex(embedding_model)

    # Sample documents (DocumentChunks)
    docs = [
        DocumentChunk("doc1", "The quick brown fox jumps over the lazy dog.", {"source": "wiki"}),
        DocumentChunk("doc2", "A fast brown canine leaps over a sluggish hound.", {"source": "thesaurus"}),
        DocumentChunk("doc3", "Artificial intelligence agents require robust memory systems.", {"source": "mortalapps"}),
        DocumentChunk("doc4", "Memory management in AI agents is crucial for context handling.", {"source": "mortalapps"}),
        DocumentChunk("doc5", "The red car is parked in the garage.", {"source": "daily_log"})
    ]

    # Add documents to both indexes
    for doc in docs:
        lexical_index.add_document(doc)
        vector_index.add_document(doc)

    # Initialize hybrid retriever
    hybrid_retriever = HybridRetriever(lexical_index, vector_index)

    # Test queries
    query1 = "fast fox"
    print(f"
--- Query: '{query1}' ---")
    results1 = hybrid_retriever.retrieve(query1, top_k=2)
    for r in results1:
        print(f"  Retrieved: {r.id} - {r.text[:70]}...")

    query2 = "AI agent memory"
    print(f"
--- Query: '{query2}' ---")
    results2 = hybrid_retriever.retrieve(query2, top_k=2)
    for r in results2:
        print(f"  Retrieved: {r.id} - {r.text[:70]}...")

    query3 = "sluggish canine"
    print(f"
--- Query: '{query3}' ---")
    results3 = hybrid_retriever.retrieve(query3, top_k=2)
    for r in results3:
        print(f"  Retrieved: {r.id} - {r.text[:70]}...")

    query4 = "red car"
    print(f"
--- Query: '{query4}' ---")
    results4 = hybrid_retriever.retrieve(query4, top_k=1)
    for r in results4:
        print(f"  Retrieved: {r.id} - {r.text[:70]}...")
Expected Output
--- Query: 'fast fox' ---
  Retrieved: doc1 - The quick brown fox jumps over the lazy dog...
  Retrieved: doc2 - A fast brown canine leaps over a sluggish hound...

--- Query: 'AI agent memory' ---
  Retrieved: doc3 - Artificial intelligence agents require robust memory systems...
  Retrieved: doc4 - Memory management in AI agents is crucial for context handling...

--- Query: 'sluggish canine' ---
  Retrieved: doc2 - A fast brown canine leaps over a sluggish hound...
  Retrieved: doc1 - The quick brown fox jumps over the lazy dog...

--- Query: 'red car' ---
  Retrieved: doc5 - The red car is parked in the garage...

Key Takeaways

Hybrid retrieval combines lexical (keyword) and semantic (vector) search to overcome the limitations of each individually.
Reciprocal Rank Fusion (RRF) is a robust algorithm for merging ranked lists from different retrieval methods without requiring score normalization.
Implementing hybrid retrieval involves parallel querying of lexical and vector indexes, followed by RRF to produce a unified result.
Proper document chunking, embedding model selection, and RRF parameter tuning are critical for optimal hybrid retrieval performance.
Hybrid retrieval significantly enhances the accuracy and robustness of RAG systems, leading to more reliable AI agent responses.
Production deployments require careful consideration of indexing consistency, performance monitoring, and cost management for dual indexing systems.
Hybrid retrieval is particularly beneficial for complex queries that demand both precise keyword matching and conceptual understanding.

Frequently Asked Questions

What is the primary advantage of hybrid retrieval over pure vector search? +
Hybrid retrieval combines the precision of keyword search (BM25) with the conceptual understanding of vector search, ensuring both exact matches and semantic synonyms are captured, which pure vector search might miss.
When should I use hybrid retrieval instead of just BM25? +
Use hybrid retrieval when your queries are diverse, requiring both exact keyword matches (e.g., product IDs, names) and conceptual understanding (e.g., 'tools for collaboration'). BM25 alone struggles with synonyms.
What is Reciprocal Rank Fusion (RRF) and why is it used? +
RRF is an algorithm that merges multiple ranked lists into a single, unified list. It's used because it's robust to different scoring scales from individual rankers and doesn't require complex score normalization.
Does hybrid retrieval increase latency? +
Yes, hybrid retrieval typically increases query latency because it involves executing two separate searches in parallel and then merging their results. The total latency is often dominated by the slower of the two search components plus fusion time.
What are the limitations of hybrid retrieval? +
Limitations include increased infrastructure cost, higher operational complexity, potential for indexing inconsistencies, and the need for careful tuning of fusion parameters like the RRF 'k' value.
How does hybrid retrieval work with existing RAG frameworks like LlamaIndex or LangChain? +
Most RAG frameworks provide abstractions to integrate custom retrievers. You can implement hybrid retrieval as a custom retriever by orchestrating calls to your lexical and vector indexes and applying RRF before returning results to the framework.
What happens if one of the search indexes fails? +
If one index fails, the hybrid system's performance will degrade. Robust implementations should include fallback mechanisms, such as relying solely on the operational index or returning an error if retrieval quality is critically impacted.
Is score normalization required before applying RRF? +
No, one of RRF's key benefits is that it does not require explicit score normalization. It operates on ranks, which are inherently comparable across different scoring systems.
How do I choose the right embedding model for vector search in a hybrid system? +
Select an embedding model that performs well on your specific domain's text data. Evaluate models on relevance tasks using your dataset, considering factors like model size, inference speed, and language support.
Can I combine more than two retrieval methods with RRF? +
Yes, RRF can combine any number of ranked lists. You could integrate other methods like graph-based retrieval or metadata filtering by generating a ranked list from them and including it in the RRF input.