RAG Cross-Encoder Reranking: Precision Retrieval for Enterprise AI

TL;DR — Cross-encoder reranking is a two-stage retrieval pattern: bi-encoder (vector search) retrieves top-50 candidates fast, then cross-encoder reranks them for precision. Bi-encoders encode query and document separately (fast, O(1) lookup, imprecise). Cross-encoders encode query and document together in one transformer pass (slow, O(n), precise). The standard RAG pipeline: hybrid retrieval with RRF fusion → cross-encoder reranking → LLM generation. Best open-source reranker: BGE-Reranker-v2-m3 (568M params, multilingual, Apache 2.0). Best managed API: Cohere Rerank 3. Reranking adds 100-200ms latency but improves NDCG@10 by 10-20%. Don't use reranking when latency budget is under 200ms or first-stage retrieval is already high-precision. Combine with query rewriting for maximum accuracy.

Cross-encoder reranking is the precision layer in a RAG pipeline. After hybrid retrieval produces top-50 candidates, a cross-encoder reranks them to surface the most relevant documents for the LLM.

The problem it solves: bi-encoder (vector) retrieval is fast but imprecise. It encodes the query and document independently, missing fine-grained interactions. A cross-encoder encodes them together, capturing token-level relationships — but it's too slow for searching millions of documents. The two-stage pattern gets the best of both: fast first-stage retrieval, precise second-stage reranking.

Bi-Encoder vs Cross-Encoder

