Production RAG Architecture Patterns for Hybrid Search
Why Pure Vector Search Fails in Production
Vector-only RAG retrieval misses exact matches. A user searching for a specific error code, contract clause number, or product SKU gets semantically similar but wrong results. The embedding model captures meaning, not exact string identity.
BM25-only retrieval misses semantic matches. A user asking about data isolation gets nothing if the documents say tenant separation, because BM25 matches tokens, not concepts.
Hybrid search combines both: BM25 for lexical precision, vector search for semantic understanding. Production RAG systems that skip hybrid search trade retrieval quality for architectural simplicity. The cost shows up in answer accuracy and user trust.
For the broader RAG security architecture, see secure enterprise RAG architecture. For tenant isolation in multi-tenant RAG, see implementing RAG row-level security for multi-tenant AI.
BM25 vs Vector Search: What Each Catches
| Query Type | BM25 | Vector Search | Hybrid |
|---|---|---|---|
| Exact error code (e.g., CUDA OOM) | Strong match | Weak (semantic neighbors) | Strong |
| Synonym query (data isolation vs tenant separation) | Misses | Strong match | Strong |
| Specific document ID or clause number | Strong match | Misses | Strong |
| Conceptual question (how to govern AI) | Partial | Strong match | Strong |
| Multi-word technical query with exact terms | Strong match | Partial | Strong |
BM25 (Best Match 25) is the standard ranking function used by search engines like Elasticsearch and Lucene. It scores documents based on term frequency, inverse document frequency, and document length normalization. The original BM25 paper by Robertson and Zaragoza remains the canonical reference. (BM25 Paper, Foundations and Trends in IR)
Hybrid Search Architecture
User Query
|
+---------------------+
| |
v v
BM25 Search Vector Search
(lexical) (semantic)
| |
v v
Top-K results Top-K results
with BM25 scores with cosine scores
| |
+----------+----------+
|
v
Score Fusion
(RRF or weighted)
|
v
Re-ranked results
|
v
Top-N to LLMThe two retrieval paths run in parallel. Each returns its top-K results with scores. The fusion step combines the two ranked lists into a single ranking. The LLM receives only the top-N fused results.
Score Fusion: Reciprocal Rank Fusion (RRF)
RRF is the most reliable fusion method for combining BM25 and vector search rankings. It does not require score calibration because it uses rank positions, not raw scores.
The formula:
RRF(d) = sum over all ranklists: 1 / (k + rank_in_list(d))Where k is a constant (typically 60) that dampens the influence of high ranks. A document ranked #1 in both lists gets a higher RRF score than a document ranked #1 in one list and #50 in the other.
Implementation in Python:
def reciprocal_rank_fusion(bm25_results, vector_results, k=60, top_n=5):
rrf_scores = {}
for rank, doc in enumerate(bm25_results, start=1):
doc_id = doc['id']
rrf_scores[doc_id] = rrf_scores.get(doc_id, 0) + 1 / (k + rank)
for rank, doc in enumerate(vector_results, start=1):
doc_id = doc['id']
rrf_scores[doc_id] = rrf_scores.get(doc_id, 0) + 1 / (k + rank)
fused = sorted(rrf_scores.items(), key=lambda x: x[1], reverse=True)
return fused[:top_n]RRF is reliable because it does not assume BM25 scores and cosine similarity scores are on the same scale. They are not. BM25 scores can range from 0 to 20+ depending on corpus characteristics. Cosine similarity ranges from -1 to 1. Normalizing these into a common scale is fragile. Rank-based fusion sidesteps the problem entirely.
Weighted Score Fusion (Alternative)
If you need more control over the lexical-semantic balance, weighted fusion allows tuning:
def weighted_fusion(bm25_results, vector_results, alpha=0.5, top_n=5):
# Normalize BM25 scores to 0-1
max_bm25 = max(d['score'] for d in bm25_results) or 1
bm25_norm = {d['id']: d['score'] / max_bm25 for d in bm25_results}
# Vector scores are already 0-1 (cosine similarity)
vec_norm = {d['id']: d['score'] for d in vector_results}
all_docs = set(bm25_norm.keys()) | set(vec_norm.keys())
fused = []
for doc_id in all_docs:
score = alpha * bm25_norm.get(doc_id, 0) + (1 - alpha) * vec_norm.get(doc_id, 0)
fused.append((doc_id, score))
fused.sort(key=lambda x: x[1], reverse=True)
return fused[:top_n]alpha=0.5 gives equal weight. alpha=0.7 favors lexical matching (useful for technical documentation with precise terminology). alpha=0.3 favors semantic matching (useful for conversational queries). The right value depends on your query distribution and should be tuned with evaluation data.
PostgreSQL Implementation with pgvector
If you are using PostgreSQL with pgvector, hybrid search can be implemented in a single query using a CTE:
WITH bm25_results AS (
SELECT id, content,
ts_rank_cd(
to_tsvector('english', content),
plainto_tsquery('english', $1)
) AS bm25_score,
ROW_NUMBER() OVER (
ORDER BY ts_rank_cd(
to_tsvector('english', content),
plainto_tsquery('english', $1)
) DESC
) AS bm25_rank
FROM document_chunks
WHERE to_tsvector('english', content) @@ plainto_tsquery('english', $1)
LIMIT 20
),
vector_results AS (
SELECT id, content,
1 - (embedding <-> $2) AS vector_score,
ROW_NUMBER() OVER (
ORDER BY embedding <-> $2
) AS vector_rank
FROM document_chunks
ORDER BY embedding <-> $2
LIMIT 20
)
SELECT COALESCE(b.id, v.id) AS id,
COALESCE(b.content, v.content) AS content,
COALESCE(1.0 / (60 + b.bm25_rank), 0) +
COALESCE(1.0 / (60 + v.vector_rank), 0) AS rrf_score
FROM bm25_results b
FULL OUTER JOIN vector_results v ON b.id = v.id
ORDER BY rrf_score DESC
LIMIT 5;This query runs both retrieval paths, joins results by document ID, and applies RRF scoring in a single database round trip. (pgvector Documentation)
Retrieval Evaluation Framework
Without evaluation metrics, you cannot tell whether hybrid search is actually better than single-method retrieval. Three metrics matter for RAG retrieval:
| Metric | What It Measures | How to Compute |
|---|---|---|
| Recall@K | Fraction of relevant documents in top-K results | relevant_in_top_K / total_relevant |
| Precision@K | Fraction of top-K results that are relevant | relevant_in_top_K / K |
| MRR (Mean Reciprocal Rank) | Average position of first relevant document | mean(1 / rank_of_first_relevant) |
Build a evaluation set of 50-100 queries with human-labeled relevant documents. Run each retrieval method (BM25-only, vector-only, hybrid) against the set. Compare Recall@5 and MRR. If hybrid does not beat both single methods on your evaluation set, your fusion parameters need tuning.
When to Use Each Retrieval Strategy
- BM25 only: Legal documents, compliance text, technical documentation with precise terminology. Exact match matters more than semantic similarity.
- Vector only: Conversational Q&A, customer support, knowledge base articles. Semantic intent matters more than exact tokens.
- Hybrid: Most production RAG systems. The cost of running both paths is small compared to the retrieval quality gain. Start with RRF (k=60) and tune based on evaluation data.
Production Deployment Checklist
- BM25 index built and tested with exact-match queries
- Vector index built with pgvector or dedicated vector DB
- RRF fusion implemented and tested with mixed query types
- Evaluation set of 50+ queries with labeled relevant documents
- Recall@5 and MRR measured for BM25-only, vector-only, and hybrid
- Hybrid outperforms both single methods on evaluation set
- Latency budget defined (hybrid adds one parallel retrieval path)
- RLS policies applied to both retrieval paths (see RAG row-level security)
- Audit logging captures which retrieval path contributed each result
- Fallback strategy defined if one retrieval path fails
If you need an independent systems advisor to evaluate your RAG retrieval architecture before production deployment, schedule a strategic evaluation.
FAQ
What is hybrid search in RAG? Hybrid search combines BM25 lexical retrieval with vector semantic retrieval. BM25 catches exact matches (error codes, IDs, specific terms). Vector search catches semantic matches (synonyms, conceptual queries). The two ranked lists are fused using reciprocal rank fusion or weighted scoring.
What is reciprocal rank fusion (RRF)? RRF combines multiple ranked lists by scoring each document based on its rank position in each list. The formula is 1 / (k + rank), where k is typically 60. RRF is reliable because it does not require raw scores to be on the same scale, which is important because BM25 scores and cosine similarity scores are not comparable.
Should I use BM25 or vector search for RAG? Use both. BM25-only misses semantic matches. Vector-only misses exact matches. Hybrid search with RRF fusion outperforms either method alone on most query distributions. The cost of running both paths in parallel is small compared to the retrieval quality gain.
How do I implement hybrid search in PostgreSQL? Use a CTE that runs BM25 (ts_rank_cd with to_tsvector) and vector search (pgvector cosine distance) in parallel, then joins results and applies RRF scoring. The full SQL query is included in this article.
How do I evaluate RAG retrieval quality? Build an evaluation set of 50-100 queries with human-labeled relevant documents. Measure Recall@K (fraction of relevant docs in top-K), Precision@K (fraction of top-K that are relevant), and MRR (mean reciprocal rank of first relevant doc). Compare BM25-only, vector-only, and hybrid. If hybrid does not win, tune fusion parameters.
What is the best RRF k value? The standard value is k=60, based on the original RRF research. Higher k values dampen the influence of top ranks more. Lower k values give more weight to top-ranked documents. Start with 60 and tune based on your evaluation set.
Get new articles in your inbox
Occasional emails when I publish something worth reading. Unsubscribe anytime.
Subodh KC
Enterprise AI Advisor & AI Systems Architect. Former Sr. Program Manager, HP Inc. Founder of HAIEC - High Assurance In Every Consequence. Builds production AI systems from decision through operation.

