AI agents

Measuring Retrieval Recall After a Document Refresh

Antonio Evans9 min read

A split-screen diagram: on the left, a stack of document chunks feeding into an embedding model and an index; on the right, the same pipeline after a refresh.
Key points

Key points

  1. 1Rebuild the new index into a separate namespace and leave the old index untouched until the diff passes, so you always have a working rollback.
  2. 2Run the same frozen query set against both indexes, compute recall@k per query, and flag every query whose recall drops past a threshold you set in advance.
  3. 3Recall@k is a set-level check: it asks whether labeled relevant chunks appear inside the top k, not how far up they sit, so it survives order changes better than rank-based metrics.
  4. 4Inspect dropped-query chunks first, then chunks that crossed the k cutoff, then the highest query-frequency chunks, before you re-embed anything.

Why a rebuild quietly moves the numbers

Re-embedding is not a neutral operation, even when the text is identical. Chunk boundaries change when you switch splitters, size targets, or overlap, and chunking choices decide what can be retrieved at all. Pinecone's guide makes the point bluntly: if chunks are too small or too large you get imprecise results or missed content, and if a chunk does not make sense to a human without surrounding context, it will not carry meaning into the index either (Pinecone). NVIDIA's chunking experiments show the same instability from the other direction: the best-performing chunk size varied by dataset, with one dataset favoring 1,024-token chunks, another 512, and page-level chunking producing the highest average accuracy across all tested datasets (NVIDIA). A rebuild that changes any of those parameters changes the unit you are measuring.

The second source of drift is context loss at the chunk level. Anthropic's Contextual Retrieval work reports that adding chunk-specific context before embedding reduced the top-20 retrieval failure rate by 35 percent averaged across tested data sources, and 49 percent when combined with contextual BM25 (Claude Cookbook). That is the size of effect a context-free versus context-enriched corpus can produce. If your rebuild adds or removes that kind of prefix, your aggregate recall number will move even though your documents did not.

Add hybrid retrieval and the picture multiplies. A pipeline that fuses dense and sparse results, then reranks with a late-interaction model, has three retrieval paths that can each shift when one index is rebuilt (Qdrant). Qdrant notes that hybrid search casts a wide net to maximize recall while reranking tightens precision on a smaller candidate set. That means a rebuild can improve candidates and still degrade the final top-k if the fusion weights or reranker no longer match the new embeddings.

A rebuild also has a database-world analogue worth borrowing. SQL Server's guidance distinguishes reorganize (light maintenance) from rebuild (full regeneration), and recommends choosing based on fragmentation levels rather than rebuilding everything on a schedule (Microsoft Learn). The parallel for vector search: do not rebuild and promote in one motion. Stage, measure, then promote.

What recall@k actually measures, and what it does not

Recall@k answers one binary question per query: did at least one chunk I labeled relevant appear in the top k results? A practitioner-oriented tutorial computes it as the count of queries whose target appeared in the top k divided by total queries, and walks through a 100-query example where recall@1 came out at 0.52, recall@3 at 0.71, and recall@5 at 0.83 on the same system (Rajnish Khatri). Note what this implies: the same index can show a large drop at k=3 and a small one at k=10 because the metric only cares about presence inside the cutoff.

This is why recall@k is the right primary metric for a rebuild gate. It is set-level, so a reranker that reorders correct chunks without evicting them will not trip the gate. Rank-quality metrics move on every ordering change and generate noise you cannot act on. Use MRR or NDCG as secondary signals, not as the promotion criterion.

There is a second reason to keep the metric narrow. Retrieval quality is separable from generation quality, and many teams conflate the two and end up prompt-engineering a retrieval problem. A golden set of query-document pairs lets you gate every retrieval change on recall before you touch prompts or models (DataAIHub). Evidently's RAG evaluation guide draws the same line: evaluate retrieval with ground-truth ranking metrics like recall@k, and evaluate generation separately (Evidently AI).

Decision rule: if a query's labeled relevant set is empty or unlabeled, exclude it from the recall computation and report it as "unjudged" rather than counting it as a miss. Silent unjudged queries inflate drops and make the gate unreliable.

MetricReacts toUse in rebuild gate
Recall@kLabeled chunk absent above cutoffPrimary gate (threshold on delta)
Precision@kIrrelevant chunks crowding the top kDiagnostic only
MRRRank of first relevant chunkSecondary, expect movement
NDCG@kOrdering and graded relevanceSecondary, expect movement
Unjudged countMissing labelsHard blocker, fix labels first

Step 1: Freeze the query set and the labels

