Why Your RAG Returns the Wrong Chunk
The five causes of a confidently wrong retrieval, the query that detects each, and the fix that matches the cause.
Your retriever returned a chunk. It is the wrong one — the answer is plainly in your documents, but the chunk that came back does not contain it. This page is about that exact failure: a chunk was returned and it is wrong. It is not the case where nothing relevant was retrieved at all (that is a recall failure — see missing document), and it is not the case where retrieval was fine but the model still answered badly (see lost in the middle).
Before you change anything, look at what actually came back. Log the top-k chunks your retriever returned for the failing query, together with their similarity scores, and read them. The retriever has no way to tell you it fetched the wrong thing — it hands the chunks over and the model answers confidently from bad source. Reading the retrieval, not the answer, is where every diagnosis starts.
First, which of the five causes is it?
A wrong chunk has five distinct causes, and each has a different fix. Applying the wrong fix — adding a reranker when the problem is vocabulary, say — costs time and changes nothing. Most debugging guides walk the pipeline stage by stage: check chunking, then embeddings, then add hybrid, then a reranker, tuning each in turn. That works, eventually. The faster path is differential diagnosis: start from the one symptom, and run the single measurement that rules each cause in or out. Each row below is a measurement you run before you change code, not a fix you try and hope.
| Cause | What you see | Detection (run this first) | Fix |
|---|---|---|---|
| 1 · Vocabulary mismatch | Query words ≠ document words: acronyms, synonyms, IDs | recall@k(sparse) − recall@k(dense) | Hybrid search |
| 2 · Wrong granularity | Answer split across chunks, or buried in an oversized one | answer-in-one-chunk rate | Chunk size & overlap |
| 3 · Ranking miss | Right chunk is retrieved, but ranked below the cutoff | recall@50 − recall@k | Cross-encoder reranker |
| 4 · Embedding mismatch | High similarity score, still the wrong content | gold-chunk similarity percentile | Domain embeddings |
| 5 · Filter / metadata error | A filter silently excludes the correct document | recall(no-filter) − recall(with-filter) | Metadata filtering |
The query’s words don’t match the document’s
The most common cause. Your dense retriever encodes meaning, but when the signal is the surface form — an acronym, a synonym the docs never use, a product ID, an exact error code — the embedding blurs it and the right chunk never surfaces.
Symptom
The user searches “502 on deploy”; the docs say “bad gateway during rollout.” Same thing, no shared tokens — dense retrieval returns adjacent-but-wrong chunks.
Detection
Re-run the identical query with BM25 only. If the sparse retriever finds the gold chunk and the dense one does not, the meaning was fine — the vocabulary was the gap. Measure recall@k(sparse) − recall@k(dense); a positive gap is the tell.
Cause
Dense-only retrieval has no lexical channel, so exact-term and rare-token queries fall through.
Fix
Add the lexical signal back: run BM25 alongside the embedding retriever and fuse the results. That is hybrid search.
The chunk is the wrong size for the question
Even with perfect retrieval, a badly sized chunk cannot carry the answer: too large and the relevant sentence is diluted by unrelated text; too small and it lacks the context that makes it an answer; or the answer straddles a boundary and no single chunk holds it whole.
Symptom
The returned chunk is near the answer — same section, adjacent paragraph — but the specific fact got cut off at the split.
Detection
Grep the source document for the answer string and check where it falls relative to your chunk boundaries. Measure the answer-in-one-chunk rate across your eval set: how often the full answer lives inside a single chunk.
Cause
Fixed-length splitting cuts on token count, not on meaning, so answers land on the wrong side of a boundary.
Fix
Tune chunk size and overlap, or split on structure. A pure boundary-split miss is its own failure — see chunk boundaries.
The right chunk was found but ranked too low
Sometimes the correct chunk is in the candidate set — it just sits below your top-k cutoff, because vector similarity is not the same as relevance. A stale document at 0.92 cosine similarity will happily outrank the correct, current one at 0.87; the score is high, the ranking is wrong.
Symptom
Raising top-k “fixes” it: at k=5 the answer is wrong, at k=20 it is right. The chunk was always there, just under-ranked.
Detection
Set top-k to 50 and re-query. If the gold chunk appears at a rank greater than your production k, you have a ranking problem, not a retrieval one. Measure recall@50 − recall@k.
Cause
First-stage similarity is a coarse sort; it routinely orders a weakly-relevant chunk above the exact one.
Fix
Re-order the candidates with a cross-encoder reranker. Note that a bigger top-k is a diagnostic, not the fix — it just widens the window.
High similarity, still irrelevant
The confusing one: the retriever reports a high similarity score and returns a chunk that is simply not about the query. High confidence, wrong content.
Symptom
Top-1 cosine similarity is 0.9-something, and the chunk is unrelated. Scores look healthy; results do not.
Detection
Compute the cosine similarity between the query and the known gold chunk. If a human-relevant chunk scores in a low percentile while irrelevant chunks score high, the embedding does not capture your domain’s meaning. Measure the gold-chunk similarity percentile.
Cause
A general-purpose embedding model has no signal for your domain’s jargon, so its geometry places the wrong things close together.
Fix
Adapt the embeddings to your domain — fine-tuning or a domain model — so similarity tracks relevance.
A filter is silently excluding the right document
The cheapest cause to confirm, and the easiest to miss: a metadata filter that unintentionally removes the correct document from the candidate set before ranking ever happens.
Symptom
The document exists, is indexed, and is well-embedded — yet it never appears, for any k, for any phrasing.
Detection
Re-run the query with every metadata filter removed. If the gold chunk now appears, a filter was excluding it. Measure recall(no-filter) − recall(with-filter).
Cause
A tenant, category, date, or permission filter is too aggressive — or the document’s metadata is wrong.
Fix
Verify filters at query time and audit the document’s metadata. This is distinct from tenant leakage, where a missing filter admits the wrong documents.
How do you run all five checks at once?
Rather than reason about the five causes one at a time, run them all at once. Given a failing query, the id of the chunk you know is correct, and your retriever, this prints which cause fired. It depends only on a sparse and a dense retriever you already have, plus your embedding model for the similarity check.
Before you trust the numbers
Run this against your own corpus and a small labelled set of failing queries. The thresholds below are starting points, not universal constants — calibrate them on your data before acting.
import numpy as np
def diagnose_wrong_chunk(query, gold_id, *, dense, sparse, embed, k=5):
"""Print which of the five wrong-chunk causes fired for one failing query.
dense(query, k) -> list[chunk_id] ranked by embedding similarity
sparse(query, k) -> list[chunk_id] ranked by BM25
embed(text) -> np.ndarray (the same model dense() uses)
gold_id -> id of the chunk you KNOW contains the answer
"""
def recall_at(ids): # 1 if gold is in the list, else 0
return 1.0 if gold_id in ids else 0.0
dense_k = dense(query, k)
sparse_k = sparse(query, k)
# Cause 1 — vocabulary mismatch: sparse finds it, dense doesn't
if recall_at(sparse_k) - recall_at(dense_k) > 0:
print("CAUSE 1 vocabulary mismatch -> add hybrid (BM25 + dense)")
# Cause 3 — ranking miss: widening k surfaces the gold chunk
dense_50 = dense(query, 50)
if recall_at(dense_50) - recall_at(dense_k) > 0:
rank = dense_50.index(gold_id) + 1
print(f"CAUSE 3 ranking miss (gold at rank {rank} > k={k}) -> add a reranker")
# Cause 4 — embedding mismatch: gold sits low in the similarity distribution
q = embed(query)
sims = {cid: float(q @ embed(cid) / (np.linalg.norm(q) * np.linalg.norm(embed(cid))))
for cid in set(dense_50) | {gold_id}}
gold_sim = sims[gold_id]
pct = np.mean([gold_sim >= s for s in sims.values()])
if pct < 0.5:
print(f"CAUSE 4 embedding mismatch (gold in {pct:.0%} percentile) -> domain embeddings")
# Cause 5 — filter error: gold appears only once filters are dropped
dense_nofilter = dense(query, k, filters=None)
if recall_at(dense_nofilter) - recall_at(dense_k) > 0:
print("CAUSE 5 filter error -> a metadata filter is excluding the gold chunk")
# Cause 2 — granularity: nothing above fired, so inspect the chunk boundary
if not any([recall_at(sparse_k) - recall_at(dense_k) > 0,
recall_at(dense_50) - recall_at(dense_k) > 0,
pct < 0.5,
recall_at(dense_nofilter) - recall_at(dense_k) > 0]):
print("CAUSE 2 likely granularity -> grep the source; check the answer's chunk boundary")
The script is deliberately transparent: each block is one row of the detection table above, so you can delete the causes you have already ruled out. It reports the cause, never guesses the fix for you.
Why does my RAG retrieve the wrong chunk when the answer is clearly in my docs?
There are five distinct causes: a vocabulary mismatch between query and document, the wrong chunk size, the right chunk ranked too low, an embedding that doesn't fit your domain, or a metadata filter silently excluding the document. Each has a different fix, so run the one discriminating measurement for each before changing anything.
Is a wrong RAG answer a retrieval problem or a model problem?
Inspect the retrieval first. Pull the exact chunks the retriever returned for the failing query. If the correct chunk isn't there, it's a retrieval problem and no amount of prompt tuning will fix it. If the right chunk was retrieved and the answer is still wrong, it's a generation problem — a different page.
It retrieves the right document but still answers wrong — why?
That's no longer a wrong-chunk problem: retrieval succeeded. It's usually a conflict between sources, or the model under-using context buried in the middle of the prompt. Those are generation-side failures — see conflicting sources and lost in the middle.
What should I log to debug retrieval failures?
Log the query, the retrieved chunk ids, the chunk text, their similarity scores, any applied metadata filters, and the final prompt context. That trace is enough to tell whether the failure came from indexing, retrieval, or prompt assembly — you cannot debug what you cannot see.
Does adding a reranker actually fix wrong chunks?
Only when the cause is a ranking miss — the right chunk was retrieved but ordered below your cutoff. A reranker does nothing for a vocabulary mismatch, a bad chunk size, or a filter error. Confirm with recall@50 − recall@k before adding one.
What chunk size should I use?
There's no universal number — measure it. Start around 500–800 tokens with ~100-token overlap, then run your own eval set and compare retrieval recall across sizes. Too small and chunks lack context; too large and they dilute relevance. See the chunk size page.