Modules/Gen AI/ Reranking Lab

Re-ranking: Bi-Encoders vs Cross-Encoders

Retrieve fast and approximately with a bi-encoder, then spend more compute reading only the survivors closely with a cross-encoder. Two stages, two very different costs.

Pipeline

query: "python memory management"

the cross-encoder's judgments below are shown as a labelled example of what jointly reading query+document produces — a real one runs a trained model, which cannot run in this page

Cost

Bi-encoderprecomputed
Cross-encoder0 forward passes

Stage 1 — Bi-Encoder Retrieval (cosine)

—

Stage 2 — Final Order

Reading It

 

Overview

Retrieve fast and approximately with a bi-encoder, then spend more compute reading only the survivors closely with a cross-encoder.

In this guide
  1. The idea in brief
  2. The two-stage pattern
  3. Two ways to score a query against a document
  4. Why the two-stage pipeline exists
  5. In code
  6. Why one vector per document is not enough
  7. Try it yourself
  8. Worth recalling
  9. Choosing a reranker
  10. How many candidates to rerank
  11. Questions people ask
  12. Recap in one screen

Re-ranking: A Practical Guide

Spend the expensive model only on the candidates that survived the cheap one.

The idea in brief

A bi-encoder embeds the query and each document apart, ahead of time — that is what makes the embeddings precomputable and search fast. But it too means the net never looks at a query and a document together. A cross-encoder takes both as one input and lets the net attend across them jointly, at the cost of running a full forward pass per query-document pair, at query time, with nothing precomputable. The precision gap between these two approaches is real and well-measured. On MS MARCO and close benchmarks, a cross-encoder always and much beats a bi-encoder by 5-15 points of nDCG,. This is since joint attention can net fine-grained query-document links (negation, qualification, partial match) that free embeddings compress away. The usual pattern is a two-stage pipeline: the bi-encoder (or BM25) fetches a shortlist of 20-100 candidates cheaply. And the cross-encoder rescores just that shortlist to find the final ranking. This page shows both nets scoring the same query-document pairs side by side, shows where the bi-encoder's ranking disagrees with the cross-encoder's, and covers the latency-precision tradeoff as the shortlist size changes.

The two-stage pattern

Cross-encoders are far more right but cost too much to run over an whole corpus — a million documents means a million forward passes per query. The usual fix is two stages: a cheap bi-encoder (or BM25, or both) fetches a shortlist of maybe 20-100 candidates. And just that shortlist is re-scored by the costly cross-encoder. This then decides the final rank. You pay the cross-encoder's cost dozens of times per query instead of millions of times.

Two ways to score a query against a document

A bi-encoder embeds the query and the document apart. Then checks the two vectors. Since documents can be embedded ahead of time, search is a fast vector check over a precomputed index.

A cross-encoder takes the query and one document together as a single input and outputs a fit score. The net attends across both. So each query term can interact with each document term. And nothing can be precomputed.

bi-encoder:   score = cos(embed(q), embed(d))

cross-encoder: score = net(q, d)

That structural gap gives the whole trade-off:

 Bi-encoderCross-encoder
Documents embedded in advanceYesNo
Query costOne embedding, then vector searchOne model pass per document
Scales to millionsYesNo
AccuracyGoodSubstantially better
Interaction between q and dNone until the comparisonFull attention across both

Why the two-stage pipeline exists

A cross-encoder over a million documents means a million net passes per query. Not doable.

A bi-encoder over a million documents is one embedding plus an indexed vector search. Milliseconds.

So the usual setup uses each for what it is good at:

  1. Retrieve 50–100 candidates with the bi-encoder (and BM25, fused). Fast, broad, some noise.
  2. Rerank those candidates with the cross-encoder. Costly per thing, affordable on 50.
  3. Pass the top 3–10 to the net.

The insight is that recall and precision can be separated. Stage one needs high recall — the right document must be somewhere in the 50. Stage two gives precision, sorting them well.

Measured on common benchmarks, adding a cross-encoder reranker much improves nDCG by 10–20% over dense retrieval alone. It is one of the highest-value additions to a RAG pipeline. And it is much skipped.

In code

from sentence_transformers import SentenceTransformer, CrossEncoder

# stage 1: bi-encoder, over the whole index
bi = SentenceTransformer("all-MiniLM-L6-v2")
q_vec = bi.encode([query], normalize_embeddings=True)[0]
candidates = top_k_by_dot_product(q_vec, index, k=50)

# stage 2: cross-encoder, over the 50
ce = CrossEncoder("cross-encoder/ms-marco-MiniLM-L-6-v2")
pairs = [(query, c.text) for c in candidates]
scores = ce.predict(pairs)

ranked = [c for _, c in sorted(zip(scores, candidates), reverse=True)][:5]

Note the shapes: the bi-encoder embeds once and checks against a precomputed matrix. The cross-encoder receives 50 (query, document) pairs and returns 50 scores. The second call is where the latency is, and it is batched.

Rough latency for 50 candidates with a small cross-encoder on a GPU: 20–50ms. On a CPU, a few hundred milliseconds — which is why reranker size is a real deployment decision.

Why one vector per document is not enough

