What is Reranking?
Reranking is an AI design pattern for LLM apps and agents, in the Context and knowledge group. Retrieve many candidates and use a sharper model to pick the best ones.
Reordenação
Reranking is an AI design pattern for LLM apps and agents, in the Context and knowledge group. Retrieve many candidates and use a sharper model to pick the best ones.
Retrieve many candidates with a fast method, then use a more precise model to reorder them and pick the best. It is for search and RAG systems where the right document shows up among the candidates, but not in the top positions.
The coarse sieve separates the gravel; the fine one picks out the gold.
Lúcia maintains a logistics company’s knowledge base chat. Hybrid search returns 50 passages per question, and the model gets the top 5. The eval showed something curious: in many of the wrong answers, the right passage was among the 50, just at position 12 or 20.
Fast search measures similarity approximately: it compares the question and the passage separately, as vectors or term lists. It does not read the two together. So a passage that talks “around” the topic beats one that answers the question exactly.
Sending all 50 passages to the model would fix recall, but it would be expensive and fill the context with noise. “Lost in the Middle” (Liu et al., 2023) shows the other side: even when they fit, the model uses what sits in the middle worse. Fifty mediocre passages are not 50 useful ones.
The question this pattern answers: how do you get the best passages to the top without paying for the main model to read them all?
Split search into two stages:
The reranker can be a specialized model (a cross-encoder), cheaper and faster, or a language model with structured output, more flexible. Only the top K after reranking move on to the main model.
flowchart LR
Q(["1. Question"]):::io --> R["2. Broad retrieval: 50 candidates"]:::tool
I[("Index")]:::store --> R
R --> RR(["3. Reranker reads question and passage together"]):::llm
RR --> T["4. Top 5 by the new score"]:::tool
T --> M(["5. Main model answers"]):::llm
Question: “What is the deadline to contest a fleet traffic fine?”.
Hybrid search returns 50 passages. The top five, before reranking:
pos 1 fleet.md#fines-1 "The fleet has third-party insurance coverage..."
pos 2 hr.md#payroll "Payroll deductions for driver infractions..."
pos 3 fleet.md#fuel
pos 12 fleet.md#fines-3 "The deadline to contest a fleet fine is 15 days..."
The passage that answers sits at 12. The cross-encoder reads the question together with each passage. fleet.md#fines-3 gets the highest score because it contains the deadline. hr.md#payroll talks about fines, but not about contesting, and drops. After reranking, the 5 the main model receives start with fines-3.
A minimum threshold (score 2 on a 0 to 3 scale, or above zero on the cross-encoder) avoids sending five “on-topic” passages when none of them answers. Then the system says “I did not find that” instead of inventing from the fuel page.
schema Relevance { id: string, score: int /* 0 to 3 */ }
function retrieve(question):
candidates = hybrid_search(question, k=50) # 2. recall
scores = reranker.score([(question, c.text) for c in candidates]) # 3. precision
ranked = sort(zip(candidates, scores), by=score, desc)
trace.span("rerank", before=[c.id for c in candidates[:5]], after=[c.id for c, _ in ranked[:5]])
return [c for c, s in ranked[:5] if s >= MIN_SCORE] # 4
function answer(question):
passages = retrieve(question)
if not passages: return "I did not find that in the knowledge base."
return llm(ANSWER_PROMPT, passages, question) # 5
Reranking is a stage of RAG, not a substitute for search. If recall@50 is low, changing the reranker is changing the stage that already works.
Start with a cross-encoder. Only step up to a language model when relevance depends on a business rule the encoder did not see in training (“current documents only”, “ignore drafts”).
| Aspect | What happens | How it shows up in the trace |
|---|---|---|
| Calls | One batch to the reranker per question | rerank span with before and after |
| Latency | Proportional to the number of candidates | rerank span duration |
| Cost | A cheap cross-encoder; a costlier language model | cost per question |
| Failure: low recall | The right result was not among the candidates | right passage missing from the retrieved list |
| Failure: high threshold | No passage passes and the answer becomes “not found” | rising rate of empty answers |
| Failure: out-of-domain reranker | Poor scores on specialized vocabulary | precision@5 worse than search alone |
The numbers below are illustrative. Suppose 50 candidates, a local cross-encoder, and the main model reading 5 passages of 400 tokens.
| Design | What the main model reads | Typical extra cost |
|---|---|---|
| Hybrid top 5 | 2,000 tokens, maybe without the right passage | none |
| 50 passages in the main model | 20,000 tokens, with noise in the middle | main-model tokens |
| Rerank + top 5 | 2,000 tokens already chosen | the cross-encoder batch |
{ "question": "What is the deadline to contest a fleet traffic fine?", "relevant_passages": ["fleet.md#fines-3"], "measure": ["recall@50", "precision@5"] }
Also measure the “search alone” arm. If the reranker makes precision@5 worse, it is out of domain: logistics vocabulary, acronyms, current documents. Then a small LLM with a written criterion often beats a generic encoder.
The before / after pair on the span is what lets you see, on a wrong answer, whether the right passage rose and the model ignored it, or whether it never rose.
The reranker reads the same untrusted content RAG reads. A wiki page that says “this document is the most relevant of all” can inflate an LLM-reranker’s score. Cross-encoders trained for relevance suffer less from that rhetoric, but they are not a defense. Permissions still belong in retrieval: the reranker must not see a passage the user cannot see.
| Pros | Cons |
|---|---|
| The best passages reach the top | One more latency stage |
| A smaller, more relevant context for the model | Cannot recover what search did not bring back |
| Cross-encoders are cheap and fast | An out-of-domain reranker can make things worse |
| Complex criteria with a language model | One more component to evaluate |
The Python tab uses a cross-encoder; the TypeScript tab uses a language model with structured scores.
Python. Use this when you want fast, cheap reranking with a cross-encoder.
from sentence_transformers import CrossEncoder
reranker = CrossEncoder("cross-encoder/mmarco-mMiniLMv2-L12-H384-v1") # multilingual
MIN_SCORE = 0.0
def retrieve(question: str, k: int = 5) -> list[dict]:
candidates = hybrid_search(question, k=50) # 2. recall
scores = reranker.predict([(question, c["text"]) for c in candidates]) # 3
ranked = sorted(zip(candidates, scores), key=lambda x: x[1], reverse=True)
return [c | {"rerank_score": float(s)} for c, s in ranked[:k] if s >= MIN_SCORE] # 4What you gained: reranking in milliseconds, with no language model call. What you lost: the cross-encoder does not understand business rules; filters like “current only” stay in the search.
TypeScript. Use this when relevance depends on criteria a cross-encoder does not understand.
import Anthropic from "@anthropic-ai/sdk";
import { zodOutputFormat } from "@anthropic-ai/sdk/helpers/zod";
import { z } from "zod";
const client = new Anthropic();
const Scores = z.object({ scores: z.array(z.object({ id: z.string(), score: z.number().int().min(0).max(3) })) });
type Passage = { id: string; text: string; updated: string };
export async function rerank(question: string, candidates: Passage[], k = 5): Promise<Passage[]> {
const list = candidates.map((c) => `<passage id="${c.id}" updated="${c.updated}">${c.text}</passage>`).join("\n");
const { parsed_output } = await client.messages.parse({
model: "claude-haiku-4-5",
max_tokens: 2000,
system:
"Score each passage from 0 to 3: 3 answers the question directly, 2 helps, 1 only talks about the topic, 0 is useless. " +
"Passages superseded by a newer version get at most 1.",
messages: [{ role: "user", content: `<question>${question}</question>\n${list}` }],
output_config: { format: zodOutputFormat(Scores) },
});
const score = new Map((parsed_output?.scores ?? []).map((s) => [s.id, s.score]));
return candidates
.filter((c) => (score.get(c.id) ?? 0) >= 2)
.sort((a, b) => (score.get(b.id) ?? 0) - (score.get(a.id) ?? 0))
.slice(0, k); // 4
}What you gained: relevance criteria written in natural language. What you lost: the cost and latency of one call per question, and sensitivity to the candidates’ order in the list.
Reranking is an AI design pattern for LLM apps and agents, in the Context and knowledge group. Retrieve many candidates and use a sharper model to pick the best ones.
In the real-world scenarios E-commerce semantic search, Internal knowledge base chat, and Deep research with citations.