IDInternals Decoded
RAG, Properly
Deep DivesIntermediate12 min readJun 2026

Hybrid Search and Re-Ranking: The Cheapest Quality Win

Keywords catch what vectors miss, and re-rankers fix the order. Most teams skip both.

Part 5 of 8RAG, ProperlyView series →

Hybrid search runs two retrieval methods in parallel, BM25 for exact keyword matching and vector search for semantic similarity, then fuses their results using reciprocal rank fusion. Adding a cross-encoder re-ranker on top rescues the final ordering. Together, these two techniques consistently deliver the largest relevance improvement per engineering hour in any RAG (retrieval-augmented generation) pipeline.

Most teams stop at vector search and wonder why their retrieval quality plateaus. They never learn that BM25 catches what embeddings miss. Even fewer add a re-ranker. A single cross-encoder pass over the top 50 candidates costs milliseconds and routinely bumps nDCG by double-digit percentages. The math is mechanical, not magical.

What problem does hybrid search actually solve?

Dense vector retrieval fails on rare identifiers, exact codes, and proper nouns that appeared too infrequently in its training data. BM25 fails on paraphrases and conceptual queries where no tokens overlap. Neither failure mode is rare in real workloads.

Hybrid search exploits the empirical fact that these two failure sets are mostly disjoint. On the BEIR benchmark, documents missed by BM25 are frequently retrieved by dense models, and vice versa source. The overlap in their top-100 recall sets is often below 60% for natural language queries. By running both and fusing the results, you get recall at depth K that neither could achieve alone. Better recall creates a higher ceiling for everything downstream: re-ranking quality, generation accuracy, and user trust.

The mechanism is not a single clever algorithm. It is a pipeline of two retrievers running in parallel, a fusion step that merges their ranked lists without needing normalized scores, and optionally a re-ranker that applies true cross-attention to the surviving candidates. Each piece is simple. Combined, they produce results that feel disproportionately better than the sum of their parts.

How does BM25 retrieval work under the hood?

BM25 is a bag-of-words ranking function over an inverted index. Each unique token in your corpus maps to a postings list: a sorted sequence of document IDs paired with term frequencies and positions source. When a query arrives, the system tokenizes it, looks up the postings list for each term, and scores every document that contains at least one query term.

The scoring formula rewards rare terms heavily and dampens the impact of repeated terms:

BM25(q, d) = Σ IDF(t) · (f(t,d) · (k1 + 1)) / (f(t,d) + k1 · (1 - b + b · |d| / avgdl))
BM25 Term Scoring
the0.8
report3.2
warranty5.1
SKU-4492110.4
Illustrative impact of term rarity on BM25 score for a single document. Rare terms like 'SKU-44921' dominate, while common words add little.

The IDF term uses inverse document frequency, which means a token like "SKU-44921" that appears in three documents contributes far more than "the" which appears in every document. The denominator includes document length normalization: longer documents get penalized via the b parameter, typically set around 0.75. The k1 parameter, usually between 1.2 and 2.0, controls how quickly additional term occurrences stop boosting the score.

This design makes BM25 dominant for queries containing distinctive tokens. Error codes, product SKUs, legal clause numbers, person names. If your company handbook contains "Section 14.3(c) remote work policy" and an employee searches for exactly that string, BM25 nails it. A dense embedding model might return "flexible work arrangements" instead, which is semantically related but not what the user wanted.

The inverted index also handles structured filters natively. You can ask for documents containing "expense report" AND published after 2024-01-01 AND tagged "finance" in a single query execution source. Vector databases need separate filter intersection logic to achieve the same result.

How does dense retrieval fail on queries BM25 catches?

Dense retrieval maps queries and documents into a fixed-dimensional vector space using a bi-encoder trained with contrastive objectives. At query time, it finds the K vectors closest to the query embedding via an approximate nearest neighbor index like HNSW (Hierarchical Navigable Small World) or IVF source.

The failure mode is not about accuracy in aggregate. It is about distribution. Embedding models compress text into vectors. Rare tokens, numbers, and domain-specific jargon that appeared sparsely during training get mapped to regions of the embedding space that do not reflect their true discriminative power. The model has not seen enough examples to learn that "SKU-44921" should be a nearly exact match signal. So it treats that token as just another piece of text, smoothing its embedding into something generic.

This is why a dense retriever might rank a document about "warranty claims for product 44921" above the actual product listing page for SKU-44921. The semantic similarity is higher. The vectors are closer. But the user wanted the exact SKU match. BM25, with its IDF-weighted term matching, gets this right every time.

