AI agents

What Recall@k Misses: A Complementary Retrieval Check

Antonio Evans9 min read

Two document stacks side by side on white: the left stack has the top card highlighted with a green check labeled Recall@k equals 1.
Key points

Key points

  1. 1Recall@k only checks whether the correct chunk appears somewhere in the top k, so it stays at 1.0 even when that chunk is ranked last.
  2. 2Mean reciprocal rank captures where the first useful chunk lands, exposing ranking failures that recall hides.
  3. 3A coverage check counts how many of a query's required facts are present in the retrieved set, catching multi-fact questions that recall cannot score.
  4. 4Together the three metrics isolate ranking, completeness and freshness problems before they reach the generator.

Why Recall@k Stays Silent When the Right Chunk Is Buried

Recall@k is a binary metric. For each query, it asks a single question: was the target chunk in the top k results? If yes, the query scores 1. If no, it scores 0. Across a test set, you average those scores (the DataAIHub retrieval guide). The Evidently AI guide describes the same ground-truth approach, where each query has a set of known relevant documents and recall@k checks whether they were retrieved (Evidently AI).

This is a useful gate. If the target chunk is not in the top k at all, no prompt or model upgrade can recover it, because the generator never sees the text. Retrieval evaluation exists precisely to catch that failure before you start rewriting prompts (the DataAIHub guide).

The problem is that recall@k treats position one and position ten as identical. The classic worked example from a retrieval metrics tutorial makes the gap concrete: a recipe system evaluated on 100 queries reached Recall@1 of 0.52, Recall@3 of 0.71 and Recall@5 of 0.83 (the Recall@k and MRR tutorial). Recall@5 told the team that 83 percent of queries found their target somewhere in the top five. It said nothing about how many of those targets were sitting in first place where the generator would weight them most heavily.

That distinction matters in RAG because context order influences how the model uses its input. A chunk at rank five sits behind four competitors. Anthropic's Contextual Retrieval work reports that adding contextual prefixes to chunks reduced failed retrievals by 49 percent, and combining context with reranking pushed the improvement to 67 percent (Anthropic engineering). Reranking is by definition a position change: the same chunks arrive, but in a different order. Recall@k is blind to that change, so a reranker can improve real answer quality while leaving your headline retrieval number flat.

A note on vocabulary. The Claude Cookbook uses a metric it calls Pass@k, which checks whether the golden chunk was present in the first k results, and reports that contextual embeddings raised Pass@10 from roughly 87 percent to roughly 95 percent on a nine-codebase dataset with 248 queries (the Claude Cookbook). That is recall@k under another name. It is a good presence check. It is not a ranking check.

Decision rule: keep recall@k as a deployment gate (if it drops, something is broken in the index or the embedding step), but never use it alone to decide whether a ranking change is an improvement. Add a position metric before you tune rerankers, chunk sizes or hybrid weights.

A Position Metric: Mean Reciprocal Rank

Mean reciprocal rank (MRR) scores each query by the reciprocal of the rank of the first relevant result: 1 if the target is first, 0.5 if second, 0.33 if third, and so on. You then average across queries (the Evidently AI video on RAG evaluation). A worked example from a retrieval metrics explainer shows three queries where the top result lands at ranks 1, 2 and 4: the reciprocal scores are 1, 0.5 and 0.25, giving an MRR of 0.583 (the metrics explainer).

MRR is the right complement to recall when your queries need one specific chunk, for example "what is the refund window for enterprise plans" or "which error code does TS-999 refer to". It punishes the exact failure recall forgives: the correct chunk retrieved but ranked low.

Run the two metrics side by side on a small test set and the diagnosis writes itself:

Test set stateRecall@5MRRWhat it meansWhat to fix
Target in top 5, mostly at rank 10.950.90Healthy pipelineNothing urgent
Target in top 5, mostly at rank 4 or 50.950.28Correct chunk buriedAdd a reranker or rebalance hybrid weights
Target often absent from top 50.600.50Index or chunking gapRevisit chunking, embeddings or query expansion
Target absent and queries need several facts0.600.50Retrieval and coverage failureFix chunking first, then add a coverage check

That second row is the case recall cannot see. A system with recall@5 of 0.95 and MRR of 0.28 is retrieving the right material but ordering it badly, and the cheapest fix is usually a reranking step rather than new embeddings. Venelin Valkov's advanced retrieval pipeline tutorial walks through exactly this architecture: hybrid search over vectors and full-text, combined with reciprocal rank fusion, then a FlashRank reranker before the top chunks reach the model (the advanced retrieval pipeline tutorial).

When to use nDCG instead

