A practical guide to lexical + semantic retrieval, score fusion, reranking, Python implementation, and production RAG architecture
Introduction
A common mistake when building a RAG system is assuming that vector search is enough.
You convert documents into embeddings, store them in a vector database, retrieve the top-k chunks, and send them to the LLM.
It works.
Until a user asks:
“What is the error
SettingWithCopyWarning?”
or:
“What does Section 12.4(b) say?”
or:
“How do I use
pd.read_parquet()?”
These queries contain exact terms that matter.
A semantic retriever may understand the general meaning of the query, but a lexical retriever such as BM25 can be much better at matching exact keywords, identifiers, error messages, and technical terminology.
On the other hand, consider:
“How can I make my LLM application cheaper?”
A document might say:
“Reducing inference expenditure through quantization and model routing.”
There may be little exact word overlap, but the meanings are closely related.
This is where hybrid search becomes useful.
Instead of asking:
BM25 or vector search?
we combine:
User Query
│
┌──────────┴──────────┐
↓ ↓
BM25 Search Vector Search
│ │
↓ ↓
Keyword Results Semantic Results
│ │
└──────────┬──────────┘
↓
Score Fusion
↓
Reranker
↓
Top-K Chunks
↓
LLM
↓
Final Answer
The result is a retrieval system that can capture both lexical relevance and semantic relevance.
In this tutorial, we’ll build hybrid search from first principles and then discuss how to use it in production RAG systems.
Table of Contents
- What Is Hybrid Search?
- Why Vector Search Alone Isn’t Enough
- Why BM25 Alone Isn’t Enough
- BM25 vs Vector Search
- How Hybrid Search Works
- Hybrid Search Architecture
- Step 1 — Prepare Documents
- Step 2 — Implement BM25 Retrieval
- Step 3 — Implement Vector Retrieval
- Step 4 — Normalize Scores
- Step 5 — Combine Results
- Complete Python Example
- Reciprocal Rank Fusion
- Hybrid Search with Reranking
- Hybrid Search in RAG
- When Hybrid Search Works Best
- Common Mistakes
- Production Considerations
- How to Evaluate Hybrid Search
- Interview Questions
- Key Takeaways
- Related Tutorials
- Next Tutorial
What You’ll Learn
By the end of this tutorial, you’ll understand:
- What hybrid search is
- Why lexical and semantic retrieval complement each other
- How BM25 works inside a hybrid retriever
- How vector search contributes semantic matching
- How to combine retrieval results
- Score normalization
- Weighted score fusion
- Reciprocal Rank Fusion
- Reranking
- How hybrid retrieval fits into RAG
- How to evaluate hybrid retrieval
- Production considerations
Prerequisites
You should be familiar with:
- Python
- Basic information retrieval
- BM25
- Embeddings
- Vector databases
- RAG
If you’re new to BM25, read:
BM25 Explained: How Search Engines Find the Most Relevant Documents
before continuing.
1. What Is Hybrid Search?
Hybrid search combines multiple retrieval strategies to retrieve relevant documents.
The most common combination is:
BM25+Vector Search
BM25 performs lexical retrieval.
Vector search performs semantic retrieval.
So instead of relying on one signal:
Query ↓Vector Search ↓Documents
we use:
Query
│
┌────────┴────────┐
↓ ↓
BM25 Embeddings
↓ ↓
Keyword Search Semantic Search
│ │
└────────┬────────┘
↓
Combination
↓
Final Ranking
This is particularly useful for RAG systems because enterprise queries often contain a mixture of:
- natural language
- exact keywords
- technical terminology
- identifiers
- product names
- document sections
- error messages
2. Why Vector Search Alone Isn’t Enough
Vector search works by converting text into numerical representations called embeddings.
For example:
"How can I reduce LLM inference costs?"
might become:
[0.12, -0.45, 0.87, ...]
A document is converted into another vector.
Then we calculate similarity, commonly using:
Cosine Similarity
Conceptually:
Query Embedding │ ↓Vector Database │ ↓Nearest Neighbors │ ↓Relevant Documents
This works very well for semantic queries.
For example:
Query:How can I reduce the cost of running an LLM?Document:Techniques for reducing inference expenditure includequantization, caching, batching and model routing.
The words aren’t identical, but the meanings are related.
However, vector search can struggle with exact identifiers.
Consider:
"CUDA error 718"
or:
"EC2 i4i.2xlarge"
or:
"Article 12.4(b)"
or:
"SettingWithCopyWarning"
These are cases where exact lexical matching is valuable.
3. Why BM25 Alone Isn’t Enough
BM25 has the opposite limitation.
It is excellent at matching words.
But it doesn’t inherently understand semantic similarity.
Consider:
Query:How do I make inference cheaper?
Document:
Quantization reduces the computational and memoryrequirements of model serving.
The terms don’t overlap much.
BM25 may rank this document lower than it should.
Vector search can recognize that:
cheaper inference
and:
reduced model serving cost
are semantically related.
So:
BM25→ exact matchingVector Search→ semantic matching
This is the fundamental motivation behind hybrid search.
4. BM25 vs Vector Search
| Capability | BM25 | Vector Search |
|---|---|---|
| Exact keyword matching | Excellent | Variable |
| Semantic similarity | Limited | Excellent |
| Error messages | Excellent | Variable |
| Product IDs | Excellent | Variable |
| Technical terms | Excellent | Good |
| Synonyms | Limited | Excellent |
| Natural-language questions | Good | Excellent |
| Zero-shot semantic matching | No | Yes |
| RAG retrieval | Excellent | Excellent |
Neither approach is universally better.
They retrieve using different signals.
5. How Hybrid Search Works
Suppose the user asks:
How do I fix CUDA out of memory during PyTorch training?
BM25 might return:
1. CUDA Out of Memory Errors2. PyTorch GPU Memory Management3. CUDA Memory Troubleshooting
Vector search might return:
1. Reducing GPU memory consumption during training2. Gradient checkpointing3. Optimizing batch size
Notice that both systems found useful information, but for different reasons.
We can merge these candidates:
BM25 Results │ ├── Document A ├── Document B └── Document C Vector Results │ ├── Document D ├── Document E └── Document F ↓ Candidate Pool ↓ Final Ranking
6. Hybrid Search Architecture
A production RAG pipeline can look like this:
User Query
│
▼
Query Preprocessing
│
┌────────────┴────────────┐
│ │
▼ ▼
BM25 Index Embedding Model
│ │
▼ ▼
Lexical Retrieval Vector Retrieval
│ │
└────────────┬────────────┘
▼
Result Fusion
│
▼
Top-N Candidates
│
▼
Cross Encoder
Reranker
│
▼
Top-K Chunks
│
▼
LLM
│
▼
Final Answer
This architecture separates retrieval into multiple stages.
7. Step 1 — Prepare Documents
Let’s start with a small collection.
documents = [ "Python pandas DataFrame tutorial", "How to fix SettingWithCopyWarning in pandas", "PyTorch CUDA out of memory troubleshooting", "Reducing LLM inference costs with quantization", "Building RAG systems with vector databases", "Hybrid search using BM25 and embeddings"]
In a real RAG application, these would typically be chunks generated from:
PDFsWeb pagesDocumentationDatabase recordsInternal knowledge bases
8. Step 2 — Implement BM25 Retrieval
Install the library:
pip install rank-bm25
Tokenize the documents:
from rank_bm25 import BM25Okapitokenized_documents = [ document.lower().split() for document in documents]bm25 = BM25Okapi(tokenized_documents)
Now define a query:
query = "pandas SettingWithCopyWarning"tokenized_query = query.lower().split()
Retrieve documents:
scores = bm25.get_scores(tokenized_query)
Rank them:
ranked_documents = sorted( zip(documents, scores), key=lambda x: x[1], reverse=True)for document, score in ranked_documents: print(score, document)
BM25 will strongly favor documents containing the exact query terms.
9. Step 3 — Implement Vector Retrieval
Now let’s add semantic search.
For a simple demonstration, we can use Sentence Transformers.
Install:
pip install sentence-transformers
Load an embedding model:
from sentence_transformers import SentenceTransformermodel = SentenceTransformer( "all-MiniLM-L6-v2")
Generate document embeddings:
document_embeddings = model.encode( documents, normalize_embeddings=True)
Generate the query embedding:
query_embedding = model.encode( query, normalize_embeddings=True)
Calculate cosine similarity:
import numpy as npsimilarities = ( document_embeddings @ query_embedding)
Rank the results:
vector_results = sorted( zip(documents, similarities), key=lambda x: x[1], reverse=True)for document, score in vector_results: print(score, document)
Now we have two independent retrieval systems:
BM25 ↓Lexical scoresVector Search ↓Semantic scores
10. Step 4 — Normalize Scores
Here’s an important problem.
BM25 scores and vector similarity scores are on different scales.
For example:
BM25:Document A → 8.4Document B → 4.2Document C → 1.7
Vector similarity:
Document A → 0.82Document B → 0.71Document C → 0.43
You can’t blindly add them:
final_score = bm25_score + vector_score
because the scales aren’t comparable.
One simple approach is min-max normalization.
def normalize(scores): scores = np.array(scores) min_score = scores.min() max_score = scores.max() if max_score == min_score: return np.zeros_like(scores) return ( (scores - min_score) / (max_score - min_score) )
Apply it:
bm25_scores = normalize(
[score for _, score in ranked_documents]
) vector_scores = normalize(
[score for _, score in vector_results]
)
Now both scores are approximately:
0 → 1
11. Step 5 — Combine Results
A simple weighted fusion approach is:

