Contents
Map

12 · RAG

Retrieval & Reranking

View as:

Retrieval & Reranking

Retrieval finds candidate passages for a query - lexically (BM25), with learned sparse vectors (SPLADE), with dense embeddings, with token-level late interaction (ColBERT), or a fusion of these - and reranking re-scores the shortlist with a more expensive model that reads query and passage together.

Learning objectives 60 min
By the end of this page you will be able to:
  • Compute BM25 and explain term-frequency saturation and length normalisation
  • Compare sparse, learned-sparse, dense and late-interaction retrieval by what they match and what they cost
  • Fuse ranked lists with Reciprocal Rank Fusion and explain why it avoids score normalisation
  • Design a two-stage retrieve-then-rerank pipeline and decide when a reranker is worth its latency
  • Apply query rewriting, decomposition, HyDE and MMR where they help

The Retriever Families

flowchart TD
    Q["❓ Query"] --> BM["🔤 BM25<br/>exact terms, IDF weighting"]
    Q --> SP["🧮 Learned sparse (SPLADE)<br/>weighted terms incl. expansions"]
    Q --> DN["🧬 Dense bi-encoder<br/>one vector per text"]
    Q --> LI["🧷 Late interaction (ColBERT)<br/>one vector per token, MaxSim"]
    BM --> F["🔀 Fusion (RRF)"]
    SP --> F
    DN --> F
    LI --> F
    F --> RR["🎯 Cross-encoder / LLM reranker<br/>top 20-100 → top 5-10"]
    RR --> OUT(["📚 Context for generation"])

    style BM fill:#e8e0d4,stroke:#c8b89a
    style SP fill:#e8e2d9,stroke:#ccc4b8
    style DN fill:#d8dfe8,stroke:#b0bac8
    style LI fill:#ddd8e4,stroke:#b8b0c8
    style RR fill:#dde4dc,stroke:#b0c4b0
FamilyMatchesIndexStrengthsWeaknesses
BM25Exact termsInverted indexIDs, codes, names, rare terms; no training; cheapSynonyms and paraphrase
Learned sparse (SPLADE)Terms and model-predicted related terms, with learned weightsInverted indexKeyword precision with some semantic expansion; interpretableHeavier encoding; larger index than BM25
Dense (bi-encoder)Meaning, paraphrase, cross-lingualANN vector indexSemantic matching; multilingualRare terms, exact identifiers, negation
Late interaction (ColBERT, ColPali)Token-to-token similarity (MaxSim)Many vectors per documentNear cross-encoder quality at retrieval time; strong out of domainStorage (a vector per token, compressed in ColBERTv2/PLAID)

BM25

BM25(q, d) = Σ over query terms t:  IDF(t) · f(t,d)·(k1 + 1) / ( f(t,d) + k1·(1 − b + b·|d|/avgdl) )

f(t,d)   term frequency of t in d          |d|, avgdl   document length, average length
k1       saturation (typically 1.2-2.0)     b            length normalisation (typically 0.75)
IDF(t) = ln( (N − n(t) + 0.5) / (n(t) + 0.5) + 1 )    N documents, n(t) containing t
  • Saturation: as f(t,d) grows, the term's contribution approaches IDF(t)·(k1 + 1) - the tenth occurrence of "refund" adds far less than the first. The curve is hyperbolic (f / (f + k)), not logarithmic.
  • Length normalisation: with b > 0, the same term count scores lower in a longer document, so long documents don't win just by containing more words.
  • IDF: rare terms carry more weight than common ones.

BM25 is a strong baseline that is hard to beat out of domain: in the code lab, BM25 (nDCG@10 0.662) beat a small dense model (0.648) on scientific claims. Libraries: bm25s (fast, in-memory), Elasticsearch/OpenSearch, Postgres full-text search (ts_rank is not BM25, but extensions such as ParadeDB add it).


Learned Sparse and Late Interaction

SPLADE (Formal et al., 2021) runs a masked-language-model head over the text and keeps a sparse weight for every vocabulary term - including terms that don't appear but are strongly implied ("car" → "vehicle"). It is served from an ordinary inverted index. In the code lab, an off-the-shelf SPLADE model scored the best single-retriever result (0.710 nDCG@10), at the cost of the slowest indexing.