The code up shows the two-stage API. What it cannot show is the reason the second stage helps at all -- that a bi-encoder commits to one vector per document before it knows the question. This builds both scorers from scratch on a case where that commitment is just the problem.

example_01.pyNumPy
Output

A cross-encoder never builds that per-document vector at all. So it never has this normalisation problem in the first place.

example_02.pyNumPy
Output

Put both scorers over both queries and the disagreement shows up right at the top of the ranking, not just more down.

example_03.pyNumPy
Output

That precision is just why cross-encoders are not used alone. The next block shows what running one over a whole corpus would cost, and why the two-stage split exists.

example_04.pyNumPy
Output

Try it yourself

  1. Read Stage 1. Pure cosine likeness over word overlap. One candidate about python the snake ranks deceptively high — it shares the words "python" and "memory" with the query. And a bi-encoder scoring query and document on its own has no way to notice they mean a thing else in context.
  2. Turn on the reranker. The final rank changes: the snake document drops sharply. And a truly on-topic document that Stage 1 under-ranked moves up. This is what "reading query and document together" catches that checking two free vectors cannot.
  3. Turn it back off. The rank reverts to pure Stage 1 — the reranker did not change what was fetched, just the rank the survivors are gave in.
  4. Read the cost panel. The bi-encoder cost is now paid, before the query ever arrived. The cross-encoder cost is just the count of candidates it re-scores — six forward passes, not a million.

Worth recalling

A bi-encoder scores query and document on its own, which is what makes it fast enough to search a whole corpus but blind to links between them — a lexical coincidence can outrank a genuine match. A cross-encoder reads both together and catches that, at a cost that just scales with the shortlist, not the corpus. Two-stage retrieval — cheap and broad, then costly and narrow — is how live search gets both speed and precision instead of choosing one.

Choosing a reranker

OptionCharacter
MiniLM cross-encoderSmall, fast, strong baseline
Larger cross-encodersBetter, several times slower
Multilingual variantsWhen the corpus is not English
ColBERT (late interaction)Between the two — token-level vectors, precomputable
LLM-as-rerankerPrompt a model to score relevance; flexible, expensive

ColBERT deserves a mention as the genuine middle ground. It stores a vector per token rather than per document, and scores by matching query tokens against document tokens at query time. That gives much of a cross-encoder's fine-grained link while keeping the document side precomputable. At the cost of a far more index.

LLM reranking — asking a language net to rate each candidate's fit, or to rank a list — is flexible and adapts to cues. And it is slow and costly enough that it is much reserved for small candidate sets or offline evaluation.

How many candidates to rerank

The parameter to tune, and it is a straight latency-versus-grade trade.

CandidatesEffect
10Fast, and the reranker can only reorder what stage one already ranked highly
50The usual choice — enough room to rescue a mid-ranked document
100–200Better recall, noticeably slower

The count to measure is stage one's recall@n: if recall@50 is 0.95 and recall@100 is 0.96, reranking 100 buys almost nothing. If recall@50 is 0.7, the bottleneck is retrieval, not ranking, and fixing chunking or adding hybrid search matters more than a bigger reranker.

That diagnostic sorting is worth stating plainly: reranking cannot recover a document that was never fetched.

Questions people ask

Do I need a reranker? If retrieval grade limits your system and latency lets 20–50ms, it is one of the best available improvements.

Can a cross-encoder replace the bi-encoder? Just on a quite small corpus. It cannot be indexed.

How many candidates should I rerank? 50 is the usual starting point. Tune from measured recall.

Is an LLM a good reranker? Effective and costly. Use a purpose-trained cross-encoder for live paths.

Does reranking help with the "lost in the middle" problem? Yes. It puts the most apt chunk first, where the net attends to it most.

Can I fine-tune a reranker? Yes, on (query, apt, irrelevant) triples from your own data. It much beats a general net by a wide margin on a set domain.

Recap in one screen

  • A bi-encoder embeds query and document apart, so documents can be indexed and search is fast.
  • A cross-encoder reads both together, which is far more right and cannot be precomputed.
  • Use both: broad cheap retrieval for recall, then reranking for precision.
  • Rerank around 50 candidates. Measure stage-one recall to know whether that is the bottleneck.
  • Reranking cannot rescue a document that retrieval never gave.

Recall check

0 of 3

Say the answer out loud before you reveal it — recalling it is what makes it stick, and rereading it is not.

  1. What does this module say about “The idea in brief”?

  2. What does this module say about “The two-stage pattern”?

  3. What does this module say about “Two ways to score a query against a document”?

Cheat sheet

Re-ranking: Bi-Encoders vs Cross-Encoders

Retrieve six candidates fast with a bi-encoder, then re-score them with a slower cross-encoder that reads query and document together and catches what was missed.

GEN AI · vizlearn.in/gen_ai/reranking_bi_encoders_vs_cross_encoders.html

About the author

Ashish Jangra builds and maintains VizLearn. Every module here is written and the visualisation behind it hand-built, so the numbers in a readout come from the same code that draws the picture. Corrections are genuinely welcome and get priority over everything else — if a page states something wrong, or an animation misrepresents what the algorithm does, get in touch.