Skip to content

Rerankers and Cross-Encoders

A two-stage example: retrieve 100 candidates, score each candidate jointly with the query, then retain ten. Actual costs depend on model, input lengths, hardware and batching.

A dense retriever encodes the query into a vector, searches an ANN index over pre-encoded document vectors, and returns the top-kk by cosine similarity. That is the bi-encoder recipe: precompute document representations, then search them for each new query.

A relevant passage may be in the candidate set without appearing near the top. Reranking gives a second model a smaller, more expensive comparison problem. First measure candidate recall: no reranker can score a passage it never receives.


What this post assumes

This builds on embeddings and vector search, dense vs sparse hybrid retrieval, and chunking strategies. The first-stage retriever returns a candidate set; here we score those candidates again with the query available during the comparison.


What a bi-encoder compresses

A bi-encoder encodes the query and the document separately. Each ends up as a single fixed-length vector, and you compare them with a cosine or a dot product. The document vector is computed before the query arrives and reused across queries.

The encoder cannot adapt its document representation to the current query. Relevance must be recoverable from the two independently computed vectors.

Query-time work is encoding one query plus index search, rather than running the encoder jointly over every query–document pair.


What a cross-encoder does differently

A cross-encoder feeds the query and the document together into a single encoder, usually as [CLS] query [SEP] doc [SEP], and reads off a single relevance score from the top.

In an encoder-style cross-encoder, self-attention over the concatenated sequence permits query and document tokens to interact. This is joint self-attention, not a separate encoder–decoder cross-attention block.

bi-encoderencode separately, compare with cosinecross-encoderencode jointly, full token-to-token attention[CLS] kv cache workkv cache reuseencoderencodercosine(q, d)single scalardocs pre-encoded once, reused across queries[CLS]kvcachework[SEP]kvcachereusejoint encoder: self-attentionscore(q, d)single logitre-encodes every (q, d) pair; query-dependent scoring

Trace where query and document tokens can interact in each architecture.

Nogueira and Cho demonstrated BERT passage reranking on MS MARCO in 2019. The relevant design is joint query–passage encoding followed by a relevance head. Later rerankers use a range of architectures and training objectives.

The final document representation depends on the query, so it cannot be precomputed once for all queries. Tokenization and some input processing can still be cached. Each candidate requires joint scoring, often in a batch.

With one million indexed passages and a shortlist of 100, the second stage scores 100 query–passage pairs. Sequence lengths, model size, hardware, batching and queueing determine latency.

Pairs are independent within a pointwise reranker and can be batched. Batching improves utilization but does not make 100 pairs cost the same as one. Measure throughput and tail latency under the intended load.


The worked example: five candidates, one query

Consider the query: how does paged attention manage GPU memory?

The following constructed example assigns two sets of scores and graded relevance labels to five candidates. It computes nDCG from those labels; it does not call a model.

One candidate set, two authored rankings

Query: how does paged attention manage GPU memory?

nDCG@5 = 0.661

  1. 1. D2 — Attention scores are computed as softmax(QK^T / sqrt(d_k)) in the original transformer paper.

    cos 0.74 · grade 0
  2. 2. D4 — A KV cache stores per-layer K and V tensors so decode avoids recomputing past activations.

    cos 0.71 · grade 2
  3. 3. D1 — vLLM paged attention fragments KV into blocks of tokens so GPU memory is reused like OS paging.

    cos 0.62 · grade 3
  4. 4. D3 — GPU memory allocators typically use best-fit or buddy systems; kernels request contiguous VRAM.

    cos 0.58 · grade 2
  5. 5. D5 — Paged attention removes the need to reserve contiguous KV regions upfront, cutting waste.

    cos 0.55 · grade 3

The bar spans 0–1: cosine in the first view, sigmoid(logit) in the second. These scales are not comparable probabilities. nDCG is computed from the authored relevance grades, not a benchmark result.

Authored cosine scores, logits and relevance grades illustrate a ranking change. Endpoint nDCG@5 is calculated from each complete ordering. Cross-encoder bar widths use sigmoid(logit), which is not a calibrated probability.

In the authored scores, D2 starts first even though it explains the general attention formula rather than memory management. D5 and D1 rise under the second ranking.