The complementarity works in both directions. A dense retriever correctly maps "PTO request process" to a document titled "How to submit vacation time" even though they share zero tokens. BM25 misses that document entirely unless you add query expansion or synonyms. In a company handbook assistant, this gap appears constantly. Employees paraphrase policy questions in their own words. The handbook uses formal language. Dense retrieval bridges that gap. BM25 bridges the gap when they search for a specific form number.

How does reciprocal rank fusion combine two incompatible score distributions?

BM25 produces unbounded positive scores. Cosine similarity sits between -1 and 1. Averaging them directly makes the BM25 signal dominate, because its numeric range is often 100x larger. The dense signal vanishes source.

Reciprocal rank fusion ignores raw scores entirely. It operates only on rank positions. Each document receives a contribution from each retrieval method equal to 1 / (k + rank), where rank is the 1-based position and k is a damping constant, typically 60 source. Documents appearing in both lists accumulate contributions from both. The final ranking sorts by total RRF score.

def rrf_fuse(rankings, k=60):
    scores = {}
    for result_list in rankings:
        for idx, (doc_id, _) in enumerate(result_list):
            scores[doc_id] = scores.get(doc_id, 0.0) + 1.0 / (k + idx + 1)
    return sorted(scores.items(), key=lambda x: x[1], reverse=True)

A document ranked 1st in BM25 and 15th in vector search gets 1/61 + 1/75 ≈ 0.0297. A document ranked 5th in both gets 1/65 + 1/65 ≈ 0.0308. RRF naturally rewards documents that are consistently relevant across both signals. It penalizes documents that one retriever ranks highly but the other completely ignores.

RRF Score Composition
BM25 contribution (rank 1)55%
Vector contribution (rank 15)45%
A document ranked 1st by BM25 and 15th by vector search. Its final fused score comes mostly from the high BM25 rank.

RRF is not just a convenient hack. The original SIGIR 2009 paper and subsequent production benchmarks show it outperforms more complex rank aggregation methods like Condorcet voting in most search settings source. It requires no score normalization, no per-query calibration, and no labeled training data. For a team building a handbook chatbot, RRF is the correct default fusion strategy. Tune k downward to boost top-ranked documents, upward to spread influence more evenly. Start at 60.

What does a cross-encoder re-ranker do that vector search cannot?

A cross-encoder takes the full text of the query and a candidate document, concatenates them with separator tokens, and runs them through a transformer with full bidirectional attention source. Every token in the query attends to every token in the document. The model sees whether "not eligible" in the query negates "eligible" in the document. It sees that "required before" establishes a temporal ordering that embeddings discard.

Bi-encoders, including the ones that power vector search, compress each document into a single vector independently of the query. That compression is lossy. A 768-dimensional vector cannot preserve every semantic subtlety in a 500-word chunk. Cross-encoders pay the full attention cost per query-document pair, which makes them expensive, so they are only applied to a small candidate set, typically the top 20 to 200 documents from the fusion stage.

The cost is the reason for the two-stage architecture. Stage one retrieves broadly and cheaply. Stage two re-scores narrowly and expensively. The overall complexity becomes D + Q + NQ rather than DQ, where D is corpus size, Q is query count, and N is the re-rank depth. With N set to 50, re-ranking adds roughly 50 forward passes per query. A compact cross-encoder like ms-marco-MiniLM-L-6-v2 runs each pass in under 10ms on CPU (central processing unit). That keeps total retrieval latency under 500ms, well within acceptable bounds for a chat interface.

Re-ranking Impact on MS MARCO
0.23
BM25 only MRR@10
0.43
BM25 + Re-ranker MRR@10
1.9x
MRR improvement
Lifting MRR@10 by re-ranking the top 100 BM25 candidates with a cross-encoder.

The quality gain is not subtle. On the MS MARCO passage ranking task, re-ranking the top 100 BM25 results with a cross-encoder lifts MRR@10 from roughly 0.23 to over 0.38 source. When the base retrieval is already a hybrid BM25-plus-dense pipeline, the gain compounds because the candidate set fed to the re-ranker already contains more relevant documents.

How do you choose a re-ranking model and deployment pattern?

Two families dominate production use. The first is distilled cross-encoders based on MiniLM architectures, typically fine-tuned on MS MARCO passage ranking data. Models like cross-encoder/ms-marco-MiniLM-L-6-v2 provide strong out-of-the-box performance with 6 transformer layers and roughly 22M parameters source. They run fast on CPU and are easy to deploy via HuggingFace or ONNX (Open Neural Network Exchange) runtimes.

The second family is late-interaction models, particularly ColBERT. ColBERT encodes the query and document into multiple token-level embeddings and computes relevance as the sum of maximum similarity scores between query tokens and document tokens source. Unlike a full cross-encoder, ColBERT allows document token embeddings to be precomputed and stored, reducing per-query inference cost while preserving token-level alignment. In practice, ColBERT offers a middle ground: better quality than bi-encoders, cheaper than cross-encoders source.