For example:
alpha = 0.7
means:
70% semantic relevance30% lexical relevance
Implementation:
hybrid_score = ( alpha * vector_score + (1 - alpha) * bm25_score)
For example:
alpha = 0.7final_score = ( 0.7 * vector_score + 0.3 * bm25_score)
However, this isn’t the only way to combine rankings.
And in many systems, Reciprocal Rank Fusion (RRF) is a better starting point.
12. Reciprocal Rank Fusion
Instead of combining raw scores, we can combine rankings.
This is called:
Reciprocal Rank Fusion (RRF).
The formula is:

where:
d= documentr= retrieval systemrank= document’s rankk= constant
The idea is simple.
Suppose:
BM25
1. Document A2. Document B3. Document C
Vector Search
1. Document C2. Document A3. Document D
RRF rewards documents that consistently appear near the top.
For example:
Document ABM25 rank = 1Vector rank = 2Document CBM25 rank = 3Vector rank = 1
Both are strong candidates.
13. Python RRF Implementation
def reciprocal_rank_fusion( rankings, k=60): scores = {} for ranking in rankings: for rank, document in enumerate( ranking, start=1 ): scores[document] = ( scores.get(document, 0) + 1 / (k + rank) ) return sorted( scores.items(), key=lambda x: x[1], reverse=True )
Now we can create separate rankings:
bm25_ranking = [ "Document A", "Document B", "Document C"]vector_ranking = [ "Document C", "Document A", "Document D"]
Fuse them:
result = reciprocal_rank_fusion( [ bm25_ranking, vector_ranking ])print(result)
RRF is attractive because it avoids the problem of comparing incompatible score scales.
14. Hybrid Search With Reranking
Retrieval doesn’t necessarily have to end after score fusion.
A stronger architecture is:
Query
│
┌─────────┴─────────┐
↓ ↓
BM25 Vector Search
│ │
└─────────┬─────────┘
↓
Candidate Pool
│
↓
Cross Encoder
│
↓
Top-K
│
↓
LLM
For example:
BM25 → Top 20Vector → Top 20 ↓ Merge ↓ 30 unique chunks ↓ Reranker ↓ Top 5 ↓ LLM
Why do this?
Because retrieval and reranking solve different problems.
Retrieval
Fast candidate generation.
Reranking
More expensive relevance estimation.
This gives us:
High recall+High precision
15. Hybrid Search in RAG
Now let’s connect everything to RAG.
A complete architecture might be:
User
│
▼
Query
│
┌─────────────┴─────────────┐
│ │
▼ ▼
BM25 Retriever Vector Retriever
│ │
│ │
└─────────────┬─────────────┘
▼
Result Fusion
│
▼
Candidate Documents
│
▼
Reranker
│
▼
Top-K Chunks
│
▼
Prompt Construction
│
▼
LLM
│
▼
Grounded Answer
This approach is especially useful for enterprise RAG.
For example, imagine an insurance knowledge base.
User asks:
“What does policy clause 7.2 say about accidental hospitalization?”
BM25 can identify:
Clause 7.2
while vector search can identify documents discussing:
accidental hospitalization
Combining both signals increases the chance that the correct chunk reaches the LLM.
16. When Hybrid Search Works Best
Hybrid retrieval is particularly useful for datasets containing:
Technical Documentation
"read_parquet""SparkSession""SettingWithCopyWarning""CUDA out of memory"
Legal Documents
"Section 12.4(b)""Article 17""Clause 8.2"
Financial Documents
"ISIN INE...""Form 10-K""EBITDA"
Healthcare
"ICD-10""HCPCS""drug identifiers"
E-commerce
"SKU-12345""iPhone 17 Pro"
The more important exact terminology is, the more valuable lexical retrieval becomes.
17. When You May Not Need Hybrid Search
Hybrid search isn’t automatically the right solution.
If your dataset consists mostly of:
Natural-language questions+Natural-language documents
and exact identifiers aren’t important, pure semantic retrieval may perform well.
Similarly, adding another retrieval system increases:
- infrastructure complexity
- latency
- operational overhead
- evaluation complexity
Don’t add BM25 simply because “production RAG uses hybrid search.”
Benchmark it.
18. Common Mistakes
Mistake 1: Combining Raw Scores
Avoid:
final_score = bm25_score + vector_score
unless you’ve deliberately calibrated the scores.
They may have completely different distributions.
Use:
- normalized scores
- rank fusion
- RRF
- learned fusion
instead.
Mistake 2: Assuming 50/50 Is Optimal
You might see:
alpha = 0.5
and assume this is correct.
It isn’t necessarily.
For some datasets:
BM25 → 70%Vector → 30%
may perform better.
For others:
BM25 → 20%Vector → 80%
may be better.
Use an evaluation dataset to determine this.
Mistake 3: Retrieving Too Few Candidates
Suppose:
BM25 → Top 3Vector → Top 3
You may miss relevant documents.
A common architecture is:
BM25 → Top 20Vector → Top 20 ↓Merge ↓Rerank ↓Top 5
The exact numbers should depend on latency, corpus size, and evaluation results.
Mistake 4: Ignoring Duplicate Documents
Both retrievers may return the same chunk.
Always deduplicate before reranking.
unique_documents = list( dict.fromkeys( bm25_results + vector_results ))
Mistake 5: Ignoring Chunking
Hybrid retrieval cannot fix poor chunking.
If your chunks are too large:
Large Chunk ├── Topic A ├── Topic B ├── Topic C └── Topic D
the retrieved context may be noisy.
If they’re too small:
Chunk A → incomplete sentenceChunk B → missing contextChunk C → isolated fact
retrieval may lose important information.
Chunking should therefore be evaluated together with retrieval.
19. Production Considerations
A production hybrid RAG system should consider more than retrieval quality.
Latency
Running two retrieval systems means:
BM25+Vector Search
can introduce additional latency.
Run them in parallel when possible:
Query
│
┌───────┴───────┐
↓ ↓
BM25 Vector DB
│ │
└───────┬───────┘
↓
Fusion
Caching
Cache repeated queries.
For example:
Query ↓Hash ↓Cache ↓Retrieved results
This can reduce repeated retrieval work.
Indexing
Maintain:
BM25 Index+Vector Index
when documents are created, updated, or deleted.
Your two indexes must remain reasonably synchronized.
Metadata Filtering
Don’t rely exclusively on retrieval ranking.
Apply metadata filters where appropriate:
document_type = "policy"country = "US"version = "2026"
A good pipeline might be:
Metadata Filter ↓BM25 + Vector Search ↓Fusion ↓Reranking
This reduces irrelevant candidates before the expensive stages.
20. How to Evaluate Hybrid Search
This is one of the most important parts of a production RAG system.
Don’t evaluate only the final LLM answer.
Evaluate retrieval separately.
Create a golden dataset:
QueryExpected Document IDsRelevant Chunks
For example:
| Query | Expected Chunk |
|---|---|
| How does clause 7.2 work? | policy_7_2 |
| What is SettingWithCopyWarning? | pandas_12 |
| How can inference cost be reduced? | llm_cost_03 |
Then measure:
Recall@K
How many relevant documents were retrieved?
Precision@K
How many retrieved documents were actually relevant?
MRR
How high did the first relevant result appear?
nDCG
How good was the overall ranking?
Then evaluate the downstream RAG system:
Retrieval ↓Context Relevance ↓Context Recall ↓Faithfulness ↓Answer Correctness ↓Task Success
The goal isn’t simply:
“Did retrieval improve?”
The real question is:
Did better retrieval improve the user’s outcome?
21. Hybrid Search Interview Questions
1. What is hybrid search?
Hybrid search combines multiple retrieval strategies, commonly BM25 lexical retrieval and vector semantic retrieval.
2. Why combine BM25 and vector search?
Because BM25 is strong at exact lexical matching while vector search is strong at semantic similarity.
3. When would BM25 outperform vector search?
For exact terms such as:
- error messages
- product IDs
- API names
- legal clauses
- technical identifiers
4. How do you combine BM25 and vector scores?
Possible approaches include:
- score normalization + weighted fusion
- Reciprocal Rank Fusion
- learned ranking models
5. Why use RRF?
RRF combines rankings rather than raw scores, avoiding the need to calibrate different score distributions.
6. Where would you put a reranker?
After candidate retrieval and fusion:
BM25 + Vector ↓Candidate Pool ↓Reranker ↓Top-K
7. How would you evaluate a hybrid retriever?
Use:
Recall@KPrecision@KMRRnDCG
and evaluate downstream RAG quality.
8. How would you decide between BM25, vector search, and hybrid search?
Benchmark them against representative production queries.
Don’t choose based purely on architecture trends.
Key Takeaways
Hybrid search isn’t about choosing between traditional information retrieval and modern AI.
It’s about combining their strengths.
Hybrid Retrieval
│
┌─────────┴─────────┐
↓ ↓
BM25 Vector Search
│ │
↓ ↓
Exact Matching Semantic Matching
│ │
└─────────┬─────────┘
↓
Fusion / RRF
↓
Reranker
↓
Top-K
↓
LLM
Remember:
BM25 is strong at exact lexical matching.
Vector search is strong at semantic similarity.
Hybrid search combines both.
Reranking can further improve precision.
And evaluation determines whether the architecture actually works.
The most important production principle is:
Don’t optimize your retrieval architecture based on what is popular. Optimize it based on the queries your users actually ask.
Related Tutorials
RAG Fundamentals
- What Is RAG?
- How RAG Works
- Why LLMs Hallucinate
- Dense vs Sparse Retrieval
- BM25 Explained
- Hybrid Search: BM25 + Vector Search(You are here)
- Cross-Encoder Reranking
- Parent-Child Retrieval
- Multi-Query Retrieval
Advanced RAG
- Self-RAG
- Corrective RAG
- Graph RAG
- Agentic RAG
- RAG Evaluation
- Production RAG Architecture
Next Tutorial
← Previous
BM25 Explained: How Search Engines Find the Most Relevant Documents
You are here:
RAG Fundamentals → Hybrid Search
Next →
Cross-Encoder Reranking: How to Improve RAG Retrieval
Author
Ved Prakash
Senior Data Scientist | AI Engineer | Generative AI
Writing practical tutorials on RAG, LLMs, AI Agents, LangGraph, MCP, Machine Learning, and production AI systems.
1 thought on “Hybrid Search in RAG: Combining BM25 and Vector Search for Better Retrieval”