RAG Reranking: How to Add a Second-Stage Reranker to Retrieval
First-stage retrieval (vector ANN or TF-IDF) casts a wide net for recall, but the most relevant passage often lands second or third - not first. A second-stage reranker rescores only the shortlist with a heavier relevance function and reorders it, so the best answer reaches the LLM at position 1.
Why retrieval order matters
When you pass retrieved passages to an LLM, it weights the top context most. If the most relevant document is ranked third, answer quality drops - even though it was fetched. Retrieval and ranking are two separate jobs:
- First stage (retriever) - fast, coarse; bi-encoders or TF-IDF scan millions of documents for recall. They bring the right answer somewhere in the top-k.
- Second stage (reranker) - slower, more precise; rescores only the shortlisted candidates with a richer (query, doc) relevance signal and reorders them.
The two-stage shape is the industry standard pattern because applying a heavy cross-encoder to the full corpus is too slow; running it on just the top-k is fast.
Implementing rerank()
Given a list of candidate passages from the retriever, score each one against the query and sort by score descending:
def relevance(query: str, doc: str) -> float:
"""Score one (query, doc) pair. Higher = more relevant."""
q_terms = set(query.lower().split())
d = doc.lower()
# exact word match scores higher than substring match
return sum(3 if t in d.split() else (1 if t in d else 0) for t in q_terms)
def rerank(query: str, candidates: list[str]) -> list[str]:
"""Return candidates sorted by relevance score, highest first."""
return sorted(candidates, key=lambda doc: relevance(query, doc), reverse=True)
Then wire it into your search() function as a second stage:
def search(query: str, k: int = 2) -> list[str]:
candidates = retrieve(query, k=10) # broad first-stage fetch
ranked = rerank(query, candidates) # precision reorder
return ranked[:k] # return top k
Fetching more candidates at the first stage (e.g. 10) and trimming after reranking (to 2) is the right shape - more recall, then higher precision.
Production rerankers
In production, the relevance() function is typically a cross-encoder - a model
that reads both the query and the document together before scoring, which gives far
better precision than a bi-encoder or lexical score:
- Cohere Rerank API - cloud, one API call for the whole shortlist
- BGE-Reranker / MiniLM cross-encoder - self-hosted via
sentence-transformers - ColBERT late-interaction models - good accuracy/speed tradeoff
All of them plug into the same rerank(query, candidates) slot; swapping the
relevance function does not change the pipeline shape.
What to watch for
- Fetch more, rerank to fewer - first stage
kshould be 3-10x the finalkso the reranker has a real shortlist to work with. - Don't rerank the whole corpus - reranking is O(k), not O(N); keep first-stage retrieval as the broad filter.
- Score ties - use a stable sort (Python's
sortedis stable) and consider a tiebreak on retriever rank to preserve original ordering within equal scores.
Want to try it hands-on? HeyDevJob gives you this exact setup in a live cloud workspace in your browser - edit it, run it, and see it work. Free, nothing to install.
Try it in a workspace →What you'll practice
- Implementing a rerank() function that scores and sorts retrieval candidates
- Wiring a two-stage retrieve-then-rerank pipeline in Python
- Understanding when to use a cross-encoder vs a bi-encoder in RAG
FAQ
What is reranking in RAG?
Reranking is a second retrieval stage that takes the top-k candidates returned by the first-stage retriever, scores each one against the query with a more precise relevance function, and reorders them so the most relevant passage is first. It improves answer quality without changing what the retriever fetches.
Why not just use a better retriever instead of reranking?
Fast retrievers (bi-encoders, BM25) trade precision for speed across a large corpus. A cross-encoder reranker is much more accurate but too slow to run on every document - so you use both: retriever for recall on the full corpus, reranker for precision on the shortlist.
What cross-encoder rerankers are used in production?
Common choices are the Cohere Rerank API (managed, no GPU needed), BGE-Reranker or a fine-tuned MiniLM via sentence-transformers (self-hosted), and ColBERT for a good accuracy/speed balance. All accept (query, doc) pairs and return a relevance score.
Keep learning
Learn it by doing. Open this in a live cloud workspace, make the change yourself, and keep a record of the work you can share.
Open the workspace →