search_ads_recsys / 45 · semantic reranking search · 6 / 10

Multi-stage & semantic reranking

The cascade exists because cost-per-document rises 100–1000× at each stage. You can only afford the most accurate model on a handful of documents — so reranking is where you spend a latency budget to buy accuracy, and the whole craft is spending it wisely.

The plan

This lesson depends on two predecessors. From 43 (dense & hybrid retrieval) you have the bi-encoder: query and document encoded into separate vectors, scored by dot product, precomputed and indexed in an ANN structure — that's how retrieval pulls a thousand candidates out of millions cheaply. From 44 (learning to rank) you have the mid-stage GBDT/LTR ranker that scores those thousand on hundreds of features. This lesson adds the final, most expensive stage — the neural reranker that runs on the top few dozen — and explains the model architectures (cross-encoder, late interaction) that make accuracy buyable. It sets up 48 (relevance evaluation) — how you prove the rerank actually helped — and 49 (system design), where the whole cascade gets a latency budget.

The cascade exists because cost rises faster than accuracy

A search pipeline is a cascade (also called a multi-stage retrieval-and-ranking funnel; lesson 1 introduced the recsys version). Each stage takes the survivors of the previous one, applies a more expensive and more accurate model, and passes a smaller set forward. The reason this shape is forced — not a convenience but a necessity — is a single inequality:

cost-per-document × number-of-documents ≤ stage latency budget

The left side is what you spend; the right is what you have. Retrieval scores millions of documents, so its per-doc cost must be near-zero (a dot product, or a posting-list increment). The reranker is a transformer that does full attention over a query and a document — milliseconds per pair — so it can only touch a handful. If cost-per-doc rises 100–1000× from one stage to the next, the candidate count must fall by a similar factor to keep the product under budget. That is the whole logic of the funnel.

query "best wireless earbuds for running" │ ▼ ┌────────────────────────────────────────────────────────────┐ │ STAGE 1 · RETRIEVE N ≈ 10,000,000 docs │ │ BM25 (inverted index) + dense bi-encoder (ANN) │ │ cost ≈ 0.0005 ms/doc · precomputed doc vectors │ ~10 ms │ keeps: top ~1,000 │ └─────────────────┬──────────────────────────────────────────┘ │ ÷ 10,000 (10M → 1K) ▼ ┌────────────────────────────────────────────────────────────┐ │ STAGE 2 · MID-RANK N ≈ 1,000 docs │ │ GBDT / LTR over hundreds of features (lesson 44) │ │ cost ≈ 0.05 ms/doc │ ~20 ms │ keeps: top ~50–100 │ └─────────────────┬──────────────────────────────────────────┘ │ ÷ 20 (1K → 50) ▼ ┌────────────────────────────────────────────────────────────┐ │ STAGE 3 · RERANK N ≈ 50 docs │ │ cross-encoder transformer (query × doc joint attention) │ │ cost ≈ 8 ms/doc (batched/parallel → effective ~1–2 ms) │ ~50–100 ms │ keeps: top ~10 (the SERP) │ └─────────────────┬──────────────────────────────────────────┘ │ blend ▼ ┌────────────────────────────────────────────────────────────┐ │ STAGE 4 · BLEND / RERANK-FOR-BUSINESS │ │ diversity + dedup (MMR), freshness boost, business rules │ ~3 ms │ produces final top-10 shown to the user │ └────────────────────────────────────────────────────────────┘ cost/doc: 0.0005 ms -> 0.05 ms -> 8 ms (~100x and ~160x jumps) doc count: 10,000,000 -> 1,000 -> 50 -> 10 (~10^4x and ~20x cuts)

Worked numbers — the budget at each stage. Suppose retrieval has 10 ms. At 0.0005 ms/doc that is 10 / 0.0005 = 20,000 dot products of headroom — but ANN (lesson 3) lets it touch millions without scoring them all, so the real constraint there is the index, not the per-doc cost. The mid-ranker gets 20 ms at 0.05 ms/doc → it can score 20 / 0.05 = 400 docs comfortably, ~1,000 if you push it. The reranker gets, say, 50 ms at 8 ms/doc → 50 / 8 ≈ 6 docs sequentially. That is far too few — which is exactly why reranking lives and dies on batching and parallelism, the subject of the widget below. Without them, the expensive stage can rerank single digits; with them, dozens.

