Data as of Sep 9, 2026 · Based on 283 AI responses from ChatGPT Search and Google AI Mode · See how Parse measures this
To build a retrieval system that ignores basic cosine similarity, use a two-stage approach. First, perform an initial retrieval using keyword-based methods like BM25 to get candidates. Then, apply advanced re-ranking techniques such as Learning to Rank, cross-encoders like MiniLM-L-6-v2, or LLMs with Chain-of-Thought for more accurate, relevance-based ordering.
Brands AI recommends here
Named in 33% of answers
Named in 24% of answers
Named in 18% of answers
Named in 18% of answers
Building a modern retrieval system that goes beyond simple vector cosine similarity usually means implementing a **multi-stage retrieval architecture** (often called **Bi-Encoder + Cross-Encoder** or **Hybrid Search with Reranking**). Here is a clean, structured blueprint to build a high-performance,…
Building a modern retrieval system that goes beyond simple vector cosine similarity usually means implementing a multi-stage retrieval architecture (often called Bi-Encoder + Cross-Encoder or Hybrid Search with Reranking).
Here is a clean, structured blueprint to build a high-performance, non-cosine-reliant retrieval system:
Instead of relying on a single vector space, combine dense and sparse methods to fetch the top K candidates (e.g., K = 100 to 200).
Pass the top K candidates along with the user query into a Cross-Encoder model. Unlike bi-encoders (which embed the query and document separately), a cross-encoder processes the query and document together through a transformer attention mechanism. This captures deep, fine-grained semantic interactions at the cost of higher latency.
BAAI/bge-reranker-large or BAAI/bge-reranker-base (via Hugging Face BGE Reranker)Cohere Rerank API (via Cohere Rerank API ) for a managed, state-of-the-art cross-encoder solution.Jina Reranker (via Jina AI Reranker)If you are combining BM25 and vector scores before or alongside the reranker, avoid basic linear addition. Normalize the scores using:
To help tailor this implementation to your stack, let me know:
transformers and rank_bm25 ) or a managed API approach?No sources cited
If you want a retrieval system that **re-ranks results instead of just sorting by cosine similarity**, you typically want a **multi-stage retrieval architecture**. The first stage optimizes recall; the second stage uses richer query-document interactions to optimize relevance. A strong design looks like this: ```
If you want a retrieval system that re-ranks results instead of just sorting by cosine similarity, you typically want a multi-stage retrieval architecture. The first stage optimizes recall; the second stage uses richer query-document interactions to optimize relevance.
A strong design looks like this:
Query
|
v
Candidate Retrieval (fast)
----------------------------
- BM25 / sparse retrieval
- Dense retriever
- Hybrid retrieval
|
Top 100-1000 docs
|
v
Neural Re-ranking (accurate)
----------------------------
- Cross-encoder
- ColBERT / late interaction
- Learning-to-rank model
|
Top 10-50 docs
|
v
Final ranking / RAG context
Instead of:
score = cosine(query_embedding, document_embedding)
you compute:
score = Transformer(query + document)
The model sees both texts together and predicts relevance.
Example:
Query:
"How do I reset my company password?"
Candidate A:
"Password resets require an admin approval workflow."
Candidate B:
"Passwords are encrypted using bcrypt."
Cross-encoder:
A → 0.94
B → 0.31
A cross-encoder is usually much better because it can reason over the exact relationship between the query and document, but it is slower because every query-document pair requires inference. It is therefore normally used on a small candidate set (for example, the top 50–200 retrieved documents).
Common choices:
Instead of one vector per document:
document → [0.21, 0.55, ...]
store token-level vectors:
document →
[
token1 vector,
token2 vector,
token3 vector,
...
]
At query time, compare query tokens against document tokens and aggregate the best matches.
This avoids the information loss of a single embedding while being cheaper than a full cross-encoder. ColBERT's late-interaction approach uses token-level representations and a MaxSim scoring mechanism rather than a single embedding similarity.
Pipeline:
query
|
v
encode query tokens
document
|
v
retrieve token embeddings
score:
query token → best matching document token
sum scores
Use this when:
Combine many signals:
ranking_score =
0.35 * semantic_score
+ 0.20 * BM25_score
+ 0.15 * freshness
+ 0.10 * authority
+ 0.10 * click_probability
+ 0.10 * metadata_match
Train with:
This works especially well for production search systems where you have behavioral data.
I would build it like:
Use hybrid retrieval:
BM25 candidates: top 200
Dense embedding: top 200
Keyword filters: metadata constraints
Merge with Reciprocal Rank Fusion
For the merged ~300 candidates:
Cross-encoder(query, document)
|
v
relevance score
|
v
sort descending
Apply:
# Stage 1: retrieve
dense_results = vector_index.search(query, k=200)
bm25_results = bm25.search(query, k=200)
candidates = reciprocal_rank_fusion(
dense_results,
bm25_results
)
# Stage 2: rerank
pairs = [
(query, doc.text)
for doc in candidates[:100]
]
scores = reranker.predict(pairs)
ranked = sorted(
zip(candidates[:100], scores),
key=lambda x: x[1],
reverse=True
)
return ranked[:10]
| Corpus size | Recommended approach |
|---|---|
| <100k docs | BM25 + cross-encoder |
| 100k–10M docs | Hybrid retrieval + cross-encoder |
| 10M+ docs | Hybrid retrieval + ColBERT/late interaction + optional cross-encoder |
| Enterprise search | Hybrid + LTR + personalization |
For most RAG systems, the best upgrade from cosine similarity is:
Hybrid retrieval → candidate generation → cross-encoder reranking.
If latency is a concern, replace the cross-encoder stage with ColBERT-style late interaction.
Use hybrid retrieval:
BM25 candidates: top 200
Dense embedding: top 200
Keyword filters: metadata constraints
Merge with Reciprocal Rank Fusion
For the merged ~300 candidates:
Cross-encoder(query, document)
|
v
relevance score
|
v
sort descending
Apply:
Yes. I’d avoid making **cosine similarity the final ranking signal**. A strong modern design is a **multi-stage retrieval + learned reranking pipeline**: ``` Query
Yes. I’d avoid making cosine similarity the final ranking signal. A strong modern design is a multi-stage retrieval + learned reranking pipeline:
Query
│
├── BM25 / lexical retrieval ─────┐
│ │
├── Dense retrieval ──────────────┤
│ ▼
└── Metadata / filters ─────── Candidate pool
│
▼
Cross-encoder / reranker
│
▼
Top N results
Instead of comparing two independent vectors with cosine similarity, use a model that sees the query and candidate document together.
A cross-encoder/reranker effectively evaluates:
relevance(query, document)
rather than:
similarity(embedding(query), embedding(document))
This allows much finer-grained interactions between query terms and document content. Cohere's current Rerank models, for example, explicitly use cross-attention for this kind of fine-grained ranking. Cohere Cohere Documentation 3. Optional second reranking layer
For particularly important search, you can use:
BM25 + dense
↓
100 candidates
↓
neural cross-encoder
↓
20 candidates
↓
LLM / task-specific ranker
↓
final 5
The LLM stage should generally be restricted to a small candidate set because evaluating every document this way is expensive.
If latency matters and you don't want a conventional cross-encoder over every candidate, ColBERT is particularly interesting. It represents documents and queries as multiple contextualized token vectors and uses late interaction/MaxSim rather than reducing each document to one vector. The Stanford implementation is designed for scalable retrieval and reports retrieval in the tens-of-milliseconds range.
So you could have:
┌── BM25
Query ───────────┼── Dense retrieval
└── ColBERT
│
▼
Candidate fusion
│
▼
Cross-encoder
│
▼
Results
For a new system, I'd start with:
BM25 + dense retrieval → RRF → cross-encoder reranker → top 10
That gives you lexical matching, semantic recall, and a much stronger final relevance function without making cosine similarity your ranking mechanism. Reranking can be applied to either lexical or vector-retrieved candidates, and this two-stage pattern is explicitly supported by current reranking systems.
If you tell me your corpus size, document type, latency target, and whether this is RAG/search/recommendations, I can design the concrete architecture—including models, indexing strategy, scoring function, and example Python implementation.
To build a high-performance retrieval system that moves beyond basic cosine similarity, you should implement a **two-stage retrieval pipeline** . This industry-standard pattern maximizes efficiency by using a fast initial filter for broad recall, followed by a **Cross-Encoder re-ranker** or **Maximum Marginal…
To build a high-performance retrieval system that moves beyond basic cosine similarity, you should implement a two-stage retrieval pipeline . This industry-standard pattern maximizes efficiency by using a fast initial filter for broad recall, followed by a Cross-Encoder re-ranker or Maximum Marginal Relevance (MMR) to achieve precise, context-aware precision.
Here is how to design and build it.
🧱 Core Architecture: The Two-Stage Pipeline
Instead of relying on a single vector search, your architecture splits the workload into two phases to balance speed and accuracy.
[ User Query ]
│
▼
┌───────────┐
│ STAGE 1 │ ──► Hybrid Retrieval (BM25 Keyword Search + Dense Vector Search)
└───────────┘ Retrieves top 50–100 raw candidate documents
│
▼
┌───────────┐
│ STAGE 2 │ ──► Re-Ranking System (Cross-Encoder / LLM / MMR)
└───────────┘ Computes true deep query-document interactions
│
▼
[ Final Top K ] ──► Delivered to user or LLM generator
🛠️ Step 1: Upgrade the Initial Retrieval (Stage 1)
Stop using pure vector distance. Instead, employ Hybrid Retrieval , which combines semantic intent with exact keyword token matching.
🧠 Step 2: Implement Advanced Re-Ranking (Stage 2)
Once you narrow your database down to 50–100 candidate fragments, apply a re-ranking algorithm. Unlike cosine similarity (which evaluates a query and a document independently), a Cross-Encoder re-ranker processes the query and document together , capturing deep token-to-token relationships.
Option A: Deep Semantic Re-ranking (Cross-Encoders)
Production-grade open-source options like the BGE-Reranker or Cohere Rerank evaluate actual helpfulness rather than just geometric proximity.
python``` from sentence_transformers import CrossEncoder
reranker = CrossEncoder("BAAI/bge-reranker-large")
query = "How do I fix a timeout exception in production?"
candidates = [ "Production timeout exceptions happen when the gateway configuration limits are breached.", "To handle exceptions, use try-catch blocks in your python code structure.", "Network timeouts can be debugged by analyzing latency spikes in the system logs." ]
pairs = [[query, doc] for doc in candidates] scores = reranker.predict(pairs)
reranked_results = [doc for _, doc in sorted(zip(scores, candidates), reverse=True)] print(reranked_results)
Use code with caution.
Option B: Diversity Re-ranking via MMR
If your initial search retrieves redundant text fragments, use **Maximum Marginal Relevance (MMR)** . It explicitly penalizes similarity among the results themselves, ensuring your final list is both highly relevant *and* diverse.
MMR=arg max D i∈R∖S[λ⋅Sim 1(D i,Q)−(1−λ)⋅max D j∈S Sim 2(D i,D j)]MMR equals arg max over cap D sub i is an element of cap R ∖ cap S of open bracket lambda center dot Sim sub 1 open paren cap D sub i comma cap Q close paren minus open paren 1 minus lambda close paren center dot max over cap D sub j is an element of cap S of Sim sub 2 open paren cap D sub i comma cap D sub j close paren close bracket M M R=a r g m a x𝐷𝑖∈𝑅∖𝑆𝜆⋅S i m 1(𝐷𝑖,𝑄)−(1−𝜆)⋅m a x𝐷𝑗∈𝑆S i m 2(𝐷𝑖,𝐷𝑗)
Option C: LLM-as-a-Judge Re-ranking
For complex reasoning, feed the top 20 candidates into a small, fast LLM using a strict system prompt. Ask it to score each text segment on a scale of 1–5 based strictly on structural alignment with the user's intent.[](https://news.ycombinator.com/item?id=42704078) [[1]](https://news.ycombinator.com/item?id=42704078)
📊 Comparing Metrics
| Method | Computational Cost | Primary Benefit | Avoids Cosine Pitfalls? |
|---|---|---|---|
| **Hybrid Search (BM25 + Dense)** | Very Low | Catches exact keywords & intent | Partial (Vectors still use cosine) |
| **Cross-Encoder Model** | Medium | Captures full semantic context | **Yes** (Uses multi-layer attention) |
| **MMR Filtering** | Low | Eliminates repetitive text blocks | **Yes** (Penalizes redundancy) |
| **LLM Evaluation** | High | Understands logical reasoning | **Yes** (Uses deep contextual logic) |
Would you like help choosing the right **open-source model** for your specific data type, or do you want to explore setting up an **RRF blend** function?
To build a robust retrieval system that goes beyond basic cosine similarity, you need a **two-stage architecture (Retrieval + Re-ranking)** or a **learned sparse/dense late-interaction** approach . This allows you to scale to millions of documents quickly while applying deep semantic/cross-attention models for…
To build a robust retrieval system that goes beyond basic cosine similarity, you need a two-stage architecture (Retrieval + Re-ranking) or a learned sparse/dense late-interaction approach . This allows you to scale to millions of documents quickly while applying deep semantic/cross-attention models for precision.
Key Architecture Components
Popular Non-Cosine Re-Ranking Strategies & Frameworks
Recommended Tooling & Libraries
cross-encoder/ms-marco-MiniLM-L-6-v2 ) for zero-shot or fine-tuned re-ranking.Implementation Blueprint (Two-Stage Pipeline)
To help tailor this design, tell me:
No sources cited
Yes. I’d build it as a **multi-stage retrieval + learned reranking system**, rather than treating cosine similarity as the final relevance function. ## Recommended architecture ```
Yes. I’d build it as a multi-stage retrieval + learned reranking system, rather than treating cosine similarity as the final relevance function.
┌── BM25 / lexical retrieval ──┐
Query ───────────┤ ├──► candidate pool
├── Dense retrieval ────────────┤ │
└── Optional metadata filters ──┘ ▼
Learned reranker
│
▼
final ranking
Use multiple retrieval signals rather than one embedding similarity:
This is useful because lexical and semantic retrieval have complementary failure modes. Sentence Transformers' own retrieval documentation describes hybrid sparse+dense retrieval followed by reranking as a strong architecture.
For example:
BM25: top 100
Dense: top 100
Sparse NN: top 100
↓ RRF
~100–200 unique candidates
This is the key piece if you specifically don't want cosine similarity deciding the final ordering.
A Cross-Encoder receives the query and candidate document together:
[query, document] → Transformer → relevance score
Because the model jointly attends to the query and document, it can recognize things that a single embedding comparison misses—negation, specific requirements, relationships between entities, and whether the document actually answers the query.
The standard retrieve-then-rerank pattern is exactly this: retrieve perhaps 100 candidates cheaply, then run the more expensive Cross-Encoder over those candidates.
For example:
from sentence_transformers import CrossEncoder
reranker = CrossEncoder(
"cross-encoder/ms-marco-MiniLM-L6-v2"
)
scores = reranker.predict([
(query, document1),
(query, document2),
(query, document3),
])
ranking = sorted(
zip(documents, scores),
key=lambda x: x[1],
reverse=True
)
The Sentence Transformers documentation provides this exact style of Cross-Encoder ranking, and pretrained MS MARCO rerankers are readily available.
I'd seriously consider ColBERT-style late interaction.
Instead of:
query embedding
↓
one vector
↓
document embedding
↓
one similarity
you retain token-level representations:
Query tokens Document tokens
│ │
└───── MaxSim ──────┘
↓
relevance score
ColBERT independently encodes queries and documents, then performs a fine-grained late interaction between their token representations. This gives you much more expressive matching than a single cosine/dot-product score while still allowing document representations to be precomputed.
So I'd choose between:
| Requirement | Best approach |
|---|---|
| Highest quality, moderate scale | Dense/BM25 → Cross-Encoder |
| Very large corpus | BM25 + dense → ColBERT/late interaction |
| Maximum quality | Hybrid retrieval → ColBERT → Cross-Encoder |
| Domain-specific search | Hybrid retrieval → fine-tuned Cross-Encoder |
Don't rely on an off-the-shelf reranker indefinitely. If you have query/document interaction data, train the reranker on your actual relevance judgments.
For example:
query document label
────────────────────────────────────────────────────────
"reset my MFA device" MFA reset guide 1
"reset my MFA device" password reset guide 0
"reset my MFA device" account creation guide 0
Then train the reranker to distinguish your relevant documents from hard negatives. Fine-tuning is particularly important because a generic reranker can otherwise optimize for a different notion of relevance than your application.
I'd evaluate the whole pipeline with NDCG@k, MRR@k, Recall@k, rather than just measuring embedding similarity. Sentence Transformers provides a reranking evaluator supporting MRR, NDCG, and MAP.
If I were implementing this today, I'd start with:
┌── BM25 ──────────────┐
│ │
Query ──────────────┼── Dense retriever ───┼──► RRF
│ │
└── Sparse retriever ──┘
│
top 100
│
▼
Cross-Encoder
│
top 10
│
▼
final results
That gives you cosine similarity only as one candidate-generation signal—not as your definition of relevance. The final ordering is determined by a model that sees the query and document together.
This is useful because lexical and semantic retrieval have complementary failure modes. Sentence Transformers' own retrieval documentation describes hybrid sparse+dense retrieval followed by reranking as a strong architecture.
For example:
Yes. If your goal is **retrieval + learned re-ranking without treating cosine similarity as the final relevance function**, I’d use a multi-stage architecture. ## Recommended architecture ```
Yes. If your goal is retrieval + learned re-ranking without treating cosine similarity as the final relevance function, I’d use a multi-stage architecture.
┌── BM25 / sparse retrieval ──┐
Query ──────────────┼── Dense retrieval ──────────┼── Candidate fusion
└── ColBERT / multi-vector ───┘
│
top 100–300
│
▼
Cross-encoder reranker
│
top 10–30
│
▼
final results
Use multiple retrieval signals rather than relying on one embedding distance:
Then combine their candidate lists with something like Reciprocal Rank Fusion (RRF).
This gives you robustness: a document doesn't have to be highly ranked by one retrieval mechanism to make it into the candidate pool.
For the top ~100 candidates, use a cross-encoder:
score = Reranker(query, document)
Unlike cosine similarity,
cosine(embed(query), embed(document))
the cross-encoder sees the query and document together and can model interactions between their tokens. That's why it is generally much more expressive as a relevance function, although it is considerably more expensive.
For example:
candidates = retrieve_hybrid(query, k=200)
scores = reranker.predict([
(query, doc.text)
for doc in candidates
])
results = sorted(
zip(candidates, scores),
key=lambda x: x[1],
reverse=True
)[:20]
The important point is that cosine similarity is only being used, if at all, to generate candidates—not to determine the final ranking.
If you want to move substantially beyond the conventional:
embedding → cosine → cross-encoder pipeline, I'd seriously consider ColBERTv2.
It keeps multiple contextual vectors per document instead of one pooled vector. At query time, it performs token-level MaxSim interactions between query and document representations.
Conceptually:
query tokens: q1 q2 q3 q4
│ │ │ │
▼ ▼ ▼ ▼
document tokens: d1 d2 d3 ... dn
score =
max(sim(q1, d1...dn))
+ max(sim(q2, d1...dn))
+ max(sim(q3, d1...dn))
+ max(sim(q4, d1...dn))
So you get fine-grained query/document matching without running a full transformer jointly over every candidate. ColBERTv2 also introduced compression techniques specifically to make the multi-vector representation substantially more practical.
For a production RAG/search system, my default would be:
| Stage | Method | Candidates |
|---|---|---|
| Recall | BM25 | 100 |
| Recall | Dense embedding | 100 |
| Recall | ColBERTv2 | 100 |
| Fusion | RRF | ~200 |
| Rerank | Cross-encoder | 50 |
| Final | learned score / reranker | 10–20 |
You can also eliminate the cross-encoder and use ColBERT as the final ranking stage if latency is important. Late interaction occupies a useful middle ground between inexpensive single-vector retrieval and expensive cross-encoder ranking.
Don't assume the architecture is better merely because it is more sophisticated. Evaluate it on your actual queries and relevance judgments using metrics such as:
In particular, if the first-stage retriever has poor Recall@100, a reranker cannot recover documents it never receives.
If you tell me your corpus size, document type, latency target, and whether this is RAG/search/recommendation, I can propose a concrete model + indexing + reranking architecture.
Unlike cosine similarity,
cosine(embed(query), embed(document))
the cross-encoder sees the query and document together and can model interactions between their tokens. That's why it is generally much more expressive as a relevance function, although it is considerably more expensive.
For example:
candidates = retrieve_hybrid(query, k=200)
scores = reranker.predict([
(query, doc.text)
for doc in candidates
])
results = sorted(
zip(candidates, scores),
key=lambda x: x[1],
reverse=True
)[:20]
The important point is that cosine similarity is only being used, if at all, to generate candidates—not to determine the final ranking.
If you want to move substantially beyond the conventional:
embedding → cosine → cross-encoder pipeline, I'd seriously consider ColBERTv2.
It keeps multiple contextual vectors per document instead of one pooled vector. At query time, it performs token-level MaxSim interactions between query and document representations.
Conceptually:
query tokens: q1 q2 q3 q4
│ │ │ │
▼ ▼ ▼ ▼
document tokens: d1 d2 d3 ... dn
score =
max(sim(q1, d1...dn))
+ max(sim(q2, d1...dn))
+ max(sim(q3, d1...dn))
+ max(sim(q4, d1...dn))
So you get fine-grained query/document matching without running a full transformer jointly over every candidate. ColBERTv2 also introduced compression techniques specifically to make the multi-vector representation substantially more practical.
For a production RAG/search system, my default would be:
| Stage | Method | Candidates |
|---|---|---|
| Recall | BM25 | 100 |
| Recall | Dense embedding | 100 |
| Recall | ColBERTv2 | 100 |
| Fusion | RRF | ~200 |
| Rerank | Cross-encoder | 50 |
| Final | learned score / reranker | 10–20 |
You can also eliminate the cross-encoder and use ColBERT as the final ranking stage if latency is important. Late interaction occupies a useful middle ground between inexpensive single-vector retrieval and expensive cross-encoder ranking.
Don't assume the architecture is better merely because it is more sophisticated. Evaluate it on your actual queries and relevance judgments using metrics such as:
To build a modern retrieval system that goes beyond basic vector cosine similarity, you need a **two-stage architecture (Retrieval + Re-ranking)** or an **alternative scoring paradigm** . Basic cosine similarity on dense embeddings is fast, but it often misses exact keyword matches, complex logical constraints, and…
To build a modern retrieval system that goes beyond basic vector cosine similarity, you need a two-stage architecture (Retrieval + Re-ranking) or an alternative scoring paradigm . Basic cosine similarity on dense embeddings is fast, but it often misses exact keyword matches, complex logical constraints, and deep semantic interactions between the query and the document.[1][2][3]
Here is a structured breakdown of how to build a state-of-the-art re-ranking retrieval system:
Phase 1: Advanced First-Stage Retrieval (Beyond Pure Dense Vectors)
Instead of relying solely on a single dense embedding space, combine multiple signals:
Phase 2: High-Performance Re-ranking Models
The core of your re-ranking requirement should be handled by a Cross-Encoder architecture. Unlike bi-encoders (which embed the query and document separately), a cross-encoder processes the query and document together in a transformer network, allowing full attention across every query-document token pair.[1][2][3][4][5]
bge-reranker-large ) that you can self-host and run locally or on your own infrastructure. Look into the project via the FlagEmbedding GitHub Repository.[1][2][3][4]Phase 3: Architecture Blueprint
To implement this in code (e.g., using Python and LangChain, LlamaIndex, or raw PyTorch):
To help tailor this architecture, tell me:
No sources cited
Yes. A strong design is a **multi-stage hybrid retrieval + learned reranking system**, rather than treating cosine similarity as the final relevance score. ## Recommended architecture ```
Yes. A strong design is a multi-stage hybrid retrieval + learned reranking system, rather than treating cosine similarity as the final relevance score.
Query
│
├── BM25 / lexical retrieval ───────┐
│ │
├── Dense vector retrieval ────────┤
│ ▼
└── Sparse/semantic retrieval ──► Candidate fusion
│
▼
Top 50–200 candidates
│
▼
Cross-Encoder
re-ranker
│
▼
Top 5–20
The key idea is that cosine similarity is only one candidate-generation signal, not the final ranking function. A cross-encoder sees the query and document together and can model interactions between them, which generally gives better ranking quality than independently embedded documents.
I'd use at least:
You can then combine the candidate lists with Reciprocal Rank Fusion (RRF) rather than trying to make BM25 and embedding scores directly comparable. Hybrid retrieval plus reranking is a well-established pattern.
For example:
BM25 → top 100
Dense → top 100
SPLADE → top 100
↓
RRF fusion
↓
top 100–200
Instead of:
score = cosine(query_embedding, document_embedding)
use:
score = reranker.predict([
[query, document]
])
The reranker jointly processes the query and candidate document, so it can recognize things such as:
Query:
"How do I rotate an AWS access key?"
Document A:
"To rotate AWS access keys, create a new key, update applications,
then deactivate the old key."
Document B:
"AWS access keys are credentials used to authenticate API requests."
A cosine-based retriever may consider both highly relevant. A cross-encoder can determine that A actually answers the question.
Sentence Transformers explicitly recommends this retrieve-then-rerank architecture because cross-encoders are more accurate but too expensive to run over an entire corpus.
This is probably the biggest improvement if you have your own search data.
Train the reranker on:
(query, relevant_document, irrelevant_document)
rather than relying entirely on a generic MS MARCO model.
For example:
Query: "reset postgres password"
Positive: "To reset the PostgreSQL password for role..."
Hard negative: "PostgreSQL authentication configuration..."
Hard negative: "Changing the database user's username..."
The hard negatives are particularly important. Random negatives are usually too easy; you want documents that your first-stage retriever thinks are plausible but that users would actually reject.
Fine-tuning a cross-encoder on domain-specific data can improve its usefulness substantially, and Sentence Transformers provides tooling specifically for reranker training and evaluation.
For a production system, I'd potentially use a final learning-to-rank layer:
┌─ BM25 score
├─ Dense rank
Query + candidates ──────┼─ RRF score
├─ Cross-encoder score
├─ freshness
├─ authority
├─ document type
└─ business/domain features
│
▼
Learning-to-Rank model
│
▼
final rank
For example, a LambdaMART/XGBoost ranker can learn:
final_score =
f(
bm25_score,
dense_rank,
rrf_score,
reranker_score,
freshness,
authority,
metadata,
...
)
This is preferable to manually inventing something like:
0.4 * cosine + 0.3 * bm25 + 0.3 * reranker
because those scores aren't necessarily calibrated to one another.
If you're building this from scratch, I'd start with:
BM25 ────────────────┐
│
Dense embeddings ────┼──► RRF ─► top 100
│ │
Sparse retrieval ────┘ ▼
Cross-Encoder
│
▼
top 10
Then evaluate Recall@100 before reranking and NDCG@10 / MRR@10 after reranking. The latter metrics specifically measure whether your most relevant documents end up near the top.
For a first implementation, something like cross-encoder/ms-marco-MiniLM-L6-v2 is a reasonable lightweight baseline; Sentence Transformers reports substantially different speed/quality tradeoffs across its pretrained rerankers.
If you tell me what you're retrieving (documents, code, products, support tickets, research papers, etc.) and roughly how many documents you have, I can propose a concrete architecture—including models, indexing strategy, candidate counts, reranking model, and evaluation setup.
To build a modern retrieval system that goes beyond basic cosine similarity, you need to transition from single-vector dense retrieval to multi-stage architectures, hybrid search, and cross-encoder re-ranking. Here is a structured blueprint for a state-of-the-art re-ranking retrieval system: 1. Architectural Blueprint…
To build a modern retrieval system that goes beyond basic cosine similarity, you need to transition from single-vector dense retrieval to multi-stage architectures, hybrid search, and cross-encoder re-ranking.
Here is a structured blueprint for a state-of-the-art re-ranking retrieval system:
Bge-Reranker or Cohere-rerank open models).To help tailor the specific code or architecture for you, tell me: