TL;DR
-
Retrieval evaluation measures whether the right documents are found - before the LLM ever generates an answer.
-
Recall@k is the most important metric - is the correct document in the top-k results? If not, no LLM can save you.
-
Build a golden test set of 50–200 query-document pairs from your actual corpus and expected queries.
-
Evaluate retrieval separately from generation - conflating them makes it impossible to diagnose which stage failed.
-
Run evals in CI - block deploys that regress retrieval metrics. Catch quality drops before users do.
Why This Matters
Most RAG quality problems are retrieval problems. When a user gets a wrong answer, the failure usually falls into one of three stages:
- The correct document wasn't retrieved (retrieval failure — often the dominant source of incorrect answers)
- The correct document was retrieved but ranked too low (ranking failure)
- The LLM ignored or misinterpreted the context (generation failure)
Generation quality cannot exceed retrieval quality — if the right document never reaches the LLM, no prompt or model upgrade can fully compensate.
If you only evaluate end-to-end answer quality, you can't distinguish between these. You end up prompt-engineering a generation problem that's actually a retrieval problem - or switching embedding models when chunking is the real issue.
Retrieval evaluation isolates the search step, gives you actionable metrics, and lets you iterate on chunking, embedding models, hybrid search, and reranking with clear feedback. It's the difference between guessing and engineering.
The Problem Retrieval Evaluation Solves
RAG pipelines have many configurable components - chunk size, embedding model, search method, reranker, filters. Each change can help or hurt retrieval quality. Without measurement:
- You don't know if a change improved or degraded retrieval
- You can't compare embedding models objectively on your data
- You can't set quality gates in CI/CD
- You can't diagnose user-reported failures systematically
- You optimize the wrong component (prompts instead of chunking)
Retrieval evaluation provides a quantitative feedback loop for one of the highest-leverage stages in the pipeline. A 5% recall@5 improvement translates directly to fewer wrong answers in deployed systems.
How We Got Here
Search quality measurement existed long before RAG. TREC benchmarks, MS MARCO, and BEIR established IR metrics that RAG teams now reuse for embedding retrieval.
Diagram: Evolution of retrieval evaluation
timeline
title Retrieval evaluation
1990s : TREC ad-hoc benchmarks
2016 : MS MARCO passage ranking
2020 : DPR + RAG paper metrics
2022 : BEIR zero-shot eval
2023 : RAGAS automated RAG eval
2024 : CI gates on recall@k in prod teams
Production RAG teams adapted classical IR metrics (recall, MRR, nDCG) because retrieval failure — not generation — causes most wrong answers.
| Era | Focus | Lesson for RAG |
|---|---|---|
| TREC / BEIR | Benchmark corpora | Domain transfer matters — eval on your data |
| MS MARCO | Cross-encoder labels | Reranker gains measurable with nDCG |
| RAG era | End-to-end faithfulness | Still need isolated retrieval eval |
| Ops era | CI regression gates | Block deploys that drop recall@k |
Use Retrieval Evaluation before RAG Evaluation for end-to-end faithfulness. Tune ANN indexes and re-ranking against the same golden set.
What Is Retrieval Evaluation?
Retrieval evaluation measures how well a search system finds relevant documents for a given query. You provide:
- A test set of queries with known relevant documents (ground truth)
- A retrieval system to evaluate
- Metrics that quantify search quality
# Minimal retrieval eval
test_cases = [
{"query": "refund policy for premium members", "relevant_doc_ids": ["doc_042"]},
{"query": "API rate limit configuration", "relevant_doc_ids": ["doc_118", "doc_203"]},
{"query": "SSO setup with Okta", "relevant_doc_ids": ["doc_087"]},
]
for case in test_cases:
results = retriever.search(case["query"], top_k=10)
retrieved_ids = [r.id for r in results]
hit = any(doc_id in retrieved_ids for doc_id in case["relevant_doc_ids"])
print(f"Query: {case['query']} → {'HIT' if hit else 'MISS'}")
The output tells you whether retrieval works for each query - independent of the LLM.
How Retrieval Evaluation Works
Core Metrics
| Metric | What It Measures | Formula Intuition | When to Use |
|---|---|---|---|
| Recall@k | Is the relevant doc in top-k? | Hits / total queries | Primary metric - did we find it? |
| Precision@k | What fraction of top-k is relevant? | Relevant in top-k / k | When false positives matter |
| MRR | How high is the first relevant result ranked? | Average of 1/rank of first hit | When rank position matters |
| nDCG@k | Are relevant docs ranked higher than irrelevant? | Discounted cumulative gain | When multiple relevant docs exist |
| Hit Rate | Same as recall@k for single relevant doc | Binary: found or not | Simplest metric to start |
Recall@k - The Primary Metric
Recall@k = (number of queries where relevant doc appears in top-k) / (total queries)
If recall@5 = 0.80, the correct document appears in the top 5 results for 80% of test queries.
def recall_at_k(retrieved_ids: list[str], relevant_ids: list[str], k: int) -> float:
top_k = set(retrieved_ids[:k])
return 1.0 if top_k & set(relevant_ids) else 0.0
def mean_recall_at_k(all_results: list[tuple], k: int) -> float:
scores = [recall_at_k(retrieved, relevant, k) for retrieved, relevant in all_results]
return sum(scores) / len(scores)
Target benchmarks:
- recall@5 < 0.60 - retrieval is broken, fix before deploying
- recall@5 0.60–0.80 - acceptable, optimize chunking and search
- recall@5 0.80–0.90 - good, focus on reranking and generation
- recall@5 > 0.90 - excellent, diminishing returns on retrieval
MRR (Mean Reciprocal Rank)
MRR = average of 1/rank_of_first_relevant_document
If the first relevant document is at rank 1, score = 1.0. At rank 3, score = 0.33. MRR rewards getting the right document to the top.
def reciprocal_rank(retrieved_ids: list[str], relevant_ids: list[str]) -> float:
for rank, doc_id in enumerate(retrieved_ids, 1):
if doc_id in relevant_ids:
return 1.0 / rank
return 0.0
Important when you pass only top-1 or top-3 to the LLM - rank matters, not just presence.
nDCG@k (Normalized Discounted Cumulative Gain)
Accounts for multiple relevant documents and their rank positions. Relevant documents ranked higher score better. Uses graded relevance (highly relevant = 3, partially relevant = 1, irrelevant = 0).
import math
def dcg_at_k(relevances: list[int], k: int) -> float:
relevances = relevances[:k]
return sum(rel / math.log2(i + 2) for i, rel in enumerate(relevances))
def ndcg_at_k(retrieved_ids: list[str], relevance_map: dict[str, int], k: int) -> float:
relevances = [relevance_map.get(doc_id, 0) for doc_id in retrieved_ids[:k]]
dcg = dcg_at_k(relevances, k)
ideal = dcg_at_k(sorted(relevance_map.values(), reverse=True), k)
return dcg / ideal if ideal > 0 else 0.0
Use nDCG when queries have multiple relevant documents with different relevance grades.
Diagram: Metric selection by scenario
flowchart TD
A[Eval goal?] -->|Found at all?| B[Recall@k]
A -->|First hit rank?| C[MRR]
A -->|Multiple relevant docs?| D[nDCG@k]
A -->|After reranker?| E[Precision@3]
B --> F[Primary RAG gate]
E --> G[Reranker A/B]
Recall@k is the primary gate; MRR and nDCG add rank-quality signal; precision@3 validates rerankers.
Architecture
A retrieval evaluation pipeline has four components:
| Component | Purpose | Implementation |
|---|---|---|
| Golden test set | Ground truth query-document pairs | JSON/CSV file, 50–200 cases |
| Retriever under test | The search pipeline to evaluate | Your RAG retrieval code |
| Metrics calculator | Computes recall, MRR, nDCG | Custom script or RAGAS |
| Reporting | Tracks metrics over time | Dashboard, CI output, logs |
The retriever scores document chunks against the query embedding and returns the top-k passages that ground the generator.