Why each stage must cut by ~10–100×, not less
If a stage only halved the candidate set, the next (100× costlier) stage would blow its budget by ~50×. The cut factor has to roughly cancel the cost-per-doc jump. A 100× cost jump wants a ~10–100× count cut so latency stays roughly flat across stages. This is also why you don't see a four-stage cascade where every stage costs 2× the last — there'd be no point; the funnel earns its complexity only when each stage is dramatically more expensive and dramatically more selective than the one before. A senior tell: when asked "why not just run the cross-encoder on all 1,000?", you answer with the arithmetic — 1,000 × 8 ms = 8 s — not a hand-wave.

Bi-encoder vs cross-encoder — the central distinction

Everything about why retrieval and reranking use different neural models comes down to when the query meets the document. There are two architectures, and the difference is whether they interact early (inside the transformer) or late (only at the dot product).

Bi-encoder encode separately → dot product query tokens doc tokens query enc doc enc q ∈ ℝ^d d ∈ ℝ^d ⟨·,·⟩ score doc vector PRECOMPUTED indexed in ANN → retrieval Cross-encoder encode jointly → full cross-attention [CLS] query [SEP] doc [SEP] transformer every query token attends to every doc token relevance score CANNOT precompute score depends on the pair; run once per candidate at query time → reranking

Bi-encoder. The query and each document are encoded independently into fixed vectors, and the relevance score is their dot product (or cosine). Because the document vector doesn't depend on the query, you can compute it once at index time and store it. At query time you encode only the query and do a vector search — that's dense retrieval (43). The architecture's defining property is precisely that the document side is precomputable and indexable. The cost: query and document never see each other inside the model, so the score can only capture coarse, query-independent notions of what the document is "about." A passage that answers the query through subtle term overlap or negation often scores no differently than a topically-similar passage that doesn't actually answer it.

Cross-encoder. The query and a document are concatenated into a single input — [CLS] query [SEP] document [SEP] — and fed jointly into one transformer. Now every query token can attend to every document token through full self-attention. The model can directly represent "does this specific phrase in the document answer this specific part of the query?" This is dramatically more accurate — typically several NDCG points over a bi-encoder on the same data. The cost is the mirror image of the bi-encoder's benefit: the score depends on the (query, document) pair, so nothing can be precomputed. You must run the full transformer forward pass for every candidate, at query time. That is why a cross-encoder is a reranking model and never a retrieval model — you cannot index a function that hasn't yet seen the query.

Bi-encoderCross-encoder
When q meets dLate — only at the dot product, outside the networkEarly — inside the transformer, full cross-attention
Doc encodingQuery-independent → precompute & indexQuery-dependent → cannot precompute
Query-time cost~0 marginal per doc (vectors already exist; one ANN search)One full transformer pass per candidate
AccuracyGood for recall; misses fine-grained relevanceState of the art per-pair relevance
RoleRetrieval (millions of docs)Reranking (tens of docs)
The one-line reason you can't "just use the cross-encoder everywhere"
A bi-encoder amortizes the document-encoder cost across all future queries — encode 10M docs once, reuse forever. A cross-encoder amortizes nothing: its cost is (queries) × (candidates per query) × (forward-pass cost). At 8 ms/pair, scoring 10M docs for a single query is 10,000,000 × 8 ms = 80,000 s ≈ 22 hours. The bi-encoder isn't "the cheap approximation we tolerate" — it's the only thing that makes retrieval tractable. The cross-encoder isn't "the model we'd use everywhere if we could afford it" — it's structurally a reranker. Conflating the two is the most common conceptual error in this space.

The reranking latency math, done explicitly

Reranking N candidates with a cross-encoder costing c ms per pair has a naive cost of N × c if you run them one at a time. The whole engineering problem is that this number is enormous unless you control both N and the effective per-doc cost. Three regimes:

ScenarioArithmeticVerdict
Rerank all retrieved, sequentialN = 1,000, c = 8 ms → 1,000 × 8 = 8,000 ms = 8 sInfeasible. No user waits 8 s.
Rerank top-50, sequentialN = 50, c = 8 ms → 50 × 8 = 400 msStill too slow for most surfaces (web search wants <100 ms reranking).
Rerank top-50, batched + parallel50 docs in 1 batch on a GPU ≈ 1 forward pass ≈ 15–30 ms; or split across 4 replicas → less eachFeasible. This is the real deployment.
Bi-encoder rerank (precomputed)~0 marginal/doc — vectors already in the index; just dot productsEssentially free, but lower accuracy. The retrieval score itself.

The crucial subtlety the table makes concrete: a cross-encoder forward pass is not linear in N on real hardware. A GPU processing a batch of 50 (query, doc) pairs does it in roughly one forward pass of wall-clock time — the 8 ms/pair figure is the amortized cost, and batching collapses it. So the real latency knob is "how big a batch fits, and across how many replicas," not "8 ms times N." The widget below lets you turn those knobs. But batching has a ceiling — sequence length, GPU memory, and tail latency from the slowest doc in the batch — so you cannot make N arbitrarily large for free. The two levers remain: keep N small (let the mid-ranker do the culling), and batch hard.

NDCG gain from reranking saturates in N — so spending budget past the knee is waste
Reranking can only reorder documents it receives, and it can only help NDCG by promoting a relevant doc that the mid-ranker placed below the cut. Most such mis-rankings live in the top ~50–100: that's where relevant and near-relevant docs are densely interleaved and the cross-encoder's fine-grained judgment pays off. Below rank ~100, the candidates the mid-ranker placed there are mostly genuinely irrelevant — promoting them rarely changes the top-10 the user sees. So the NDCG-gain-vs-N curve rises steeply then flattens.

Worked intuition. Say reranking top-25 buys +0.040 NDCG@10, top-50 buys +0.052, top-100 buys +0.056, top-200 buys +0.057. Going 50→100 doubles your cost for +0.004; 100→200 doubles it again for +0.001. The marginal NDCG per millisecond is collapsing. The senior move is to find the knee empirically (sweep N on a judged set, plot NDCG@10 vs N) and set N just past it — typically 50–100 — rather than reranking everything retrieval handed you.

Late interaction (ColBERT) — the middle ground

The bi-encoder collapses a whole document into one vector before the query is known; the cross-encoder refuses to collapse anything and pays for it at query time. Late interaction (ColBERT, Khattab & Zaharia 2020) sits between them with a clever trick: precompute a vector per token of the document, not one per document. The query is also encoded per token at query time. The relevance score is MaxSim: for each query token, find its best-matching document token (max dot product over the document's token vectors), then sum those maxima over query tokens.

score(q, d) = Σi ∈ q-tokens maxj ∈ d-tokens ⟨ qi, dj

This recovers much of the cross-encoder's token-level matching ("does some doc token strongly match this query token?") while keeping the document side precomputable — the per-token doc vectors don't depend on the query, so they can be computed at index time and stored. The query-time work is just the MaxSim aggregation: cheap dot products and maxes, no transformer forward pass per candidate. ColBERT can even drive retrieval directly (via a token-level ANN index), but its most common use is exactly the role of this lesson: a cheap, accurate reranker over the mid-stage candidates.

Worked numbers — the storage cost. A bi-encoder stores one vector per doc: at d = 768 fp16, that's 768 × 2 = 1,536 bytes ≈ 1.5 KB/doc. ColBERT stores one vector per token; a 200-token document at the same dim is 200 × 1,536 = 307,200 bytes ≈ 300 KB/doc — ~200× the storage. In practice you compress aggressively: project to d = 128 and quantize to ~2 bits/dim (ColBERTv2's residual compression), bringing it down to roughly 200 × 128 × 0.25 ≈ 6.4 KB/doc — still several× a bi-encoder, but workable. The trade is explicit: ColBERT buys cross-encoder-adjacent accuracy and bi-encoder-adjacent query latency by paying in index storage.

Bi-encoderLate interaction (ColBERT)Cross-encoder
Accuracybaseline+ (near cross-encoder on many tasks)highest
Query-time cost / doc~0 (1 dot product)low (MaxSim: token-matrix dot products, no transformer)high (full transformer pass)
Doc encoding precomputed?yes (1 vector)yes (one vector per token)no
Storage / doc~1 vector (~1.5 KB)~tokens × vectors (~6–300 KB)0 stored (recomputed each query)
Typical roleretrievalretrieval or cheap rerankfinal rerank on tens of docs

The frame to carry away: each architecture trades along precompute-vs-accuracy by choosing the granularity at which it commits to a representation before seeing the query. Bi-encoder commits one vector per doc (most precompute, least accuracy). Cross-encoder commits nothing (no precompute, most accuracy). ColBERT commits per-token vectors (precompute the representation, defer the interaction) — buying accuracy with storage rather than query-time compute.

Pushing accuracy upstream: distillation

If the cross-encoder is the most accurate model but too expensive to run on many docs, the natural question is: can we transfer its judgment into a cheaper model that can run earlier and on more docs? Yes — that's distillation (the teacher→student idea from lesson 26). Run the expensive cross-encoder offline as a teacher over a large set of (query, doc) pairs, producing soft relevance scores. Train a bi-encoder or ColBERT student to match those scores (typically a margin or KL/MSE loss on the teacher's score distribution, not just the binary labels). The student inherits much of the teacher's relevance ordering while keeping the cheap, indexable architecture.

Why this works and why it matters: the binary click/judgment labels are sparse and noisy, but the teacher's soft scores carry a rich, dense signal — relative ordering among negatives, degrees of relevance — that a bi-encoder could never learn from labels alone. Distillation is how modern dense retrievers (and ColBERTv2) got good: the accuracy of cross-attention is baked into the retrieval/rerank model at train time, so you pay the cross-encoder cost once, offline, instead of per-query forever. The senior framing: distillation moves accuracy upstream in the cascade, where it's cheap to apply, by paying for it once at training time instead of every query at serving time.

LLM rerankers — the newest, most expensive tier

Large language models can rerank, and they're increasingly used as either the top tier of the cascade or as offline judges/teachers. Three prompting styles, each with a different latency/cost profile:

The LLM-reranker latency/cost reality
An LLM reranking call is hundreds of milliseconds to seconds and costs real money per query — orders of magnitude more than a cross-encoder, which is itself orders of magnitude more than a bi-encoder. A listwise pass over 20 passages with a frontier model can be 1–3 s and is dominated by output-token generation (the model must emit the full ranked order). Pointwise over 50 docs is 50 calls, parallelizable but expensive. Consequence: LLM rerankers are mostly used (a) offline as teachers/judges to distill into cheap online models, (b) on very small candidate sets (top 5–10) where the per-query cost is bounded, or (c) on low-QPS, high-value surfaces (enterprise RAG, legal/medical search) where latency and cost tolerances are looser than web search. Putting a frontier LLM in the synchronous path of a billion-QPS web search is not how production systems work — interviewers expect you to know that and reach for distillation instead. When you need an LLM, see lesson 26 for the build-vs-buy and pricing reasoning; never quote model prices from memory.

Interactive · rerank budget explorer

This widget makes the rerank arithmetic concrete. Pick how many docs you rerank (N), the per-doc cross-encoder cost, and how much parallelism/batching you have. It reports the total added latency, whether it fits a chosen latency budget, and an approximate (saturating) NDCG-gain curve, then renders a verdict: worth it, over budget, or past the saturation point. The cost and NDCG models are toy approximations — they get the shape and order of magnitude right so the trade-off feels real, not the precise numbers for your stack.

Spend your rerank budget: how big an N can you afford, and is it worth it?
Toy model. Latency = ceil(N / parallelism) × (per-doc cost + small batch overhead). NDCG gain saturates in N: gain(N) = G_max × N / (N + N₅₀), where N₅₀ is the N at which you have captured half the achievable gain. Real numbers depend on your model, hardware, and judged set — use this to reason about the shape.
added latency
fits budget?
approx NDCG@10 gain
marginal gain / +50 docs
Verdict

The blend layer — relevance is not the last word

