Why do cross-encoder rerankers outperform dual-encoder retrievers at picking the most relevant chunks, given both models can be trained on the same data?
Same topic, related formats. Practice these next.
Same topic, related formats. Practice these next.
A cross-encoder reads the query and document together in one forward pass, so self-attention can cross between them. A dual-encoder embeds each in isolation and only compares afterward.
Imagine two ways to judge whether a job applicant fits a role. In the first way, two people separately write a one paragraph summary, one of the job, one of the applicant, then a clerk who never read either resume just checks how similar the two paragraphs sound. That's a dual-encoder: fast, but it squeezes everything into a summary before comparing. In the second way, a hiring manager reads the job description and the resume side by side, line by line, noticing that this specific skill matches that specific requirement. That's a cross-encoder: slower, because someone has to read every job applicant pair fresh, but far more accurate because the comparison happens at the level of actual words, not pre-written summaries. RAG uses the fast clerk to shortlist, then the careful manager to rank that shortlist.
Everything you need to truly understand this topic: intuition, mechanics, step by step explanation, code, formulas, and worked example. Click to expand.
Everything you need to truly understand this topic: intuition, mechanics, step by step explanation, code, formulas, and worked example.
Everything important, quickly.
3 min: contrast joint vs independent encoding, explain why pooling loses token interactions, derive the per-pair cost, then justify the two-stage retrieve then rerank funnel and name a production reranker.
from sentence_transformers import SentenceTransformer, CrossEncoder
# Stage 1: dual-encoder retrieval (embeds query and docs independently)
bi = SentenceTransformer("BAAI/bge-base-en-v1.5")
q_vec = bi.encode(query, normalize_embeddings=True)
candidates = ann_index.search(q_vec, top_k=100) # cosine over precomputed doc vectors
# Stage 2: cross-encoder rerank (scores each (query, doc) pair jointly)
ce = CrossEncoder("cross-encoder/ms-marco-MiniLM-L-6-v2")
pairs = [(query, c.text) for c in candidates]
scores = ce.predict(pairs) # one joint forward pass per pair
reranked = [c for _, c in sorted(zip(scores, candidates), reverse=True)][:5]| Property | Dual-encoder (bi-encoder) | Cross-encoder |
|---|---|---|
| Encoding | Query and doc embedded separately | Query and doc concatenated, encoded jointly |
| Interaction | One dot product of pooled vectors | Token-level self-attention across the pair |
| Scoring | Cosine / dot-product similarity | Relevance score from a regression head |
| Doc vectors precomputable | Yes, index once and reuse | No, fresh forward pass per query-doc pair |
| Cost at query time | 1 embed + ANN lookup | O(candidates) forward passes |
| Role in RAG | First-stage retrieval over millions | Second-stage rerank over top-k |
Real products, models, and research that use this idea.
What an interviewer would ask next. Try answering before peeking at the approach.
Red flags and common mistakes that signal junior thinking. Click to expand.
Claiming cross-encoders win because they are larger or train on more data. The real reason is architectural: joint encoding lets self-attention cross between query and document tokens.
The night-before-the-interview bullets. Scan these on the way to the call.
Primary sources. Skim if you want the original framing.