ColBERT (Khattab & Zaharia, 2020) keeps one vector per token. A query-document score is the sum, over query tokens, of the maximum similarity to any document token (MaxSim). This captures fine-grained matches a single vector loses, and generalises well across domains. ColBERTv2 and the PLAID engine compress token vectors to make storage and latency practical; ColPali applies the same idea to page images (see Advanced RAG Patterns). BGE-M3 produces dense, sparse and multi-vector outputs from one model.


Hybrid Retrieval and Reciprocal Rank Fusion

Different retrievers fail on different queries, so combining them raises recall. Their scores are on incompatible scales (BM25 is unbounded, cosine is in [-1, 1]), so fuse ranks, not scores:

RRF(d) = Σ over retrievers r:  1 / (k + rank_r(d))        k = 60 by convention

A document ranked 1st by one retriever and absent from the other scores 1/61 ≈ 0.0164; one ranked 3rd and 5th scores 1/63 + 1/65 ≈ 0.0313 - agreement across retrievers wins. The constant k damps the dominance of top ranks.

from collections import defaultdict

def rrf(ranked_lists: list[list[str]], k: int = 60, depth: int = 100) -> list[str]:
    scores = defaultdict(float)
    for ranking in ranked_lists:
        for rank, doc_id in enumerate(ranking, start=1):
            scores[doc_id] += 1.0 / (k + rank)
    return sorted(scores, key=scores.get, reverse=True)[:depth]

Weighted score fusion (α·dense + (1−α)·bm25 after min-max normalisation) can beat RRF when tuned on labelled data, but it is sensitive to score distributions; RRF is the robust default. Most engines implement hybrid search natively (Elasticsearch/OpenSearch, Weaviate, Qdrant, Azure AI Search, Vertex/Agent Platform Vector Search). LangChain's EnsembleRetriever (now in langchain_classic.retrievers) applies weighted RRF.

In the code lab, hybrid RRF of BM25 and a small dense model lifted nDCG@10 from 0.662/0.648 to 0.690 and Recall@100 to 0.955 - above either input. With a stronger dense model (bge-small-en-v1.5, 0.721), equal-weight RRF scored 0.715 at the top while still raising Recall@100: fusion reliably helps recall, but when one retriever is much better, weight toward it or let a reranker order the fused candidates.


Reranking

A cross-encoder reads the query and a passage together in one forward pass and outputs a relevance score. Full attention between query and passage tokens captures negation, conditions and exact relationships that independent embeddings blur - but nothing can be precomputed, so it only runs on a shortlist.

Reranker typeExamplesNotes
Small cross-encoderscross-encoder/ms-marco-MiniLM-L6-v2Fast on CPU; trained on web search (MS MARCO)
Multilingual / larger cross-encodersBAAI/bge-reranker-v2-m3, mixedbread, Jina rerankersBetter quality, need a GPU for low latency
LLM-based rerankersQwen3-Reranker, listwise prompting of a general LLMStrongest, most expensive; can reason about relevance
Hosted APIsCohere Rerank, Voyage rerank, Google's ranking API, Amazon RerankNo hosting; per-query cost
from sentence_transformers import CrossEncoder
import numpy as np

reranker = CrossEncoder("cross-encoder/ms-marco-MiniLM-L6-v2")

def rerank(query: str, candidates: list[str], top_n: int = 5) -> list[str]:
    scores = reranker.predict([(query, c) for c in candidates], batch_size=32)
    return [candidates[i] for i in np.argsort(-scores)[:top_n]]

Guidelines:

  • Retrieve wide, rerank narrow: 20-100 candidates in, 5-10 out. A reranker can only reorder what first-stage retrieval found, so first-stage recall@k at the candidate depth is the number to watch.
  • Rerankers are domain-sensitive. In the code lab, the MS MARCO-trained MiniLM reranker did not improve hybrid results on scientific claims (0.690 → 0.686). A reranker is a model with a training distribution - evaluate it on your data, and prefer larger multilingual or LLM rerankers, or a fine-tuned one, out of domain.
  • Latency: a small cross-encoder over 50 passages takes tens of milliseconds on a GPU and noticeably longer on CPU; LLM rerankers take hundreds of milliseconds or more. Budget it (see RAG System Design).
  • Score thresholds from a calibrated reranker are a good way to drop irrelevant passages entirely rather than always sending k.