Dimension Bi-Encoder Cross-Encoder
How it works Encode query and doc separately, compute cosine similarity Encode query + doc together in one transformer pass
Speed O(1) per document (pre-computed embeddings) O(n) per document (full transformer inference)
Accuracy Good (captures semantic similarity) Excellent (captures token-level interactions)
Scalability Millions of documents 50-100 candidates only
Use case First-stage retrieval Second-stage reranking
Model examples text-embedding-3, BGE-large, E5 BGE-Reranker, Cohere Rerank, Voyage rerank
Latency 10-50ms 100-200ms
Cost Pre-compute embeddings once Per-query inference
flowchart LR Query["User Query"] --> BiEncoder["Bi-Encoder
(vector search)"] BiEncoder --> Top50["Top-50 Candidates
(fast, imprecise)"] Top50 --> CrossEncoder["Cross-Encoder
(reranking)"] CrossEncoder --> Top10["Top-10 Results
(precise, slow)"] Top10 --> LLM["LLM Generation"] style BiEncoder fill:#4169E1,color:#fff style CrossEncoder fill:#39FF14,color:#000

How Cross-Encoder Reranking Works

class CrossEncoderReranker:
    """Rerank retrieved documents using a cross-encoder model."""

    def __init__(self, model_name: str = "BAAI/bge-reranker-v2-m3"):
        from sentence_transformers import CrossEncoder
        self.model = CrossEncoder(model_name)

    def rerank(self, query: str, documents: list, top_k: int = 10) -> list:
        """Rerank documents by cross-encoder relevance score."""
        # Create query-document pairs
        pairs = [[query, doc["content"]] for doc in documents]

        # Score each pair with cross-encoder
        scores = self.model.predict(pairs)

        # Attach scores and sort
        for doc, score in zip(documents, scores):
            doc["rerank_score"] = float(score)

        # Sort by rerank score descending
        reranked = sorted(documents, key=lambda d: -d["rerank_score"])

        return reranked[:top_k]

Full Pipeline with Hybrid Retrieval + Reranking

class RAGPipelineWithReranking:
    """Full RAG pipeline: hybrid retrieval → RRF fusion → cross-encoder reranking."""

    def __init__(self, bm25_store, vector_store, reranker, llm, k_rrf=60):
        self.bm25_store = bm25_store
        self.vector_store = vector_store
        self.reranker = reranker
        self.llm = llm
        self.k_rrf = k_rrf

    def retrieve(self, query: str, user_id: str, top_k: int = 10) -> list:
        # Stage 1: Hybrid retrieval (BM25 + vector)
        bm25_results = self.bm25_store.search(query, top_k=50)
        vector_results = self.vector_store.search(
            query_embedding=self._embed(query),
            top_k=50,
            filter_acl=self._get_user_acls(user_id)
        )

        # Stage 1b: RRF fusion
        fused = self._rrf_fuse(bm25_results, vector_results, k=self.k_rrf)

        # Stage 2: Cross-encoder reranking (top-50 → top-k)
        reranked = self.reranker.rerank(query, fused[:50], top_k=top_k)

        return reranked

    def answer(self, query: str, user_id: str) -> dict:
        # Retrieve with reranking
        documents = self.retrieve(query, user_id, top_k=10)

        # Check confidence
        if not documents or documents[0]["rerank_score"] < 0.1:
            return {"answer": "I don't have enough information.", "sources": []}

        # Generate with citations
        context = self._format_context(documents)
        answer = self.llm.generate(query=query, context=context)

        return {
            "answer": answer,
            "sources": documents[:5],
            "rerank_scores": [d["rerank_score"] for d in documents[:5]]
        }

Reranker Model Comparison

Model Type Size Languages Latency (50 docs) License Best For
BGE-Reranker-v2-m3 Self-hosted 568M 100+ ~100ms (GPU) Apache 2.0 Enterprise self-hosted
Cohere Rerank 3 API 100+ ~150ms (API) Proprietary Managed, low setup
Jina Reranker v2 API/Self-hosted 278M 100+ ~80ms (GPU) CC-BY-NC Multilingual
mxbai-rerank Self-hosted 110M English ~50ms (GPU) Apache 2.0 Lightweight
Voyage rerank-2 API English ~120ms (API) Proprietary Highest accuracy
FlashRank Self-hosted 8M English ~20ms (CPU) Apache 2.0 Ultra-low latency
RankLLM LLM-based varies varies ~500ms Open LLM-as-judge

Latency Breakdown

Pipeline Stage Latency % of Total
Query embedding 10-20ms 2%
Hybrid retrieval (BM25 + vector) 20-50ms 5%
RRF fusion <1ms 0%
Cross-encoder reranking 100-200ms 10%
LLM generation 500-2000ms 80%
Citation verification 10-30ms 3%

Reranking is 10% of total latency but improves NDCG@10 by 10-20%. The ROI is clear: a 150ms investment for a 15% accuracy gain.

When to Use Reranking

Scenario Use Reranking? Why
Enterprise knowledge base ✅ Yes Precision matters for business answers
Customer support chatbot ✅ Yes Wrong answers damage trust
Simple FAQ lookup ❌ No BM25 is sufficient, latency matters
Real-time search (<200ms budget) ❌ No Reranking exceeds budget
Large candidate pool (top-100) ✅ Yes More candidates need precision
Small candidate pool (<10) ❌ No Not enough candidates to rerank
Hybrid retrieval already high precision ⚠️ Maybe Measure NDCG@10 first
Multi-hop reasoning ✅ Yes Precision per hop compounds

Fine-Tuning Rerankers

For domain-specific accuracy, fine-tune a reranker on your query-document pairs:

class RerankerFineTuner:
    """Fine-tune a cross-encoder on domain-specific data."""

    def prepare_training_data(self, queries, relevant_docs, irrelevant_docs):
        """Create training pairs: (query, doc, label)."""
        train_data = []

        for query, rel_docs, irr_docs in zip(queries, relevant_docs, irrelevant_docs):
            # Positive pairs (relevant docs)
            for doc in rel_docs:
                train_data.append((query, doc, 1.0))

            # Negative pairs (irrelevant docs)
            for doc in irr_docs:
                train_data.append((query, doc, 0.0))

        return train_data

    def fine_tune(self, model_name, train_data, epochs=3):
        from sentence_transformers import CrossEncoder, InputExample
        from torch.utils.data import DataLoader

        model = CrossEncoder(model_name)

        train_examples = [
            InputExample(texts=[q, d], label=label)
            for q, d, label in train_data
        ]

        train_dataloader = DataLoader(train_examples, batch_size=16, shuffle=True)

        model.fit(
            train_dataloader=train_dataloader,
            epochs=epochs,
            warmup_steps=100,
            output_path="./fine-tuned-reranker"
        )

RAG Cross-Encoder Reranking Checklist

  • [ ] Start with hybrid retrieval (BM25 + vector + RRF) as first stage
  • [ ] Retrieve top-50 candidates from first stage (3-5x final result count)
  • [ ] Choose reranker: BGE-Reranker-v2-m3 (self-hosted) or Cohere Rerank 3 (API)
  • [ ] Rerank top-50 down to top-10 for LLM context
  • [ ] Measure NDCG@10 before and after reranking to validate improvement
  • [ ] Set rerank score threshold — filter documents below 0.1 score
  • [ ] Profile latency: reranking should be under 200ms
  • [ ] For self-hosted: use GPU for BGE-Reranker (100ms vs 300ms on CPU)
  • [ ] For ultra-low latency: use FlashRank (20ms on CPU, 8M params)
  • [ ] Consider fine-tuning on domain-specific query-document pairs
  • [ ] Use query rewriting before retrieval for better candidates
  • [ ] Apply RRF fusion before reranking
  • [ ] Enforce document-level ACLs in first-stage retrieval
  • [ ] Monitor reranking latency p99 (target: <200ms)
  • [ ] A/B test reranked vs non-reranked results on user satisfaction
  • [ ] Consider ColBERT as a middle ground (late interaction, faster than cross-encoder)
  • [ ] For hallucination prevention: high rerank scores = high confidence
  • [ ] Integrate with AI knowledge base pipeline
  • [ ] Cache reranking results for repeated queries
  • [ ] Log rerank scores for audit trail and quality monitoring

FAQ

What is cross-encoder reranking in RAG?

Cross-encoder reranking is a two-stage retrieval pattern where a fast bi-encoder (vector search) retrieves top-50 candidates, then a cross-encoder reranks them for precision. A bi-encoder encodes the query and document separately and computes cosine similarity — fast but imprecise. A cross-encoder encodes the query and document together in a single transformer pass, capturing fine-grained interactions — precise but slow. The cross-encoder is too slow for searching millions of documents, so it's used only to rerank the top candidates from the first stage.

What is the difference between bi-encoder and cross-encoder?

A bi-encoder encodes the query and document independently into vectors, then computes similarity (cosine, dot product). This is fast (O(1) lookup) but misses query-document interactions. A cross-encoder concatenates the query and document as a single input to a transformer, which attends to all token interactions. This is more accurate but O(n) per document — too slow for large-scale retrieval. The standard pattern: bi-encoder for first-stage retrieval (top-50), cross-encoder for second-stage reranking (top-10).

Which cross-encoder reranker should I use?

For self-hosted: BGE-Reranker-v2-m3 (BAAI) is the best open-source option — multilingual, 568M parameters, Apache 2.0 license. For managed API: Cohere Rerank 3 is production-ready with low latency. For lightweight deployment: FlashRank or mxbai-rerank are fast and small. For enterprise: Voyage rerank-2 offers the highest accuracy but requires API access. Choose based on your self-hosted vs managed preference, latency budget, and language requirements.

How much latency does cross-encoder reranking add?

Cross-encoder reranking adds 50-200ms depending on the model and number of candidates. BGE-Reranker-v2-m3 reranking 50 candidates takes ~100ms on GPU, ~300ms on CPU. Cohere Rerank 3 API adds ~100-150ms network latency. The total RAG pipeline with reranking is typically: retrieval (20-50ms) + reranking (100-200ms) + generation (500-2000ms). Reranking is 5-15% of total latency but improves NDCG@10 by 10-20%.

When should you NOT use cross-encoder reranking?

Don't use cross-encoder reranking when: (1) latency budget is under 200ms total (reranking alone takes 100-200ms), (2) your first-stage retrieval already has high precision (NDCG@10 > 0.9), (3) you're doing simple factual lookups where BM25 is sufficient, (4) your candidate pool is small (<10 documents), (5) cost is a concern and you're using a paid API reranker at high volume. For these cases, hybrid retrieval with RRF fusion is sufficient.


Want a self-hosted AI company brain that does all of this out of the box?
Book a demo →