Retrieval quality: hybrid search, reranking, query transforms and metrics
Getting the right chunks to the top, and proving it
Retrieval quality is the ceiling on everything a RAG system does (9.1 Why RAG, and the canonical pipeline), and it is improvable in measurable steps: lexical plus dense search fused by rank, a cross-encoder reranker, and cheap query rewrites before search. This lesson derives BM25 term by term, gives you reciprocal rank fusion and the three retrieval metrics with a worked example, and shows how to build the test set that turns retrieval tuning from guesswork into engineering.
You'll be able to
- Explain the BM25 formula term by term and contrast it with TF-IDF and dense retrieval
- Fuse rankings with reciprocal rank fusion and know when score interpolation is better
- Place cross-encoder, ColBERT and LLM rerankers in a pipeline with their cost profiles
- Choose query transformations (rewriting, multi-query, HyDE, step-back, decomposition, routing) from observed failure modes
- Compute recall@k, MRR and nDCG by hand and build a retrieval test set
Key ideas
- Dense retrieval matches meaning and misses exact identifiers; lexical retrieval is the mirror image — hybrid search beats either alone, especially on jargon-heavy corpora.
- BM25 = Σ IDF(t) · f(k₁+1) / (f + k₁(1 − b + b·|D|/avgdl)): term-frequency saturation (k₁), length normalization (b) and rarity weighting (IDF).
- Reciprocal rank fusion RRF(d) = Σ_r 1/(60 + rank_r(d)) merges rankings without calibrating scores; documents liked by both systems rise.
Show 4 more key ideas
- Rerank the top 50–100 with a cross-encoder (or ColBERT/LLM reranker) to turn recall into precision; it is the most reliable upgrade after hybrid search.
- Query transformations — rewriting, multi-query, HyDE, step-back, decomposition, routing — fix vocabulary mismatch and multi-hop questions at the cost of an extra LLM call.
- Recall@k is the ceiling metric, MRR rewards the first hit's rank, nDCG handles graded relevance with a log₂ position discount.
- A versioned retrieval test set of 50–200 labelled queries, grown from real failures, is what makes every retrieval change measurable.
Suggested path through this lesson
Two kinds of relevance
Dense retrieval (9.2 Embeddings deep dive) matches meaning: "how do I reset my password" finds "credential recovery procedure". But it is weak at exact tokens that carry little semantics: product codes, error strings, names, legal clause numbers, function names. Lexical retrieval is the mirror image: exact matches are perfect, paraphrases are invisible. Because real queries contain both, hybrid search — run both, fuse the rankings — beats either alone on most benchmarks, and the gap is largest on domain data full of jargon.
BM25, term by term
BM25 is the lexical workhorse (the default in Elasticsearch and OpenSearch, Lucene, and the rank_bm25 library). For a query with terms and a document :
- is the term frequency in . The fraction grows with but saturates at : the tenth occurrence of "password" adds much less than the first. sets how fast.
- is the document's length relative to the average; (usually 0.75) controls length normalization so long documents do not win merely by containing more words.
- rewards rare terms: documents in total, containing . A term in every document scores near zero; a term in one document scores near .
Compared with TF-IDF (6.2 Classical NLP and n-gram language models), BM25 adds saturation and length normalization — two small changes that made it the 25-year champion of lexical search.
Math check. , ⇒ . For , , and : the fraction is , so the term contributes . With the fraction is — ten times the occurrences, only 1.4× the score.
Fusing rankings: reciprocal rank fusion
Dense scores (cosines near 0.8) and BM25 scores (unbounded, 7.6 above) live on different scales, so adding them is meaningless without calibration. Reciprocal rank fusion sidesteps scales by using ranks:
Example: dense ranks , BM25 ranks . , , , . Final order: A, C, B, D — documents both rankers like float to the top. The constant dampens the dominance of top ranks; 60 is the conventional value and rarely needs tuning. The alternative — min-max normalize each score list and take — can beat RRF when you have a test set to tune .
def rrf(rankings, k=60):
scores = {}
for ranked_ids in rankings: # each list: doc ids, best first
for rank, doc_id in enumerate(ranked_ids, start=1):
scores[doc_id] = scores.get(doc_id, 0.0) + 1.0 / (k + rank)
return sorted(scores, key=scores.get, reverse=True)
Reranking
First-stage retrieval optimizes recall with cheap scores; a reranker spends more compute on fewer candidates to optimize precision. The classic choice is a cross-encoder (9.2 Embeddings deep dive) that reads query and candidate together and emits a relevance score — 50–100 candidates in, the best 5–10 out, typically 50–300 ms on a GPU-backed service. ColBERT late interaction scores over token vectors and is fast enough to rerank thousands. LLM rerankers show a model the query and a list of passages and ask it to order them (listwise) or to judge each one (pointwise); they are the most accurate and the most expensive. Rule: if your latency budget allows it, rerank — it is the most reliable improvement after hybrid search, and it is what let contextual retrieval reach its 67% error reduction (9.4 Chunking and ingestion).
Query transformations
The user's words are not always the best search key. Cheap LLM calls before retrieval fix that:
- Rewriting: fix typos, expand abbreviations, resolve references to earlier turns ("what about the Pro plan?" → "Pro plan pricing").
- Multi-query: generate 3–5 paraphrases, retrieve for each, fuse with RRF — covers vocabulary mismatch and raises recall.
- HyDE (hypothetical document embeddings): ask the model to write a plausible answer, embed that, and search with it — answers look like passages, questions do not.
- Step-back: first ask a more general question ("what are the rules for refunds?") when the specific one is too narrow.
- Decomposition: split multi-hop questions into sub-questions, retrieve for each, then compose (9.7 Advanced RAG patterns).
- Routing: classify the query to pick an index, a tool (SQL, web search) or to skip retrieval entirely for small talk (10.5 Agentic workflow patterns).
Each adds an LLM round-trip of latency; apply them where your failure analysis shows vocabulary mismatch or multi-hop questions, not everywhere.
Measuring retrieval: recall@k, MRR and nDCG
Let the set of relevant chunks for a query be and the ranked list the system returns be .
- Recall@k — the fraction of relevant chunks that made the cut; the ceiling on answer quality (9.1 Why RAG, and the canonical pipeline).
- Precision@k — how much of the context is useful.
- MRR — rewards putting a right answer on top.
- nDCG@k with — graded relevance, discounted by position; IDCG is the DCG of the ideal ordering, so nDCG .
Worked example: , the system returns , binary relevance. Recall@3 , recall@5 , precision@5 , and this query contributes to MRR (first relevant at rank 2). ; ; .
Report recall@k at the you actually pass to the generator, MRR for "is the best chunk on top", and nDCG when you have graded labels.
Building the retrieval test set
You need 50–200 queries with labelled relevant chunks. Sources, best first: real user questions from logs; questions written by domain experts; synthetic questions generated by an LLM from sampled chunks, then filtered by a second pass asking "would a user really ask this?". Beware synthetic questions that copy the chunk's wording — they make lexical search look perfect. Store the set in version control, add every production failure to it, and keep a held-out slice so you do not tune to your own test. Every change — embedding model, chunk size, hybrid weights, reranker — gets a before/after on this set and a look at the five worst queries.
Where this shows up later
- Chunk design is tuned with these metrics — 9.4 Chunking and ingestion; prompt assembly consumes the reranked top- — 9.6 Generation side: context assembly, citations, structured outputs and tool calling.
- Query transformations grow into agentic retrieval — 9.7 Advanced RAG patterns; retrieval metrics join end-to-end metrics in 9.8 Evaluating and observing LLM apps.
- BM25 is TF-IDF (6.2 Classical NLP and n-gram language models) refined; cross-encoders are BERT-style encoders (7.5 Encoder-only: BERT and friends (and sentence embeddings)).
Common pitfalls (4)
- Adding raw BM25 scores to cosine similarities; they are on different scales — fuse by rank (RRF) or normalize and tune the weight.
- Evaluating retrieval with synthetic questions that copy chunk wording; lexical search looks perfect and real users then suffer.
- Applying every query transformation to every query; each adds latency and some (HyDE) can hallucinate the wrong topic.
- Reranking only the top 5; the reranker can only promote what first-stage retrieval gave it — feed it 50–100 candidates.
Recap: all 7 key points
- Dense retrieval matches meaning and misses exact identifiers; lexical retrieval is the mirror image — hybrid search beats either alone, especially on jargon-heavy corpora.
- BM25 = Σ IDF(t) · f(k₁+1) / (f + k₁(1 − b + b·|D|/avgdl)): term-frequency saturation (k₁), length normalization (b) and rarity weighting (IDF).
- Reciprocal rank fusion RRF(d) = Σ_r 1/(60 + rank_r(d)) merges rankings without calibrating scores; documents liked by both systems rise.
- Rerank the top 50–100 with a cross-encoder (or ColBERT/LLM reranker) to turn recall into precision; it is the most reliable upgrade after hybrid search.
- Query transformations — rewriting, multi-query, HyDE, step-back, decomposition, routing — fix vocabulary mismatch and multi-hop questions at the cost of an extra LLM call.
- Recall@k is the ceiling metric, MRR rewards the first hit's rank, nDCG handles graded relevance with a log₂ position discount.
- A versioned retrieval test set of 50–200 labelled queries, grown from real failures, is what makes every retrieval change measurable.
Watch
The query translation parts (multi-query, RAG-Fusion, decomposition, step-back, HyDE) and the ColBERT part implement this lesson's techniques.
Read
Try it live
Interactive demo
runs in your browserExercises
- BM25 from scratch
Implement BM25 in about 25 lines of Python (tokenize, document frequencies, average length, the formula with , ). Compare your rankings with
rank_bm25on 20 queries; they should match. Then set and observe how long documents start to win. - Hybrid search with RRF
Combine your BM25 with dense retrieval from 9.2 Embeddings deep dive using the
rrffunction. On your retrieval test set report recall@5 for dense only, BM25 only and hybrid. Expected: hybrid matches or beats both, with the biggest gains on queries containing names, codes or rare terms. - Metrics by hand, then in code
Reproduce the worked example (recall@3 = 0.5, MRR contribution 0.5, nDCG@5 ≈ 0.651) with a function
evaluate(ranked_ids, relevant_ids, k). Then compute all three metrics over your whole test set and list the five worst queries. - Add a reranker
Rerank the hybrid top-50 with a local cross-encoder (for example an MS MARCO MiniLM cross-encoder from
sentence-transformers). Measure recall@5 and MRR before and after, and the added latency per query. Decide whether the gain is worth the latency for your use case.
Quiz
Flashcards
These cards are now in your spaced-repetition queue. Review all due cards →