MRR only credits the first relevant result, which is fine when one chunk answers the query. Many RAG queries need several chunks: a comparison, a policy plus an exception, a figure plus its footnote. In that case use normalized discounted cumulative gain (nDCG), which sums graded relevance across the ranked list while discounting each item by its position (the Toloka RAG evaluation guide). It rewards putting several useful chunks near the top, not just one. A medium tutorial on RAG evaluation lists nDCG alongside MRR and MAP as the standard offline position-aware metrics (the RAG evaluation walkthrough).

Decision rule: single-answer lookups → MRR. Multi-chunk answers with graded relevance → nDCG@k. Both → report recall@k plus one of them, never recall alone.

A minimal scoring helper looks like this:

def mrr(ranked_ids, relevant_ids):
    for position, doc_id in enumerate(ranked_ids, start=1):
        if doc_id in relevant_ids:
            return 1.0 / position
    return 0.0

def coverage(ranked_ids, fact_to_chunks):
    hit_facts = {f for f, chunks in fact_to_chunks.items()
                 if set(chunks) & set(ranked_ids)}
    return len(hit_facts) / len(fact_to_chunks)

The first function returns a position score. The second returns the fraction of required facts your retrieved set touches, which is the metric most teams are missing.

The Coverage Check: Does the Retrieved Set Contain Every Required Fact?

Here is the failure that both recall@k and MRR miss. A user asks a question that requires two facts: one from the pricing page and one from the contract terms. Your test set records one "golden chunk." The pipeline retrieves it at rank one. Recall@1 is 1.0, MRR is 1.0, and the answer is still wrong because the second required fact never made it into the context window.

Evidently AI's guide describes this as a context quality question: given the retrieved context as a whole, is there enough information to answer the user's question? (Evidently AI). The guide also notes that you can ask an LLM judge to produce that verdict at scale, which makes coverage practical to run in a regular evaluation loop (Evidently AI).

The coverage check turns that idea into a number. For each query in your test set, list the facts needed for a complete answer and the chunks or passages that supply each fact. Then, for the retrieved set, count how many of those facts are represented:

  • Required facts: 3 (price, contract term, cancellation window).
  • Retrieved chunks: 5, containing facts about price and contract term only.
  • Coverage: 2 / 3 = 0.67.

Set a threshold, for example 0.9, and fail the query in CI when coverage drops below it. This catches the failure mode where the right chunk is present but the right set is not.

Coverage is also where freshness problems surface. A retrieval index built before a document changed will still return a chunk that matches the query, so recall looks healthy. The chunk is simply out of date, and the fact it supplies is stale. A coverage check tied to the current source of truth will flag the gap as soon as the fact is no longer present in any current chunk. The Galileo guide makes the related point with chunk attribution: a high-quality retrieval set that the generator ignores or that contains the wrong content still produces a weak answer, and measuring the connection between retrieved content and the final output is the only way to see it (Galileo).

Building the coverage test set

  1. Sample 50 to 200 queries from real traffic logs. The DataAIHub guide recommends a golden set in that range built from your actual corpus and expected queries (the DataAIHub guide).
  2. For each query, write the minimum set of facts a correct answer must contain. One to four facts per query is typical.
  3. Map each fact to the chunks that contain it, and record the document version those chunks came from.
  4. Score every retrieval change against recall@k, MRR (or nDCG) and coverage.

Evidently AI's video walks through the alternative path: rather than writing the dataset by hand, you can generate synthetic question and answer pairs from your corpus and use an LLM judge to label relevance and context sufficiency (the Evidently AI video). That is often the faster route to a usable coverage set for a large corpus.

Why These Failures Happen: Chunking and Exact Match

Both hidden failure modes, buried chunks and missing facts, trace back to how the corpus was chunked and how it is searched.

Anthropic's post explains the core problem: traditional RAG removes context when encoding information, which causes the system to fail to retrieve relevant information from the knowledge base (Anthropic engineering). A chunk that reads "the company's revenue grew by 3% over the previous quarter" does not say which company or which quarter. A query about a specific company and period will not rank it well, so recall@5 drops or, worse, passes at rank five and fails at answer time (the Contextual Retrieval implementation writeup).

Exact-match failures cause the other side of the problem. Anthropic gives the example of the query "Error code TS-999": an embedding model may surface general error-code content while missing the exact string (Anthropic engineering). Venelin Valkov makes the same point: semantic embeddings miss specific IDs, acronyms and keywords, which is why hybrid search adds full-text search and combines the two result lists with reciprocal rank fusion (the retrieval pipeline tutorial).

Reranking then decides whether the right chunk actually lands at rank one. The Claude Cookbook's pipeline uses Cohere for reranking on top of hybrid retrieval as its final quality step (the Claude Cookbook). Once a reranker is in the pipeline, recall@k is effectively pinned: the same candidate set arrives, refined in order. Only a position metric can tell you whether the reranker is earning its latency.

A Worked Example with Numbers

Take a 100-query test set for a support assistant over product documentation and contracts. Before adding a reranker:

  • Recall@5 = 0.90 (90 queries have the target chunk in the top five).
  • MRR = 0.31 (targets are spread deep across the list).
  • Coverage at threshold 0.9 = 0.68 (32 queries are missing at least one required fact).

The recall number looks fine, so a team relying on it alone would ship. Coverage reveals that a third of queries cannot be answered completely no matter where the chunks rank.

Now add a reranker over 20 candidates:

  • Recall@5 = 0.90 (unchanged, as expected).
  • MRR = 0.74.
  • Coverage = 0.71.

The reranker fixed the ordering problem but only marginally improved coverage, because reordering cannot conjure a missing fact. That tells you the next investment is chunking or query expansion, not a bigger reranker. The Evidently AI guide's separate retrieval and generation framing supports this sequence: fix what the retriever finds before you tune what the model does with it (Evidently AI).

If you are generating synthetic queries or running an LLM judge across several models to score these metrics, Quanta Inference puts text and embedding models behind one OpenAI-compatible API, which keeps the evaluation harness simple when you compare backends.

Common Mistakes and a Practical Checklist

The most common mistake is tuning prompts against a retrieval metric. If recall@k or coverage is low, prompt changes cannot help: generation quality cannot exceed retrieval quality (the DataAIHub guide). The second mistake is scoring retrieval and generation together, which makes it impossible to tell which stage failed.

A practical checklist before any retrieval change ships:

CheckQuestionGate
Recall@kIs the target chunk in the top k?Must not drop below baseline
MRR or nDCG@kWhere do useful chunks land?Should rise when rerankers or fusion weights change
CoverageAre all required facts for the query present?Must clear your threshold, e.g. 0.9
FreshnessDo retrieved chunks come from current document versions?Any stale chunk is a fail
Cost and latencyDid the change add reranker calls?Within your latency budget

The only metric you should gate a deploy on is one that fails when a user would fail, and that is the combination, not any single number (the DataAIHub guide).

FAQ

FAQ

Why does Recall@k stay at 1.0 when the correct chunk is ranked last?

Because recall@k is binary per query: it scores 1 if the target appears anywhere in the top k and 0 otherwise, regardless of position (the DataAIHub guide). Position is simply not part of the calculation. To detect a buried chunk, pair recall with MRR or nDCG, which discount by rank (the Toloka guide).

When should I use MRR and when should I use nDCG?

Use MRR when each query needs one specific chunk: it scores the reciprocal rank of the first relevant result (the metrics explainer). Use nDCG when several chunks matter and their relevance differs, because it sums graded relevance with a position discount across the whole ranked list (the Toloka guide).

How do I measure coverage without labeling everything by hand?

Start with a golden set of 50 to 200 real queries and write the required facts for each (the DataAIHub guide). For the rest, generate synthetic queries from your corpus and use an LLM judge to answer the question "is there enough information in this context to answer the query," which is exactly the context quality judgment Evidently AI describes (Evidently AI).

Does a reranker improve recall@k?

Usually no. A reranker reorders the candidate set, so the same chunks are present and recall@k stays flat, while MRR and nDCG rise. Semantically, reranking is a position fix, which is why Anthropic pairs contextual retrieval with reranking and measures the combined effect on failed retrievals (Anthropic engineering).

What is the smallest useful retrieval evaluation setup?

One golden set of 50 or more queries with recorded target chunks, run in CI on every change, scoring recall@k, one position metric and a coverage threshold (the DataAIHub guide). Run retrieval and generation evals separately so a failure points to one stage rather than the whole pipeline (Evidently AI).

Sources

Sources

  1. RAG Retrieval Evaluation: Metrics & Methods | DataAIHub
  2. A complete guide to RAG evaluation: metrics, testing and best practices
  3. Retrieval Metrics Tutorial: Recall@k and MRR Explained
  4. Contextual Retrieval in AI Systems - Anthropic
  5. Enhancing RAG with contextual retrieval | Claude Cookbook
  6. 6.1 How to evaluate a RAG system: methods and metrics - YouTube
  7. Recall@K, Precision@K, MRR, MAP, NDCG #rag
  8. Advanced Retrieval Pipeline for RAG (HyDE, Hybrid Search, Reranking)
  9. RAG evaluation: a technical guide to measuring retrieval- ...
  10. RAG: Part 7: Evaluation
  11. Optimizing RAG Evaluation: Key Techniques and Metrics
  12. Implementing Contextual Retrieval in RAG pipeline | by Plaban Nayak
About the author

About the author

Antonio Evans
Antonio Evans

Antonio Evans is the founder of Quanta Intelligence and a fractional CTO who builds HIPAA-ready AI systems for health and life-science companies. Before that he was technical co-founder of FriendsEAT for twenty years, CEO of Socialdraft, and a data engineer at IBM. Antonio lives with his dog Santino in Lisbon, Portugal.

All articles