A real evaluation asks how often such corrections occur and how often the reranker makes a good ranking worse. Use relevance judgments across many queries, not this one designed example.

Some rerankers output a raw logit; others apply an activation. Read the model and library contract. A sigmoid maps a logit into 0–1 but does not by itself calibrate the result as a probability. Scores are safest as within-query ranking signals unless calibration has been tested.


Minimal math: what the scores actually are

A bi-encoder score:

sbi(q,d)=f(q)⋅f(d)∥f(q)∥ ∥f(d)∥s_{\text{bi}}(q, d) = \frac{f(q) \cdot f(d)}{\lVert f(q) \rVert \, \lVert f(d) \rVert}

where ff is a shared encoder. The query and the doc never see each other inside ff. They only meet at the dot product.

A cross-encoder score:

sce(q,d)=w⊤ g([q;SEP;d])s_{\text{ce}}(q, d) = w^\top \, g([q; \text{SEP}; d])

where gg is a transformer encoder over the concatenated sequence and ww is a small output head. There's no separation between query and doc inside gg. It's one sequence, and the self-attention layers mix information across the [SEP] freely. The document representation depends on the query, so there is no reusable g(d)g(d) to precompute.

Joint encoding lets the model condition its representation on the query at every layer. That adds expressive capacity and query-time work; improved ranking still depends on training and evaluation.


ColBERT: can we have both?

This is late interaction. ColBERT encodes the query and the document separately, like a bi-encoder. But instead of collapsing each into a single vector, it keeps one vector per token. At query time, you compute the similarity between every query token and every doc token, take the max over doc tokens for each query token, and sum those row-maxes:

MaxSim(q,d)=∑i∈qmax⁡j∈d Eq,i⋅Ed,j\text{MaxSim}(q, d) = \sum_{i \in q} \max_{j \in d} \, E_{q,i} \cdot E_{d,j}

Each query token gets to "vote" for its best-matching doc token. Then the votes are summed. No cross-attention, but a much richer signal than a single-vector dot product.

ColBERT MaxSim: token x token cosine, max across doc, sum across queryvllmpageskvcacheinblocksreusehowpagedattnmem0.180.220.150.120.200.140.170.350.880.300.280.100.720.400.400.320.780.680.090.350.300.250.300.650.580.120.450.62max = 0.22MaxSim(q, d) = ∑ max = 0.22constructed similarities; sum of highlighted row maxima

Each row is a query token, each column is a doc token, each cell is a cosine between their contextualized embeddings. ColBERT picks the max across each row (the orange boxes) and sums them. The query token 'paged' finds 'pages', 'attn' selects 'kv' (0.78), and the four row maxima sum to 2.53. These similarities are authored, not outputs from ColBERT. The doc can be pre-indexed once because the per-token embeddings don't depend on the query.

Document encoding happens at indexing time, and the query is encoded once. The MaxSim step then compares token vectors. This adds work over one-vector scoring but avoids joint encoding of every pair.

Storing one vector per token instead of one per document increases the index’s storage requirement.

ColBERTv2 uses residual compression to reduce token-vector storage: approximate each vector with a centroid plus a quantized residual. It also changes supervision. Its paper reports results for that combined system; compression does not make the index equivalent in size or cost to one vector per passage.


Compare the work before claiming a frontier

A latency–quality frontier requires a common dataset, candidate set, hardware and serving setup.

Compare interaction, storage and query work
Single-vector bi-encoder
Precompute one vector per passage. Encode the query once and compare vectors.
ColBERT late interaction
Precompute token vectors. Compare query and passage token vectors with MaxSim; keep a larger index.
Cross-encoder
Encode each query–passage pair jointly. Candidate count and sequence length determine the work; batching affects latency.
Generative reranker
Prompt for pairwise or listwise judgments. Include prompt length, output tokens and repeated calls in the budget.

Quality and latency require a common evaluation. Architecture alone does not establish a Pareto ranking.

Compare what each architecture precomputes, stores and scores at query time.

Record the candidate recall before reranking, ranking quality after it, and total service latency. This separates an upstream miss from a scoring error.

A small reranker may be sufficient, a larger one may help, or the existing ordering may already be good. Include the no-reranker baseline.


