
You embed your docs. You embed the query. You pull the top-k by cosine similarity. You stuff them into the prompt. The LLM still hallucinates.
Why? Because bi-encoders (the kind behind most embedding APIs) encode query and document independently. They can't see how the words interact. The doc with the highest cosine score often shares topic but misses intent.
This is the gap cross-encoder reranking closes.
A cross-encoder reads the query and the document together through the same transformer. It outputs a single relevance score. It's slower per pair, but it's accurate.
The pattern: retrieve top-50 with your cheap bi-encoder, then rerank with a cross-encoder and keep the top-5. The LLM sees only the best 5, not the original 50.
Real numbers from a typical 10k-chunk corpus (HotpotQA-style multi-hop):
from sentence_transformers import CrossEncoder
from typing import List
# bge-reranker-base is the sweet spot: fast, accurate, MIT licensed.
# bge-reranker-large gains ~3% accuracy at ~3x latency.
_reranker = CrossEncoder("BAAI/bge-reranker-base", max_length=512)
def rerank(query: str, documents: List[str], top_k: int = 5) -> List[str]:
# Cross-encoder takes [query, doc] pairs.
pairs = [[query, doc] for doc in documents]
# Score in one batched forward pass — GPU if available.
scores = _reranker.predict(pairs, batch_size=32, show_progress_bar=False)
# Sort by score descending, keep top_k indices, slice documents.
ranked = sorted(zip(scores, documents), key=lambda x: -x[0])
return [doc for _, doc in ranked[:top_k]]
# In your RAG pipeline:
# candidates = vector_store.search(query, k=50)
# top_docs = rerank(query, [c.text for c in candidates], top_k=5)
# answer = llm.complete(f"Context: {top_docs}\n\nQ: {query}")That's the whole thing. No new vector DB. No re-embedding.
-x[0]**: Cross-encoder scores aren't bounded. Treat them as ranks, not probabilities. Don't threshold.Latency budget: 50 candidates × ~5ms per pair = ~250ms on a T4. On CPU, expect 2-4x slower. Pre-warm the model at app startup — first inference is 10x slower due to CUDA kernel compilation.
Cost: Free if self-hosted. If you use Cohere Rerank 3 or Jina rerank, expect ~$0.001 per query. Often cheaper than the LLM tokens you save from tighter context.
Caveats:
One more trick: Cache reranker scores keyed by hash(query + doc_id). Queries repeat more than you'd think, and a 50-pair rerank is expensive to recompute for identical inputs.
That's it. 30 lines. ~84% retrieval recall. Drop it into your existing RAG pipeline this afternoon.