After the reranker produces a relevance-ordered list, one more stage runs: the blend (or "rerank-for-business") stage. Its job is everything relevance scoring deliberately ignores, and — critically — this is the last place in the pipeline where you can enforce these constraints, because nothing downstream sees the full candidate set anymore. Four concerns:

Why the blend layer is last, and why that's load-bearing
Diversity, caps, and dedup are listwise properties — they depend on the whole set of results, not any single document. The reranker scores documents independently (even a cross-encoder scores one (query, doc) pair at a time), so it cannot enforce "at most 2 per seller" or "these two are duplicates." Only a stage that sees the full ranked set can. And it must run after relevance ranking, because diversity/business adjustments are perturbations on top of a relevance ordering — you diversify the good results, you don't diversify first and rank second (that would surface diverse-but-irrelevant junk). The interview tell: when asked "where do you enforce diversity / dedup / sponsored placement?", the senior answer is "the blend stage, after relevance reranking, because these are listwise constraints and this is the last stage that sees the whole list." A junior tries to bake them into the ranker's score and gets a tangled, unauditable objective.

Interview prompts you should be ready for

  1. "Why does search use a bi-encoder for retrieval but a cross-encoder for reranking — why not one model?" (The core distinction. Bi-encoder's doc side is query-independent → precomputable & indexable, so it scales to millions; cross-encoder's joint attention is query-dependent → can't precompute, so it only runs on the few survivors. Says the magic word: amortization. The bi-encoder amortizes doc encoding across all queries; the cross-encoder amortizes nothing.)
  2. "Your reranker is a cross-encoder at 8 ms/doc. The PM wants to rerank all 1,000 retrieved docs. What do you say?" (Do the arithmetic out loud: 1,000 × 8 = 8 s, infeasible. Then: keep N small (50–100, past the NDCG knee), batch hard so per-doc cost collapses, and let the mid-ranker cull. A junior says "we'll optimize the model"; a senior says "we'll bound N and batch.")
  3. "You bumped N from 50 to 500 and NDCG@10 barely moved but latency 10×'d. Why?" (NDCG gain saturates in N — almost all the reorderable relevance is in the top ~50–100; below that the candidates are genuinely irrelevant and promoting them doesn't change the top-10. You spent budget past the knee. Find the knee empirically and stop there.)
  4. "What's ColBERT and when would you reach for it over a cross-encoder?" (Late interaction: per-token doc vectors precomputed, query-time MaxSim. Near-cross-encoder accuracy at far lower query cost, but multiplies storage by tokens-per-doc. Reach for it when you want better-than-bi-encoder accuracy at scale and can pay the storage / can't pay cross-encoder query latency.)
  5. "You want cross-encoder accuracy but cross-encoder latency is too high. Options?" (Distillation: cross-encoder teacher → bi-encoder/ColBERT student, baking the accuracy in at train time so you pay the cost once offline. Plus: shrink N, batch, smaller/quantized cross-encoder, or only rerank the top-K with the expensive model. The senior insight is moving accuracy upstream via distillation.)
  6. "Where do you enforce result diversity, dedup, freshness, and sponsored placement — in the ranker or somewhere else?" (The blend stage, after relevance reranking. These are listwise constraints (depend on the whole set) and policy, not per-doc relevance; the reranker scores docs independently and structurally can't see them. It's the last stage that sees the full list. Don't tangle them into the ranker's objective.)
Takeaway
The cascade is an economic structure: cost-per-doc rises 100–1000× per stage, so the candidate count must fall 10–100× per stage to keep latency flat, and the expensive model only ever touches a handful of docs. The bi-encoder vs cross-encoder split is the load-bearing idea — separate encoding is precomputable and indexable (retrieval); joint encoding is accurate but query-time-only (reranking). ColBERT's late interaction buys a middle ground with storage; distillation pushes cross-encoder accuracy upstream by paying once offline; LLM rerankers are accurate but too slow/costly for the synchronous web-search path, so they mostly serve as offline teachers or on tiny candidate sets. Reranking buys accuracy with latency — so keep N just past the NDCG knee, batch hard, and leave the listwise concerns (diversity, dedup, freshness, business rules) to the final blend stage, the last place that sees the whole list.