The cost side of the tradeoff

The calculator below assumes a price per scored pair. It makes the candidate-count multiplication explicit; it is not a managed-service price list.

Reranking budget calculator

1,000 queries × 100 candidates = 100,000 pairs
Assumed reranking cost = $100.00

The rate is an editable assumption, not a provider quote. This excludes retrieval and answer generation. Token-based or request-based billing needs a different cost model.

Editable cost assumptions: 1,000 queries × N candidates × dollars per pair. Token-priced and request-priced services require their own billing formula.

Scoring more candidates gives the reranker more opportunities to recover a good result and also more work. A larger shortlist only helps if it brings useful passages into consideration.

Choose several shortlist depths, evaluate their relevance gains, and time them under realistic load. Keep the final prompt budget separate from the number of candidates scored.

Reducing N reduces pair count exactly for this pointwise setup. Wall-clock latency may change less predictably because batches, padding and queueing also matter.


A tiny implementation

This sentence-transformers example retrieves and reranks the passages shown earlier.

from sentence_transformers import SentenceTransformer, CrossEncoder
import torch
 
# --- Stage 1: bi-encoder over a pre-encoded corpus ---
bi = SentenceTransformer("BAAI/bge-base-en-v1.5")
 
docs = [
    "vLLM paged attention fragments KV into blocks so GPU memory is reused.",
    "Attention is softmax(QK^T / sqrt(d_k)) V, from the original paper.",
    "GPU memory allocators use best-fit or buddy systems for VRAM.",
    "A KV cache stores per-layer K and V so decode skips recomputation.",
    "Paged attention removes contiguous KV reservations, cutting waste.",
]
 
# Pre-encoded once, reused across queries.
doc_vecs = bi.encode(docs, normalize_embeddings=True, convert_to_tensor=True)
 
query = "how does paged attention manage GPU memory?"
# BGE v1.5 recommends this instruction for short-query passage retrieval.
instruction = "Represent this sentence for searching relevant passages: "
q_vec = bi.encode(instruction + query, normalize_embeddings=True, convert_to_tensor=True)
 
cos = torch.matmul(doc_vecs, q_vec)       # shape (5,)
top_n = torch.topk(cos, k=5).indices.tolist()
print("bi-encoder order:", [docs[i][:40] for i in top_n])
 
# --- Stage 2: cross-encoder rerank over the candidates ---
ce = CrossEncoder("cross-encoder/ms-marco-MiniLM-L-6-v2")
 
pairs = [(query, docs[i]) for i in top_n]
ce_scores = ce.predict(pairs)             # shape (5,)
 
# Reorder by cross-encoder score, descending.
reranked = sorted(zip(top_n, ce_scores), key=lambda p: -p[1])
print("reranked order:", [docs[i][:40] for i, _ in reranked])

This is a pointwise two-stage example. It downloads weights and runs real inference when executed; its output need not match the authored scores in the visual.

The bi-encoder computes doc_vecs before the query arrives. Those vectors can be reused until the documents or encoder change. The cross-encoder’s pairs are built and scored at query time.


Misconceptions

"A reranker can rescue a bad retriever." It can improve the ordering of retrieved candidates. It cannot recover a relevant passage absent from that set. Measure candidate recall before attributing a final ranking failure to the reranker; a larger shortlist may recover evidence at additional scoring cost.

"Cross-encoders are a specific model architecture." They're not; "cross-encoder" describes how you use any sequence-to-scalar model. You can fine-tune BERT, a T5 (monoT5 does exactly this), a Gemma, an LLM, anything that takes a concatenated sequence and produces a relevance score. The architecture is just "encoder that reads (query, doc) jointly."

"ColBERT is only a reranker." Its token-level score can support retrieval over a full index or reranking of candidates. Those are different deployment choices.

"More rerankers stacked is better." A cascade can run a fast stage over many candidates and a slower stage over fewer. Each stage can also discard relevant evidence. Compare the whole cascade with a simpler baseline.

What's next

The next post covers RAG end-to-end, where we wire retrieval, chunking, reranking, and generation together, look at what breaks in production, and trace a query through the whole pipeline.


Additional reading (and watching)