Someone asks your documentation assistant a simple question: “What’s the rate limit on the /v2/upload endpoint?” The number is right there in your API reference, in a table, under a heading that literally says upload limits. The assistant comes back with a confident paragraph about uploads that never states the number.
You check. The document is indexed. The answer exists. And the model, when you paste the right passage in by hand, answers perfectly. So the model isn’t the problem, and the data isn’t missing. Retrieval simply failed to hand the model the one passage it needed.
This is the most common and most frustrating failure in production RAG, and almost everyone misdiagnoses it. They swap the embedding model, or upgrade to a bigger LLM, and the miss stays exactly where it was. Because the miss isn’t in the model. It’s in the pipe that feeds it.
Retrieval is a funnel, not a search box
Here’s the reframe that fixes how you debug this. Retrieval is not one operation. It’s a two-stage funnel. The first stage, retrieve, casts a wide net and pulls maybe a hundred candidate passages out of your whole corpus; its only job is recall, getting the right passage into that candidate set at all. The second stage, rank, sorts those candidates and forwards the top handful to the model; its job is precision, getting the right passage to the top.
Two stages, two different jobs, and three places the water leaks out. The answer can get chopped up when you chunked and indexed it. The search can match the wrong words and never pull it into the candidate set. Or the right passage can get pulled in but ranked too low to survive the cut. The crucial thing is that two of those three leaks happen before ranking, which is exactly why buying a fancier reranker so often changes nothing.
Leak 1: the answer got chopped up
Before anything can be retrieved, your documents get split into chunks and embedded. That splitting is where the first answers die. Chunk on a blind character count and you slice a table clean away from the heading that gave it meaning, so the chunk holding your rate-limit number no longer says a word about /v2/upload. Or you go the other way, a single 2,000-token mega-chunk covering install, config, networking, and limits, and the embedding becomes a mush-average of six topics that is a strong match for nothing.
The first fixes are cheap. Chunk on structure, headings, sections, code blocks, not a fixed window, so a table stays with its title. Add overlap so an answer straddling a boundary survives in at least one chunk. And when a chunk is still too orphaned to make sense alone, prepend a sentence of context to it before you embed it, so “the rate limit is 100 requests per minute” carries “this is the /v2/upload endpoint reference” with it. Anthropic’s version of this, contextual retrieval, cut failed retrievals by 35% on its own benchmark, by 49% once paired with keyword search, and by 67% once a reranker was added on top, for a one-time indexing cost of about a dollar per million tokens. Cheap insurance against the leak that starts before search even runs.
Leak 2: the search matched the wrong words
Say the chunk is perfect and whole. It can still miss, because of how the default search works. Dense vector search embeds your query and every chunk into a space where nearness means similar in meaning, and meaning is exactly the wrong lens for an exact identifier. To an embedding model, /v2/upload looks like generic “upload,” ERR_CONN_RESET_481 looks like “a network problem,” and a part number looks like noise. It confidently retrieves passages that are about the right topic and skips the one with the literal string you need.
The old-fashioned answer is the fix: BM25, plain keyword search, which does nothing but match terms and therefore nails /v2/upload on the exact token an embedding fumbles. You don’t choose between them, you run both and fuse the results, a setup called hybrid search. The standard fuser is reciprocal rank fusion, which just adds up 1 / (rank + k) for each result across the two lists so a passage both methods like floats to the top. One caveat worth knowing: some libraries advertise “fusion” that is really just concatenate-and-dedupe under the hood, so check that yours actually does the rank math.
Leak 3: the right passage got buried
Now the good chunk is whole and it’s in your candidate set. You can still miss, because being retrieved isn’t the same as being ranked first. First-stage search sorts by how close two vectors are, which is a fast, rough stand-in for real relevance, not the real thing. Your rate-limit passage comes back at rank 14. You forward the top 5 to the model, and it never sees it.
The fix is to retrieve many, then rerank to few. Cast a wide net first: pull the top 50 or 100 candidates instead of 5, so the right passage is almost certainly somewhere in the pile even if it’s sitting near the bottom. Then re-sort that pile with a slower, sharper judge, a cross-encoder reranker.
Here’s why that second pass finds what the first one couldn’t. Fast search never actually reads your question and a passage together. Ahead of time, it boils the question down to a single point and every passage down to its own point, then just measures how close the two points are, so it’s really grading each passage in the abstract, without your specific question sitting next to it. That’s how a spot-on passage ends up at rank 14. A cross-encoder does the opposite: it takes your question and one candidate and reads them side by side, in the same pass, then scores how well that passage answers that question. Reading the pair together is slower, because it re-runs the model on every candidate, but it’s exactly the head-to-head comparison that lifts a buried passage to the top.
Two cautions from the real implementations. The reranker runs a full model pass over every candidate, so it adds real latency and cost. And most rerankers quietly cut their input off at 512 tokens, so an oversized chunk gets truncated before it’s even scored, which is one more reason the bloated chunks from Leak 1 come back to bite you.
Which leak is actually yours?
Three leaks, three different fixes, and here’s the mistake that wastes the most time: bolting a reranker onto a recall problem. A reranker can only reorder passages it was handed. If the right one never made the candidate set, no amount of reranking will conjure it.
So run one test before you change anything. Take a handful of questions you know the answer to, note the exact passage that answers each (this is the seed of a real retrieval eval set), and retrieve a big candidate set, the top 100. Then ask one question: is the gold passage in there at all? If it’s missing, you have a recall problem, and the fix is upstream, chunking or matching. If it’s present but sitting at rank 40, you have a ranking problem, and that’s when a reranker earns its keep. One check, and you know which hole to plug instead of guessing.
The pipe, not the model
Notice what never came up: a bigger model, a newer embedding, a longer context window. Our rate-limit answer was in the corpus the whole time. It missed because a table got split from its heading, because meaning-search skimmed past a literal path, and because a good passage got out-ranked by plausible neighbors. Three holes in a pipe, each with a boring, known patch: chunk on structure with overlap, search hybrid and fuse the results, and retrieve wide then rerank narrow, after you’ve checked which leak you actually have.
That is the unglamorous truth about RAG that works. The intelligence you’re paying for on the generation side is wasted if the retrieval side hands it the wrong page, and almost none of the wins are on the generation side. This is the half of RAG we build at Gracient: the retrieval pipe that reliably puts the right passage in front of the model, so the answer that was always in your documents is the one your users actually get. Let’s talk.