MODULE 3  ยท  Embeddings & Vector Search

Reranking: Why Your First Search Results Aren't the Best

Your vector DB returns 10 results. The perfect answer is at position #7. Reranking fixes that.

๐Ÿ“… Mar 2026
โฑ 10 min read
๐ŸŽฏ Episode 21 of 98
RerankingCross-EncoderSearch QualityRAG
In this episode

You search "how to handle database connection timeouts in Python." Your vector DB returns 10 results. Result #1 is about Python exception handling. Result #7 is a perfect guide on psycopg2 connection pool timeout configuration.

Why is #7 not #1?

Because your embedding model used a bi-encoder โ€” it embedded the query and the documents separately, then compared their vectors. Fast, but it can't see the nuances. It doesn't know that "database connection timeouts" specifically means psycopg2 pool configuration in this context.

A reranker takes those 10 results and rescores them by reading the query AND each document together, word by word. It sees the nuances. It pushes #7 to #1.

Two-stage retrieval: cast a wide net, then pick the best fish. This is how every serious search system works โ€” Google, Bing, Amazon, Netflix. And now your RAG pipeline.

Bi-Encoders vs Cross-Encoders

This is the fundamental trade-off behind all of search.

Bi-Encoder (What You've Been Using)

The embedding model you've been using is a bi-encoder. It encodes the query and documents independently:

Query: "database timeout Python" โ†’ Encoder โ†’ [0.23, -0.41, 0.87, ...]

example
code
โ†“
                                              Cosine Similarity โ†’ 0.82
                                                     โ†‘

Doc: "Handling psycopg2 pool..." โ†’ Encoder โ†’ [0.19, -0.38, 0.91, ...]

Key property: The document embedding is computed ONCE during indexing. At query time, you only embed the query and compute similarities. This is why it scales โ€” you can search millions of documents in milliseconds.

The limitation: The query and document never "see" each other. The encoder can't reason about how they interact. It's like judging compatibility from two separate resumes without ever putting the people in a room together.

Cross-Encoder (The Reranker)

A cross-encoder reads the query and document TOGETHER:

Input: "[CLS] database timeout Python [SEP] Handling psycopg2 connection

example
code
pool timeout configuration with retry logic in Python [SEP]"
                              โ†“
                      Transformer (all tokens attend to all tokens)
                              โ†“
                      Relevance Score: 0.94

Key property: Every word in the query attends to every word in the document. The model sees that "timeout" in the query relates to "timeout configuration" in the document, that "database" relates to "psycopg2" (a database driver), and that "Python" appears in both.

The limitation: You can't pre-compute anything. For each query, you must run the cross-encoder on EVERY candidate document. With 10 million documents, that's 10 million forward passes โ€” way too slow.

The Comparison

Bi-Encoder Cross-Encoder

Speed~0.001ms per comparison~10ms per comparison
AccuracyGoodExcellent
Pre-computationDocuments embedded onceNothing pre-computed
ScaleMillions of documents10-100 candidates
Use in pipelineFirst-stage retrievalSecond-stage reranking

The solution is obvious: use the bi-encoder to get top 50-100 candidates fast, then use the cross-encoder to rerank those candidates accurately.

โšก

Two-Stage Retrieval Pipeline

User Query

example
code
โ†“

Stage 1: RETRIEVAL (bi-encoder + vector DB)

example
code
"Get me the top 50 candidates"
    Speed: 5-20ms for millions of documents
    Recall: ~90% (might miss some relevant docs)
    โ†“

50 candidates

example
code
โ†“

Stage 2: RERANKING (cross-encoder)

example
code
"Score each of these 50 against the query"
    Speed: 50 ร— 10ms = 500ms
    Precision: ~95%+ (much better ordering)
    โ†“

Top 5 results โ†’ LLM context

Without reranking, you send the top 5 from stage 1 โ€” which might have the best result at position #7. With reranking, the cross-encoder fixes the ordering and pushes the actually-best result to #1.

๐Ÿ”ง

Reranking Models

Cohere Rerank

The most popular API reranker. Dead simple:

python

import cohere

snippet
code
co = cohere.Client("YOUR_API_KEY")
results = co.rerank(
model="rerank-english-v3.0",
    query="database timeout Python",
    documents=[
        "Handling psycopg2 connection pool timeout configuration...",
        "Python exception handling best practices...",
        "Database migration strategies in Django...",
        "Connection retry logic with exponential backoff...",
    ],
    top_n=3,
    return_documents=True

)

for r in results.results:

example
code
print(f"Score: {r.relevance_score:.4f}  Doc: {r.document.text[:60]}")

Score: 0.9842 Doc: Handling psycopg2 connection pool timeout configuration...

Score: 0.8231 Doc: Connection retry logic with exponential backoff...

Score: 0.4102 Doc: Python exception handling best practices...

Pricing: ~$0.002 per 1000 documents reranked. Cheap enough for production.

Open Source: Cross-Encoder Models

python

from sentence_transformers import CrossEncoder

snippet
code
model = CrossEncoder('cross-encoder/ms-marco-MiniLM-L-6-v2')
query = "database timeout Python"
documents = [
"Handling psycopg2 connection pool timeout configuration...",
    "Python exception handling best practices...",
    "Database migration strategies in Django...",

]

Score each query-document pair

snippet
code
pairs = [[query, doc] for doc in documents]
scores = model.predict(pairs)

Sort by score

snippet
code
ranked = sorted(zip(documents, scores), key=lambda x: x[1], reverse=True)

for doc, score in ranked:

example
code
print(f"Score: {score:.4f}  Doc: {doc[:60]}")

Popular Reranker Models

Model Type Speed Quality Best For

Cohere rerank-v3APIFastExcellentProduction, ease of use
cross-encoder/ms-marco-MiniLM-L-6Open sourceVery fastGoodSelf-hosted, latency-sensitive
BAAI/bge-reranker-v2-m3Open sourceMediumExcellentSelf-hosted, quality-first
Jina rerankerAPI + open sourceFastVery goodMultilingual
Mixedbread rerankerOpen sourceFastVery goodGood all-rounder

๐Ÿ“Š

The Impact: Before vs After Reranking

Real benchmark from a RAG evaluation on a technical documentation corpus:

Metric Without Reranking With Reranking

โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

Recall@5 0.72 0.72 (same โ€” reranking doesn't add docs)

Precision@5 0.58 0.81 (+40%)

MRR (Mean Reciprocal 0.65 0.88 (+35%)

example
code
Rank)

NDCG@5 0.61 0.84 (+38%)

Same retrieved documents, dramatically better ordering. The best result is now actually on top.

What This Means for RAG

If you retrieve top-5 chunks and stuff them into the LLM prompt:

Without reranking: The most relevant chunk might be at position #4 (or not in top-5 at all from the retrieval pool)

With reranking: The most relevant chunk is at position #1

Since LLMs are affected by the "lost in the middle" problem (more on this in Episode 26), having the best content at position #1 directly improves answer quality.

When NOT to Rerank

Reranking isn't free. Here's when to skip it:

Situation Why Skip Reranking

Simple FAQ lookup Bi-encoder retrieval is already precise

Latency-critical (<50ms total)Reranking adds 100-500ms
Very small corpus (<1000 docs)Brute-force is accurate enough
Query is very specificExact keyword match already wins
Budget is extremely tightAPI rerankers add per-query cost

Rule of thumb: If your retrieval precision is already above 85% and latency matters, skip reranking. If precision is below 75%, reranking is the highest-impact improvement you can make.

๐Ÿ’ก

Full Pipeline Integration

Here's a complete two-stage retrieval with Qdrant + Cohere reranking:

python

import cohere

from qdrant_client import QdrantClient

Initialize

snippet
code
qdrant = QdrantClient("localhost", port=6333)
co = cohere.Client("YOUR_COHERE_KEY")
def search_with_reranking(query: str, top_k: int = 5):
# Stage 1: Vector retrieval (wide net)
    query_vector = embed(query)  # Your embedding function
    candidates = qdrant.search(
        collection_name="docs",
        query_vector=query_vector,
        limit=50,  # Retrieve 50 candidates
    )
# Stage 2: Rerank (pick the best)
    docs = [hit.payload["content"] for hit in candidates]
    reranked = co.rerank(
        model="rerank-english-v3.0",
        query=query,
        documents=docs,
        top_n=top_k,
    )
# Return reranked results with original metadata
    results = []
    for r in reranked.results:
        original = candidates[r.index]
        results.append({
            "content": original.payload["content"],
            "metadata": original.payload,
            "rerank_score": r.relevance_score,
            "vector_score": original.score,
        })
return results

๐Ÿ”ฌ

Practical Takeaways

Reranking is the biggest quick win for RAG quality โ€” 30-40% improvement in precision

Two-stage pipeline: bi-encoder retrieves 50-100 candidates, cross-encoder reranks top-K

Cross-encoders are more accurate but can't scale โ€” that's why you need both stages

Cohere Rerank is the easiest start โ€” API, no infrastructure, cheap

Self-host with bge-reranker if you need privacy or can't use external APIs

Retrieve more, rerank fewer โ€” get 50 candidates, rerank to top 5

๐Ÿ›ก๏ธ

What's Next?

Episode 22: RAG Pipeline End-to-End โ€” You've learned embeddings, vector search, chunking, hybrid search, and reranking. Now let's put it all together into a complete pipeline: ingestion โ†’ chunking โ†’ embedding โ†’ storage โ†’ retrieval โ†’ generation. The full picture.

โ† Previous

Ep 20: Hybrid Search

Next โ†’

Ep 22: RAG Pipeline End-to-End

Next: Episode 22 โ€” RAG Pipeline End-to-End

You've learned the pieces: embeddings, vector databases, chunking, hybrid search, reranking. Now let's build the whole machine.

This is part of a 98-episode series covering AI engineering from tokens to production deployment.

โ† Previous Ep 20: Hybrid Search: When Pure Vector Search Isn't Enough