For the company handbook use case, a MiniLM cross-encoder re-ranking the top 50 hybrid results is the pragmatic choice. It requires no additional infrastructure beyond a Python process running the model. If query volume grows to the point where 50 forward passes per query becomes expensive, ColBERT with precomputed token embeddings is the natural next step. Start simple. The cross-encoder alone will feel transformative compared to raw vector search.

Quick Reference

PropertyValue
Default fusion algorithmReciprocal Rank Fusion (RRF)
Standard RRF constant (k)60
Typical re-rank depth (N)20-200 candidates
BM25 parametersk1=1.2-2.0, b=0.75
Common cross-encoderms-marco-MiniLM-L-6-v2 (~22M params)
Cross-encoder latency (CPU)~5-10ms per query-document pair
Hybrid RRF gain over single retriever5-15% nDCG improvement
RRF + re-ranker gain over hybrid alone15-30% nDCG improvement

Frequently Asked Questions

Q: Why not just use a larger embedding model instead of hybrid search?

Larger embedding models improve semantic recall but do not solve the lexical matching problem. An embedding model cannot learn that "SKU-44921" is a precise identifier unless it sees that exact pattern frequently during training. Rare codes, new product names, and domain-specific acronyms will always be underrepresented. Hybrid search gives you lexical matching for free via BM25 without requiring the embedding model to handle every token distribution.

Q: Does RRF work with more than two retrieval methods?

Yes. RRF sums contributions over an arbitrary number of ranked lists. You can add a third retriever (for example, a learned sparse model like SPLADE) and fuse all three. Each list contributes independently. The only constraint is that all retrievers share the same document ID space so contributions can be accumulated correctly.

Q: When should I skip re-ranking entirely?

Skip re-ranking if your latency budget is strictly below 50ms end-to-end and your retrieved chunks are already nearly all from a single correct document. Re-ranking helps most when the top-K candidate set is diverse and contains both relevant and irrelevant documents that need finer discrimination. If your retrieval consistently returns chunks from a single obvious source, the marginal gain is small.

Q: Can I use an LLM (large language model) as a re-ranker instead of a cross-encoder?

Yes, some systems use LLMs for listwise re-ranking by feeding a prompt with the query and a list of candidate documents and asking the model to reorder them. This approach is more expensive and slower than a cross-encoder but can capture complex reasoning. Start with a cross-encoder. Move to LLM re-ranking only if you have specific relevance criteria the cross-encoder consistently misjudges and you can tolerate the latency.

Q: How do I evaluate whether hybrid search and re-ranking are working?

Track recall@K and nDCG@K on a representative query set, with K matching your downstream consumption (for RAG, often 5 or 10). Compare four configurations: BM25-only, dense-only, hybrid (RRF fused), and hybrid plus re-ranker. If the last two configurations do not meaningfully outperform the first two, your queries are not exercising the complementary failure modes, or your re-ranker candidate depth is too shallow.

Test yourself

Your handbook chatbot receives the query "Can I carry over unused PTO to next year?" The top three BM25 results are: (1) a page titled "PTO Policy" with a table of accrual rates, (2) a page titled "Benefits Overview" listing all benefits, (3) a page titled "PTO Carryover Rules." The vector search top three are: (1) "PTO Carryover Rules," (2) "Unused Vacation Time Policy," (3) "Annual Leave and Rollover." After RRF fusion, the "PTO Carryover Rules" page ranks first. When the cross-encoder re-ranks, it demotes "PTO Carryover Rules" to third and promotes "Annual Leave and Rollover" to first. Is the cross-encoder broken?

Answer: Probably not. The cross-encoder sees full token-level interaction between the query and each candidate. "PTO Carryover Rules" might match lexically but contain language about exceptions and disqualifications that the cross-encoder detects as partially negating the query intent. "Annual Leave and Rollover" might use different terminology but describe exactly the mechanism the employee is asking about: unused days transferring to the following year. The cross-encoder is not matching tokens. It is modeling whether the document answers the question. This is the intended behavior. The correct next step is to inspect both documents manually on this specific query to confirm the cross-encoder's judgment, then check whether this pattern generalizes across other PTO-related queries in your evaluation set.

If you want breakdowns like this every week, how real retrieval systems work at the mechanical level, not just which library to import, subscribe to Internals Decoded at internalsdecoded.com. Part 6 covers query rewriting and decomposition: when the user asks a question your chunks cannot answer directly.

Sources

#hybrid-search#reranking#bm25
More from the library
The Newsletter

Keep up with AI. One email a week.

One thoughtful email each week. Unsubscribe whenever you like.