You cannot diff what you keep changing. Before any rebuild, snapshot three things: the query text, the labeled relevant chunk IDs per query, and the index parameters that produced the current baseline (splitter, chunk size, overlap, embedding request configuration, fusion weights, k values).

Label granularity matters. Anthropic's cookbook evaluation set contains 248 queries, each with a single golden chunk, and measures pass@k by checking whether the golden document appears in the first k results (Claude Cookbook). Single-golden-chunk labeling is cheap but brittle for a rebuild diff, because a re-chunking can split the golden chunk into two smaller pieces, neither of which matches the original ID. Where a document is genuinely relevant, label all passages that answer the question, not just the one your current chunker happens to produce. That is what makes the metric survive a boundary change.

If you have nothing labeled, you have three routes. Manual labeling of top-k results is what Google-style relevance rating programs do at scale, and LLM judges can stand in for a share of it (Evidently AI). Synthetic query-answer pairs bootstrapped from the corpus get you a first set faster, as does an LLM-judged relevance pass over retrieved chunks. Whatever route you take, freeze the labels before the rebuild so old and new indexes are scored against identical ground truth.

Step 2: Build into a separate namespace and keep both indexes live

Rebuild into a new collection or namespace with a name that includes the parameters, for example docs_ctx_v2_k512_ov64. Keep the serving index untouched. Every retrieval call during the diff period takes a namespace parameter, so you can send the identical query to both.

def recall_at_k(hits, gold_ids, k):
    top_k = {h["chunk_id"] for h in hits[:k]}
    return 1.0 if top_k & set(gold_ids) else 0.0

def evaluate(index, queries, k=5):
    out = {}
    for q in queries:
        hits = index.search(q["namespace"], q["text"], top_k=k)
        out[q["id"]] = recall_at_k(hits, q["gold_ids"], k)
    return out

Run the evaluation on both indexes with the same k values. Pick your k from the pipeline, not from habit: Anthropic's pass@k evaluation used k equal to the number of chunks sent to the model, ten in that example. If your prompt budget carries five chunks, gate on recall@5 and report recall@3 and recall@10 as context.

Two operational notes. First, if you use a hosted inference layer for embeddings, keep the embedding request configuration in version control alongside the namespace name; a changed input type or truncation setting is a silent variable. Second, if your chunks embed external context, generate that context once and store it with the chunk record, for example the contextual prefix pattern described in the Anthropic guide. Regenerating context prefixes with a different prompt on the new pass is another uncontrolled variable.

Step 3: Diff per query and flag drops past a threshold

Aggregate recall hides the thing you need to fix. A system that goes from 0.78 to 0.80 overall can still have thirty queries that fell off a cliff and forty that improved. Diff at query level.

A worked example with 200 labeled queries and k=5:

  • Old index: 156 queries had a labeled chunk in the top 5. Recall@5 = 0.780.
  • New index: 159 queries hit. Recall@5 = 0.795.
  • Per-query diff: 41 queries dropped by one hit, 44 gained, and 115 were unchanged.
  • With a 0.05 threshold applied to a per-query binary score, every change counts, so you flag all 85 changed queries. That is too many.

The threshold has to match the metric's granularity. For binary recall@k, use the threshold on the aggregate plus a second rule on the counts: block promotion if aggregate recall@k falls at all, and review every dropped query. For graded per-query scores (share of labeled chunks retrieved), a 0.05 drop threshold is sane. Two gates are worth running:

GateConditionAction
Aggregate floorrecall@k new < recall@k oldBlock promotion
Aggregate tolerancerecall@k new within 0.005 of oldAllow with review queue
Per-query dropany query loses a hitQueue for chunk inspection
Unjudgedany query unlabeledBlock until labeled

Report the diff with the queries sorted by largest drop, and attach the old top-k IDs, the new top-k IDs, and the labeled relevant IDs side by side. That table is the artifact you inspect in the next step.

Step 4: Inspect chunks in the right order

Not every dropped query deserves equal attention. Use this order, and stop each branch when the cause is clear.

First, inspect the labeled relevant chunks for dropped queries. In a rebuild, these are the chunks that used to carry the answer. Check whether they still exist in the new index, whether their boundaries changed, and whether the new tokenizer truncated them. Contextual prefix changes, chunking parameter changes, and truncation are all silent and all show up here first. This is where the Contextual Retrieval numbers matter: a prefix change alone can move thousands of chunks' vectors at once.

Second, inspect chunks that changed rank across the k cutoff. These are the near-misses: relevant chunks that sat at position 3 in the old index and position 7 in the new one, or the reverse. Pull them by chunk ID from both namespaces and compare the new chunk text to the old. If the text is identical and only the vector moved, your embedding request or model changed. If the text differs, your splitter changed.