Query Transformation

TechniqueWhat it doesWhen it helpsRisk
Rewriting / conversational condensationTurn "what about for enterprise?" into a standalone query using chat historyMulti-turn chatRewriter drops constraints
Multi-queryGenerate several paraphrases, retrieve for each, fuse with RRFAmbiguous or underspecified queriesN× retrieval cost
DecompositionSplit a multi-part question into sub-questionsComparative and multi-hop questionsSub-questions miss the join; see agentic RAG
HyDEHave the LLM write a hypothetical answer; embed that instead of the queryShort queries vs long, technical documentsA wrong hypothesis retrieves the wrong documents
Step-backRetrieve for a more general version of the question tooQuestions needing background principlesExtra noise
Metadata extractionPull filters (dates, product, region) out of the queryStructured constraints in natural languageWrong filter hides the answer - log and fall back

MMR (maximal marginal relevance) is a post-retrieval diversification: pick results one at a time, maximising λ·sim(query, d) − (1 − λ)·max sim(d, already selected). Use it when top results are near-duplicates.

Every transformation adds latency and a model call; add them in response to measured failure categories, not by default.


Check Yourself

Check yourself
0 / 4 answered
  1. A document is ranked 1st by BM25 and not retrieved by dense search; another is ranked 4th by both. With RRF (k=60), which scores higher?
  2. Why are cross-encoders used for reranking rather than first-stage retrieval?
  3. Adding an off-the-shelf reranker to your pipeline slightly lowers nDCG@10 on your domain. What is the most likely explanation?
  4. Which query does BM25 handle better than a dense retriever, and why?

Exercises

Exercise - Compute BM25 by hand

Corpus of N = 1,000 documents, avgdl = 100. The term "refund" appears in 10 documents. Document A (50 terms) contains it 3 times; document B (400 terms) contains it 6 times. With k1 = 1.2 and b = 0.75, compute the BM25 contribution of "refund" to each document. Which ranks higher?

Solution

IDF = ln((1000 − 10 + 0.5)/(10 + 0.5) + 1) = ln(95.33) ≈ 4.557. A: norm = 1 − 0.75 + 0.75 × 0.5 = 0.625; TF part = 3 × 2.2 / (3 + 1.2 × 0.625) = 6.6 / 3.75 = 1.76; score ≈ 8.02. B: norm = 0.25 + 0.75 × 4 = 3.25; TF part = 6 × 2.2 / (6 + 3.9) = 13.2 / 9.9 = 1.333; score ≈ 6.08. A ranks higher: twice the occurrences in a document eight times longer is weaker evidence.

Exercise - Extend the lab

Add a fusion of BM25 + SPLADE + dense to the code lab, and a stronger reranker (e.g. BAAI/bge-reranker-v2-m3, GPU recommended). Report nDCG@10 and Recall@100 for each system with the number of queries, and decide which pipeline you would ship given indexing and query latency.

Solution

Three-way fusion typically matches or beats the best single retriever on recall; a stronger, more general reranker usually recovers the gain the MS MARCO MiniLM reranker failed to deliver on scientific text. The shipping decision weighs quality gains against SPLADE's indexing cost and the reranker's per-query latency.

Study Notes

Must-know:

  • BM25: IDF × saturating TF with length normalisation (k1, b); a strong out-of-domain baseline
  • Learned sparse (SPLADE) adds expansion terms on an inverted index; late interaction (ColBERT) scores token-to-token with MaxSim
  • Hybrid + RRF: fuse ranks, not scores, k = 60; agreement across retrievers wins
  • Two-stage: retrieve 20-100 for recall, rerank to 5-10 for precision; rerankers are domain-sensitive - evaluate them
  • Query transformations (rewrite, multi-query, decomposition, HyDE, step-back, filter extraction) add cost; use them against measured failures
  • MMR diversifies near-duplicate results

References

Last reviewed: 2026-09

⚡AI-assisted content - always verify, always explore multiple perspectives·