Source: Meta AI
Run evals on every change to chunking, embedding model, search config, reranker, or query transformation. Block deploys that drop recall@5 by more than 2%.
Diagram: Eval pipeline architecture
flowchart TB
GS[Golden test set] --> EV[Evaluator]
RET[Retriever under test] --> EV
EV --> M[Metrics recall MRR nDCG]
M --> CI[CI gate]
M --> DASH[Dashboard]
CI -->|fail| BLOCK[Block deploy]
CI -->|pass| SHIP[Ship change]
Offline eval runs on every retrieval PR; production logs feed back into the golden set.
Diagram: Eval request flow
sequenceDiagram
participant CI as CI pipeline
participant E as Evaluator
participant R as Retriever
participant M as Metrics
CI->>E: run eval suite
loop Each test case
E->>R: search query
R-->>E: top-k doc IDs
E->>M: compute recall MRR nDCG
end
M-->>CI: pass or fail
Each query in the golden set produces metric contributions; use exact search when ANN non-determinism matters.
Step-by-Step Flow
Step 1: Build a golden test set. Collect 50–200 real queries and identify the correct source document(s) for each. Sources: user query logs, support tickets, manually written test cases.
Step 2: Define relevance criteria. Binary (relevant/not) for recall@k. Graded (0–3) for nDCG. Document what makes a document "relevant" for each query.
Step 3: Run baseline evaluation. Measure recall@5, MRR, and nDCG@5 on your current retrieval pipeline. This is your benchmark.
Step 4: Iterate and compare. Change one component at a time (chunk size, embedding model, hybrid search, reranker). Re-run eval. Keep changes that improve metrics.
Step 5: Automate in CI. Run eval on every PR that touches retrieval code. Fail if recall@5 drops below threshold.
Step 6: Monitor in production. Sample live queries, log retrieved documents, and periodically audit retrieval quality with human review.
Step 7: Expand the test set. Add failure cases from production. Every user-reported wrong answer becomes a new test case.
Real Production Example
An eval pipeline that runs in CI and blocks regressions:
import json
from dataclasses import dataclass
@dataclass
class EvalCase:
query: str
relevant_doc_ids: list[str]
relevance_grades: dict[str, int] = None # for nDCG
@dataclass
class EvalResult:
recall_at_5: float
recall_at_10: float
mrr: float
ndcg_at_5: float
failures: list[dict]
class RetrievalEvaluator:
def __init__(self, retriever, test_set: list[EvalCase]):
self.retriever = retriever
self.test_set = test_set
def evaluate(self, k_values: list[int] = [5, 10]) -> EvalResult:
recalls = {k: [] for k in k_values}
mrrs = []
ndcgs = []
failures = []
for case in self.test_set:
results = self.retriever.search(case.query, top_k=max(k_values))
retrieved_ids = [r.id for r in results]
for k in k_values:
hit = any(d in retrieved_ids[:k] for d in case.relevant_doc_ids)
recalls[k].append(1.0 if hit else 0.0)
rr = 0.0
for rank, doc_id in enumerate(retrieved_ids, 1):
if doc_id in case.relevant_doc_ids:
rr = 1.0 / rank
break
mrrs.append(rr)
if case.relevance_grades:
ndcg = self._ndcg(retrieved_ids, case.relevance_grades, k=5)
ndcgs.append(ndcg)
if not any(d in retrieved_ids[:5] for d in case.relevant_doc_ids):
failures.append({
"query": case.query,
"expected": case.relevant_doc_ids,
"got": retrieved_ids[:5],
})
return EvalResult(
recall_at_5=sum(recalls[5]) / len(recalls[5]),
recall_at_10=sum(recalls[10]) / len(recalls[10]),
mrr=sum(mrrs) / len(mrrs),
ndcg_at_5=sum(ndcgs) / len(ndcgs) if ndcgs else 0.0,
failures=failures,
)
def _ndcg(self, retrieved_ids, relevance_map, k):
import math
rels = [relevance_map.get(d, 0) for d in retrieved_ids[:k]]
dcg = sum(r / math.log2(i + 2) for i, r in enumerate(rels))
ideal = sorted(relevance_map.values(), reverse=True)[:k]
idcg = sum(r / math.log2(i + 2) for i, r in enumerate(ideal))
return dcg / idcg if idcg > 0 else 0.0
def assert_quality(self, min_recall_at_5: float = 0.75):
result = self.evaluate()
print(f"Recall@5: {result.recall_at_5:.3f}")
print(f"Recall@10: {result.recall_at_10:.3f}")
print(f"MRR: {result.mrr:.3f}")
print(f"nDCG@5: {result.ndcg_at_5:.3f}")
print(f"Failures: {len(result.failures)}/{len(self.test_set)}")
if result.failures:
print("\nFailed queries:")
for f in result.failures[:5]:
print(f" Q: {f['query']}")
print(f" Expected: {f['expected']}")
print(f" Got: {f['got']}")
assert result.recall_at_5 >= min_recall_at_5, (
f"Recall@5 {result.recall_at_5:.3f} below threshold {min_recall_at_5}"
)
# CI usage
test_set = [EvalCase(**c) for c in json.load(open("golden_test_set.json"))]
evaluator = RetrievalEvaluator(retriever, test_set)
evaluator.assert_quality(min_recall_at_5=0.75)
Design Decisions
| Decision | Option A | Option B | When to choose |
|---|---|---|---|
| Test set size | 50 cases | 200+ cases | 50 for CI speed; 200+ for comprehensive coverage |
| Primary metric | Recall@k | nDCG@k | Recall@k for single-answer Q&A; nDCG when multiple relevant docs |
| k value | 5 | 10 | Match k to what you pass to the LLM; also measure @10 for reranking headroom |
| Eval frequency | Every PR | Weekly | Every PR for retrieval changes; weekly for monitoring |
| Relevance labels | Binary | Graded (0–3) | Binary to start; graded when queries have multiple relevant docs |
| Synthetic vs real | Real user queries | Synthetic | Real queries always; supplement with synthetic for edge cases |
Common patterns
| Pattern | Description | | ----------------------- | ---------------------------------- | ---------------------------------------- | | Golden set CI gate | Block if recall@5 drops >2% | Standard production discipline | | Component isolation | Change one knob at a time | Attribute improvements correctly | | Failure case mining | Add prod misses to test set | Continuous improvement loop | | Exact vs ANN eval | Exact search for eval consistency | ANN tuning uses recall vs exact baseline | | Shadow comparison | Run new index beside old on sample | Safe rollout for index changes |
Comparisons
Recall@k vs MRR vs nDCG vs precision@k
| Metric | Question answered | Best for |
|---|---|---|
| Recall@k | Is relevant doc in top-k? | Primary RAG gate |
| MRR | How high is first relevant hit? | Top-1 / top-3 to LLM |
| nDCG@k | Are multiple relevant docs ranked well? | Multi-doc answers |
| Precision@k | What fraction of top-k is relevant? | After reranking |
Retrieval eval vs end-to-end RAG eval
| Layer | Measures | Tooling |
|---|---|---|
| Retrieval only | recall@k, MRR, nDCG | Custom script, ranx, Pyserini |
| Generation only | faithfulness, correctness | RAGAS, DeepEval |
| End-to-end | user-facing answer quality | Human review + LLM judge |
Decision tree: which metric to optimize
Decision tree: retrieval metrics
flowchart TD
A[Single correct doc per query?] -->|Yes| B[Recall@5 primary]
A -->|No| C[nDCG@5 primary]
B --> D{Pass to LLM top-3?}
D -->|Yes| E[Also track MRR]
D -->|No| F[Recall@10 for rerank headroom]
C --> G[Graded relevance labels]
E --> H[Add precision@3 after rerank]
Start with recall@5; add MRR when rank position matters; add nDCG for multi-relevant queries.
Compare retrieval stacks via Best Vector Databases: Milvus vs Qdrant · Qdrant vs Pinecone.
Common Mistakes
-
Only evaluating end-to-end answers. Conflates retrieval and generation failures. Evaluate retrieval independently first.
-
Test set too small. Five queries isn't evaluation - it's anecdote. Minimum 50 cases for statistical confidence.
-
Test set not representative. Only testing easy queries inflates metrics. Include hard cases: paraphrases, ambiguous queries, multi-document answers.
-
Not updating the test set. A static test set goes stale as the corpus changes. Add production failures as new test cases continuously.
-
Changing multiple variables at once. Switching embedding model AND chunk size AND adding hybrid search - you can't attribute the improvement. Change one thing at a time.
-
Ignoring failure analysis. A recall@5 of 0.82 tells you 18% fail. Reading the actual failed queries tells you WHY - and what to fix.
-
Not running evals in CI. Manual evals get skipped under deadline pressure. Automate or it won't happen.
Where It Breaks Down
Subjective relevance - "What is our culture?" may have many partially relevant documents. Binary relevance labels don't capture this well. Use graded relevance or LLM-as-judge with human calibration.
Evolving corpora - Documents are added, updated, and removed. A test case referencing a deleted document becomes invalid. Version your test set alongside your corpus.
Expensive ground truth - Labeling 200 query-document pairs requires domain expertise. Start with 50 high-confidence cases. Expand incrementally from production failure logs.
Non-deterministic retrieval - Approximate nearest neighbor search can return slightly different results across runs. Run evals multiple times or use exact search for evaluation consistency.
Retrieval eval doesn't measure generation. High recall@5 with a bad LLM still produces bad answers. Evaluate both stages - see RAG Evaluation for end-to-end metrics.
When NOT to Use Automated Retrieval Eval Alone
Skip relying only on automated retrieval metrics when:
- Relevance is subjective — culture, policy interpretation; supplement with human labels or calibrated LLM judges.
- Corpus changes daily without versioned test sets — stale ground truth produces false failures; version tests with corpus snapshots.
- You have fewer than 30 labeled queries — metrics are noise; expand the golden set before gating CI.
- You only measure end-to-end answer quality — cannot diagnose retrieval vs generation; add isolated retrieval eval.
- ANN non-determinism is high — run multiple seeds or use exact search for eval consistency.
Always pair automated metrics with periodic human audit of failed queries.
Running in Production
Best Practice
✅ Best Practices - Instrument every stage, version embedding models, enforce access control at retrieval time, and evaluate on a fixed golden set before shipping changes.
| Dimension | Consideration |
|---|---|
| Scaling | Eval is offline - run on a test set, not live traffic. 200 test cases evaluate in 1–5 minutes depending on retrieval latency. |
| Latency | Not on the query path. Run in CI (minutes) or as a scheduled job (hourly/weekly). |
| Cost | Embedding costs for 200 queries: negligible (~$0.001). LLM-based eval (RAGAS): ~$1–5 per run. |
| Monitoring | Track recall@5 over time in a dashboard. Alert on drops > 2%. Log production retrieval results for spot-checking. |
| Evaluation | Meta-evaluation: is your test set still representative? Review quarterly. Add production failures as new cases. TREC, BEIR, and other public benchmarks are useful for comparison but should not replace evaluation on your corpus and query distribution. |
| Security | Test set may contain sensitive queries. Store securely. Include access control test cases (cross-tenant queries). |
Important
Evaluate retrieval before generation. If recall@5 is below 0.70, fix retrieval first - no amount of prompt engineering or LLM upgrading will compensate.
Related Guides
Concept guides
- RAG: The pipeline being evaluated.
- Re-ranking: Evaluate precision@3 after adding reranker.
- Chunking Strategies: First iteration when recall is low.
- Embedding Models: Compare models on same golden set.
- Hybrid Search: A/B test with and without hybrid.
- ANN Indexes: Tune efSearch/nprobe against recall@k.
- Vector Quantization: Gate compression on recall regression.
- Metadata Filtering: Include ACL test cases.
- RAG Evaluation: End-to-end generation metrics.
Rankings
Comparisons
Tools
- LangChain · LlamaIndex · Qdrant · Cohere
If you understood this topic, read next:
Diagram: Evaluation learning path
flowchart LR
A[RAG pipeline] --> B[Retrieval Eval]
B --> C[Reranking]
C --> D[RAG Eval]
D --> E[Production]
Prerequisites: RAG · Embedding Models
Next topics: Re-ranking · Chunking Strategies · RAG Evaluation
Interview Questions
-
Why evaluate retrieval separately from generation?
- Expected: isolate failure stage; retrieval failures are often the dominant source — prompt tuning won't fix missing docs.
-
What is recall@k and why is it primary for RAG?
- Expected: fraction of queries where relevant doc in top-k; LLM cannot use doc not retrieved.
-
Recall@5 vs MRR — when use each?
- Expected: recall for presence; MRR when only top-1/top-3 passed to LLM and rank matters.
-
How do you build a golden test set?
- Expected: real queries + expert-labeled relevant doc IDs; 50–200 cases; grow from prod failures.
-
How do you evaluate reranking?
- Expected: same test set; compare precision@3 and nDCG@5 with/without reranker.
-
What recall@5 threshold for production?
- Expected: 0.80+ target; below 0.60 broken; 0.60–0.80 iterate chunking/search.
-
How do CI gates work for retrieval?
- Expected: run eval on PR; fail if recall@5 drops >2% vs baseline.
-
How do you eval metadata filtering / tenant isolation?
- Expected: cross-tenant queries must return zero unauthorized docs in top-k.
Key Takeaways
- Retrieval evaluation measures whether the right documents are found — independent of LLM quality.
- Recall@5 is the primary metric; target 0.80+ for production.
- Build a golden test set of 50–200 real query-document pairs from your corpus.
- Change one retrieval component at a time and re-evaluate.
- Run evals in CI to block deploys that regress retrieval quality.
- Analyze failures individually — aggregate metrics tell you how much; failure cases tell you why.
FAQs
What is the most important retrieval metric?
Recall@k - specifically recall@5 for most RAG pipelines. It answers: "Is the correct document in the top 5 results?" If not, the LLM can't produce a correct answer.
How many test cases do I need?
Minimum 50 for basic confidence. 100–200 for deployed CI gates. Expand continuously from operational failure logs.
How do I build a golden test set?
Collect real user queries (or write representative ones). For each query, identify the correct source document(s) with a domain expert. Store as query → relevant_doc_ids pairs.
Should I evaluate retrieval and generation separately?
Yes. Always. Retrieval eval tells you if the right documents were found. Generation eval tells you if the LLM used them correctly. Combined eval hides which stage failed.
What recall@5 score is good enough?
0.80+ for production. Below 0.60 means retrieval is broken. Between 0.60–0.80, iterate on chunking, embeddings, and search before deploying.
How do I evaluate reranking?
Compare precision@3 and nDCG@5 with and without reranking on the same test set. The reranked pipeline should show higher precision at lower k values.
Can I use LLMs to evaluate retrieval?
Yes - RAGAS uses LLM-as-judge for context precision and recall. Useful for scale, but calibrate against human labels on a sample. LLM judges can be inconsistent.
How often should I run retrieval eval?
On every PR that touches retrieval code (CI gate). Weekly as a scheduled job for monitoring. After any corpus update (new documents, re-indexing).
What is a good MRR score?
MRR > 0.70 means the first relevant document is typically in the top 2 results. MRR > 0.85 is excellent. Low MRR with high recall@5 means the right docs are found but ranked too low - add reranking.
How do I handle queries with multiple relevant documents?
Use nDCG@k with graded relevance scores (0–3). A query about "security policies" may have 3 relevant docs with different relevance levels.
Should I include negative test cases?
Yes. Include queries where the answer is NOT in the corpus. Verify the retriever returns low-scoring results, enabling the system to say "I don't know."
How do I evaluate metadata filtering?
Include test cases that verify tenant isolation (cross-tenant queries return zero results) and permission boundaries (employee queries don't return admin docs).
References
- Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks (Lewis et al., 2020)
- LangChain Retrieval Documentation
- LlamaIndex RAG Documentation
- Pinecone RAG Guide