Third, inspect chunks with the highest query frequency. Count how many of your labeled queries point at the same chunk ID. A chunk cited by fifteen queries is worth more of your time than a chunk cited by one. Frequent chunks are also the best candidates for adding explicit context prefixes, because the fix compounds across every query that needs them.

Fourth, inspect chunks that grew or shrank past the k cutoff by more than one position. Log the chunk's token count in both indexes and its retrieval rank per query. A chunk that dropped from position 4 to position 9 usually means competition from neighboring chunks that split off in the new pass, which is a boundary problem rather than an embedding problem.

Fourth step, but worth stating as a separate check: review any query where the new index returned a labeled chunk the old one missed in a different chunk ID with the same source document. That is a boundary win, not a regression, and it should be logged as an improvement in the review queue so the next diff does not re-flag it.

Step 5: Promote only on evidence, then decide about re-embedding

Set the promotion rule before you look at results. A defensible one: promote the new index when aggregate recall@k is at or above the old index for every gate k, no query drops below the aggregate floor, and the unjudged count is zero. If you must trade, define which k value wins: for most RAG systems recall@k matters most at the k that matches your prompt budget, so weight recall@5 over recall@10.

A worked promotion decision. Old index recall@5 is 0.780. New index, first attempt, is 0.760 with 41 dropped queries and 9 of those dropping from hit to miss at k=5. Inspect the 9 first. Suppose seven of them involve a single source document whose sections were split at 512 tokens in the new pass instead of 1,024. Raise the chunk size for that document type, rebuild the namespace again, re-run, and you get 0.790 with 3 dropped queries. That clears the floor and passes, with the 3 drops logged as accepted trade-offs. Only after promotion do you consider re-embedding the full corpus through a new model or a new context-prefix prompt, and it goes through the same diff before it is promoted.

Re-embedding is not the first fix. The chunk-boundary and context-prefix fixes are cheaper, reversible and usually the actual cause.

If you are spending most of this cycle re-plumbing embedding calls across providers, that is a separate problem from retrieval quality. Quanta Inference gives you text, image, video, audio and embedding models through one OpenAI-compatible API key, which keeps the embedding request configuration consistent across rebuilds and makes the namespace-and-diff workflow simpler to run at scale: https://quantaintelligence.ai/inference

FAQ

FAQ

What k should I gate on when I run the recall diff?

Gate on the k that matches how many chunks your prompt actually carries, and report the neighbors as context. Evidently's guide describes recall@k as a ranking metric for ground-truth retrieval evaluation, and the Anthropic cookbook used pass@k with k equal to the number of chunks passed to the model (Evidently AI, Claude Cookbook). If your pipeline sends five chunks, gate on recall@5.

Why does a rebuild lower recall even when the text has not changed?

Because the embedding input changed. Chunk boundaries, truncation, context prefixes and the embedding request itself all feed the vector, and chunking choices determine whether a chunk carries meaning at all (Pinecone). Adding contextual prefixes alone moved the top-20 retrieval failure rate by double digits in the published measurements (Claude Cookbook).

My aggregate recall went up but users report worse answers. What now?

Diff per query, not per aggregate. Aggregate gains can mask a cluster of dropped queries, and grouping them by source document or query type usually reveals a single chunking or fusion change. Hybrid pipelines fuse multiple retrieval paths before reranking, so one path can degrade while the fused total improves (Qdrant).

Can I use precision@k instead of recall@k for the gate?

No, use recall@k as the gate and precision@k as a diagnostic. Precision@k tells you how crowded the top k is, recall@k tells you whether the answer survived the cutoff at all. Galileo's evaluation write-up notes that a pipeline optimized only for precision can miss the full scope of necessary information (Galileo).

Do I need to label every query before diffing?

Yes, at minimum every query you gate on. Unjudged queries count as misses and can produce phantom regressions. For a practical start, build the set from your corpus with synthetic generation and LLM-judged relevance, then hand-verify the queries that the diff flags, in line with the labeling approaches described in Evidently's guide (Evidently AI).

Sources

Sources

  1. Chunking Strategies for LLM Applications - Pinecone
  2. Finding the Best Chunking Strategy for Accurate AI Responses
  3. Enhancing RAG with contextual retrieval | Claude Cookbook
  4. Qdrant Hybrid Search with Reranking
  5. Optimize index maintenance to improve query performance ...
  6. Retrieval Metrics Tutorial: Recall@k and MRR Explained
  7. RAG Retrieval Evaluation: Metrics & Methods | DataAIHub
  8. A complete guide to RAG evaluation: metrics, testing and best practices
  9. Optimizing RAG Evaluation: Key Techniques and Metrics
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