Most explanations of RAG start with a box diagram and leave you no wiser. This one starts at the smallest thing in the system, a single number, and zooms out one level per chapter until you can see the whole organization around it. Thirteen scales, each with an animation. About 25 minutes.
Here is the whole trick, and it is not much of a trick. A model reads a piece of text and outputs a list of numbers. That is it. Everything else in this guide is built on top of that one move.
Nothing you can name. There is no dimension for "legal-ness" or "urgency". The meaning is spread across all 1024 values at once, which is why you cannot read an embedding and why nobody tries. What matters is not any individual number but where the whole list points.
You cannot decode an embedding back into the original sentence. Information is lost on purpose.
Two sentences sharing no words can land in nearly the same place, if they mean the same thing.
Different model, different job, different bill. You will call it millions of times, so it has to be cheap.
Put those 1024 numbers together and you have coordinates. Two numbers locate a point on a page. Three locate a point in a room. A thousand locate a point in a space you cannot picture, which turns out not to matter at all, because every operation you need works exactly the same way in two dimensions as in a thousand.
It was trained that way, deliberately and unmysteriously. You feed the model millions of pairs of things people treat as related, a question and its answer, a title and its article, and you pull each pair together while shoving unrelated text apart. Do that enough times and the geometry falls out.
Which tells you exactly where it will let you down. The model only knows the notion of similarity that was in its training pairs. Nobody trained it on pairs where your internal project codename means the same thing as your product. In your corpus, it does.
"Contracts without an indemnity clause" lands almost exactly where "with" lands. There is no logical NOT in geometry.
silentERR_4021 gets smeared into "something error-shaped". Rare tokens carry almost no weight.
"Deals above $5M" needs a greater-than. The space has no concept of more.
use filters"The latest version" means nothing to a vector. It does not know what day it is.
use metadataTwo points. One question: how close are they? The answer is the single most important calculation in retrieval, and it is three lines of arithmetic you already know.
As the angle opens, similarity falls. That is the entire mechanism. The length of either arrow is thrown away before the comparison is made.
# a and b are just lists of numbers a = [3, 1, 0, 2] b = [2, 0, 1, 3] # 1. dot product: multiply position by position, add it up a·b = (3×2) + (1×0) + (0×1) + (2×3) = 12 # 2. magnitude: the length of each arrow (Pythagoras, extended) |a| = √(9+1+0+4) = √14 = 3.7417 |b| = √(4+0+1+9) = √14 = 3.7417 # 3. cosine: divide the lengths out, leaving pure direction cos(a,b) = 12 / (3.7417 × 3.7417) = 0.8571
Scale a by 100 and the dot product explodes from 12 to 1200. Cosine stays at exactly 0.8571. Dividing by both magnitudes cancels length completely, so only direction survives.
Force every vector to length 1 and the denominator becomes 1×1. Cosine collapses into a plain dot product: multiply and add, no square roots. This is why vector databases want normalized vectors.
distance² = 2 − 2·cos. Cosine appears only with a minus sign, so higher cosine
always means lower distance. Cosine and Euclidean produce identical rankings. They are
the same ordering wearing different clothes.
You do not embed documents. You embed chunks. Almost everyone assumes this is a workaround for context limits, and almost everyone is wrong. The real reason goes back to the property from scale 01: the output is always the same size.
An encoder produces one vector per token, then pools them, usually by averaging, into the single vector you store. So a chunk covering eight topics is literally the average of eight directions, and the average of eight directions points at none of them. Nothing is truncated. Nothing errors. The vector is perfectly valid. It just means "corporate document".
A single fact that spans a boundary gets severed, and each half scores worse than the whole would have. You now need three specific chunks to all rank well at once, which is much less likely than one ranking well.
fragmentationDilution, as above, so it is not found. And if it is found, it drags irrelevant text into the prompt, which measurably degrades the answer. Large chunks fail at both ends of the pipeline.
dilution + distractionThe usual band is 200 to 800 tokens, but treat any number you read as a starting point rather than an answer. It depends on your content, and it is measurable, so measure it.
Now zoom out to the thing being cut up. A document has structure, and the moment you slice it on character counts you throw that structure away. Here is the failure that results, and it is more interesting than it first looks.
"It must be renewed within 30 days of expiry. Failure to do so incurs a penalty of 15% of the outstanding balance, and access is suspended until the matter is resolved."
"This chunk is from Section 4 of the Contractor Licensing Policy, covering renewal deadlines and late penalties.""It must be renewed within 30 days of expiry. Failure to do so incurs a penalty of 15%…"
| Fix | What it does | Does it solve the example? |
|---|---|---|
| Overlap | Consecutive chunks share 10 to 20% of their text | No. "It" refers to a heading pages back, not fifty tokens back. |
| Parent-document | Embed small chunks, return the enclosing section to the model | Half. Fixes the ambiguity. The chunk you search over still has no hook, so it still is not found. |
| Contextual retrieval | An LLM writes a situating sentence per chunk at index time, prepended before embedding and before keyword indexing | Yes, both halves. |
Something new becomes possible at this scale, and it is the reason a scoring function from the 1970s is still in every serious retrieval system built today. Once you can see the whole collection, you can measure how rare a word is. An embedding model never can.
ERR_4021 appears in two documents out of two thousand, and is therefore
almost a unique fingerprint.
# inverse document frequency, over a 2,000 document corpus
IDF(term) = ln( (N − df + 0.5) / (df + 0.5) + 1 )
| term | appears in | IDF | contribution |
|---|---|---|---|
the | 1990 of 2000 docs | 0.0053 | essentially nothing |
policy | 400 docs | 1.6087 | moderate |
refund | 120 docs | 2.8098 | strong |
err4021 | 2 docs | 6.6851 | about 1270× what "the" contributes |
Cosine is bounded in [−1, 1] and means the same thing on every query, forever. BM25 is unbounded and its scale is set by whichever words you happened to type. The same scoring function over the same corpus gave a top score of 2.692 for one query and 9.550 for another, purely because the second contained a rarer word.
So a BM25 score of 6.1 is meaningless on its own, normalising is fragile because the distribution shifts per query, and the standard answer throws the scores away entirely and keeps only the ranks.
# Reciprocal Rank Fusion. Σ means "add up, over each list it appears in". score(d) = Σ 1 / (k + rankᵢ(d)) # k ≈ 60 # vector list : docX(1) docY(2) docZ(3) # bm25 list : docW(1) docY(2) docV(3) docY = 1/(60+2) + 1/(60+2) = 0.0323 # 2nd in both docX = 1/(60+1) = 0.0164 # 1st in one, absent from the other # docY wins despite never being anyone's top pick.
Comparing your query against ten million vectors one at a time is a lot of arithmetic. So at scale you stop doing it exactly. You should know precisely what you are giving up, because nothing in the system will tell you.
Memory. You store a graph on top of the vectors, often 1.5 to 2× their size again, and it wants to be resident in RAM. This is the hidden line in your bill.
RAMRemoving a node can sever paths others depended on. Most implementations tombstone and rebuild later, so "deleted" may mean "flagged, still reachable".
compliance riskTrack a candidate list instead of one best node. Bigger means better recall and more latency, tunable per query with no rebuild.
no rebuildrecall@k measured
against a brute-force ground truth is a number you have to go and get on purpose.
Zoom out from the index to the thing arriving at it. The raw string a user typed is usually a bad search key, and what you do between receiving it and answering is a funnel, not a lookup.
Query and document are encoded independently, and never see each other. Relevance is a dot product between two vectors computed at different times. That is exactly why it scales: documents are embedded once, offline, and search becomes arithmetic.
precomputableQuery and document go through a transformer together, with full attention between every query token and every document token. Far more accurate, and it can finally handle negation, because "without an indemnity clause" and the actual clauses attend to each other.
nothing precomputable| Technique | What it actually changes | Fixes | Cannot fix |
|---|---|---|---|
| Query rewriting | the query string | "and what about for contractors?" as message four | anything structural |
| HyDE | the query vector | questions and answers not looking alike | anything needing a second round |
| Multi-query | how many vectors you search with | "can't log in" vs "authentication failure" | anything needing a second round |
| Decomposition | one query into N independent queries | "compare EU and US policies" | dependent lookups |
| Iterative / agentic | adds dependent round-trips | multi-hop | completeness |
Five chunks go in a box with some instructions and a question. This step gets treated as string concatenation, and four real decisions hide inside it.
Marginally relevant chunks act as distractors and measurably degrade answers. The reranker cutting 100 down to 5 is a quality decision, not just a cost one. Over-fetch early, cut ruthlessly late.
Tell the model to answer only from the provided context and to say so when it is absent. Without that it falls back on its own weights, and the moment it does you can no longer tell whether an answer came from your corpus or from training.
Models fabricate citation IDs, confidently, in exactly the right format. Check programmatically that the ID exists and that the chunk actually contains the claim. Almost nobody does this.
Push grounding too hard and you get over-refusal, where the system declines although the answer was right there. There is no setting that is correct by default.
Far enough out to see both halves at once. There are two pipelines, not one, and they obey completely different physics. Almost every good decision in RAG comes from knowing which half you are standing in.
embedding_model_version on every chunk from day one.
Deleted at source, still in your index. The system now cites a document that does not exist, or states a policy that was formally revoked. In a regulated setting that is an incident, not a bug report.
legal exposureSomeone changes teams. Source ACLs update instantly. The ACL metadata copied into your chunks does not. You are now filtering on yesterday's permissions, and the system is behaving exactly as indexed.
silent leakA document is edited and new chunks are inserted without removing the old. Now v1 and v3 can land in the same prompt. There is no timestamp in the text, so the model picks one, or blends them into something that was never true.
insert without deleteIn a graph index, "deleted" often means "flagged, still reachable until the next rebuild". A hard-delete returning 200 OK may not mean the vector is gone. Know exactly what your store does before you promise erasure.
right to be forgotten
The fix is a posture, not a patch: ingestion is a data pipeline, not a script. Content
hashing for change detection, deterministic chunk IDs from (doc_id, version, index),
delete-then-insert semantics, and a reconciliation job that diffs source against index. Then
pick a freshness SLA deliberately, because "eventually" is how ghosts happen.
Graduate when you actually hit the wall: tens of millions of vectors, sustained high QPS, multi-region. Then choose on filtered-search behaviour, native hybrid support, real delete semantics and multi-tenancy model, not on benchmark latency, which is almost never your binding constraint.
Query-time LLM tokens dominate, as expected. The surprise is RAM: 10M chunks at 1024 dims is roughly 40GB of vectors, plus the graph again on top, resident. Quantization is a cost lever, not just a speed one.
Embedding and retrieval caches are easy wins. A semantic response cache is different: two users can ask near-identical questions and be entitled to different documents. Keyed on text alone, it is a data leak that looks like a performance win.
Long dependency chain in the request path. Decide explicitly: reranker down means serve fused-but-unranked and log it. Vector store down means BM25 only. Otherwise one dependency's bad afternoon is a total outage.
Out one more level and the question stops being how to build RAG and starts being whether to. The most senior thing you can know about an architecture is when it is the wrong one.
| Signal in the requirement | Right tool |
|---|---|
| Answer is a live authoritative value: balance, inventory, status | System of record. SQL or an API call, not retrieval. |
| Requirement is about behaviour: format, voice, structure | Prompt, structured output, or fine-tuning. Retrieval implies selection; if it applies to every query there is nothing to select. |
| Each query is scoped to one bounded document | Filter, then stuff. Do not build a pipeline for this. |
| "How many", "all of", "every", "which ones lack" | Batch analytics. Retrieval has no notion of completeness. |
| A lookup depends on the result of a previous lookup | Iterative retrieval. Decomposition will not help; those run in parallel. |
| Answer lives in a few passages, scattered, and you cannot know where in advance | This is genuine RAG. |
Zoom out past your own system and look at what has actually been built. Thousands of RAG deployments later, they are mostly five shapes. Knowing which one you are building tells you which failures to expect before you meet them.
Customer-facing assistant over help docs plus resolved tickets. The highest-volume shape in production, and the one with the clearest business case: every answered question is a ticket nobody paid to handle.
The "ask our company anything" build. Corpus sprawls across wiki, drive, chat, ticketing. Almost every enterprise has attempted one, and most of the difficulty turns out not to be retrieval.
Legal, compliance, underwriting, procurement. A specialist working through one contract, filing or policy at a time. Note that this shape is usually not RAG once you look closely.
Evidence scattered across many documents, and you cannot know in advance which ones. Incident postmortems, prior art, literature review, competitive intelligence. The purest retrieval problem on this list.
Codebases, runbooks, API references, log and error corpora. Increasingly the highest-value shape, and the one where naive semantic search fails hardest.
Analytics wearing a chat interface. "How many", "what is the trend", "which region grew". Retrieval has no notion of completeness. This is a database query and always was.
The universal company assistant. One index over everything, for everyone. Every corpus has a different freshness profile, permission model and authority level. Scope narrows or the project dies.
RAG over a database. Indexing structured rows as text. Stale the moment it is written, imprecise by construction, and impossible to debug.
The last zoom out. At this scale RAG stops being a technique and becomes a mirror. Most projects do not fail on retrieval quality. They fail here.
Teams that ship this discover their documentation contradicts itself, the same policy exists in four systems with three different answers, and nobody can say authoritatively who is allowed to see what. None of that was created by the retrieval system. It was already true. The system just started reading it out loud.
Eleven failure modes produce one symptom: the answer was wrong. Without per-stage instrumentation you cannot tell them apart, and most teams respond to all eleven by swapping the LLM. The single highest-value artifact in a RAG project is the one most commonly skipped.
100 to 300 triples of (question, expected answer, expected chunk IDs). From real user queries, not questions you invented from reading the docs, because you will phrase them the way the docs are written and engineer out the exact vocabulary mismatch you are trying to measure. Include out-of-scope questions to test refusal.
This is the ceiling. If recall@100 is 68%, then 32% of queries are unanswerable no matter what happens downstream, and every hour spent on the reranker is spent on the 68% that was already fine. That one number tells you whether your problem is upstream or downstream.
LLM-as-judge is how you measure faithfulness at scale, and it has known biases: position, verbosity, and self-preference. Check it against human labels on a sample before trusting it, and again whenever you change the judge model. An unvalidated judge is a number generator with good manners.
Chunk size, embedding model, prompt wording, k, reranker. Every one can silently degrade quality and none produce a failing test. If changes can merge without running the golden set, they will, and you will find out from a customer three weeks later.
Mitigations exist and none are complete. Structurally delimit retrieved content and declare it as data rather than instructions. Apply least privilege to any tool in the loop. Require human confirmation before consequential actions. Never auto-render generated links. Treat it like XSS before frameworks handled escaping: layer defences and assume some will fail.
Research on embedding inversion has shown substantial reconstruction of source text from vectors. A store of embeddings of medical records is, for classification purposes, a store of medical records. Same sensitivity tier, same access controls, same retention rules.
classify accordinglyA shared index with a tenant filter is one filter bug away from a cross-tenant leak, and the failure direction is open. Per-tenant indexes cost more and fail closed. For sensitive data that asymmetry is the whole argument.
failure directionThe linear pipeline you just read is increasingly a special case. The newer shape hands a model a search tool and lets it decide when to search, what for, and whether to search again after reading. Multi-hop stops being a special technique and becomes default behaviour. Long context, meanwhile, genuinely ate the low end: nobody builds an ingestion pipeline for a single 120-page contract now.
These are properties of the problem, not of any solution, which is why they have survived every architectural change so far. Any future approach still has to answer all five. If one appears to vanish, it has moved somewhere you are not looking.
search quality
provenance
authorization
freshness
evaluation
The parts worth keeping open in another tab.
| # | Failure | Stage |
|---|---|---|
| 1 | Content isn't in the corpus | source |
| 2 | Content is there but parsing mangled it | ingest |
| 3 | Indexed but not in top-k | retrieval recall |
| 4 | Retrieved but ranked below the cut | ranking |
| 5 | Right chunk present, drowned by distractors | assembly |
| 6 | Present but positionally ignored | assembly |
| 7 | Answered from model weights, not context | generation |
| 8 | Answered from a slice, presented as complete | structural |
| 9 | Answered from revoked or stale data | freshness |
| 10 | Contradictory chunks resolved arbitrarily | freshness |
| 11 | Refused when the answer was right there | generation |
embedding_model_version stored per chunk.| Thing | Value | Why it matters |
|---|---|---|
| Exact search is fine up to | ~100k vectors | 100% recall, no tuning, no delete semantics to learn |
| pgvector is fine up to | ~10M vectors | and it keeps your source of truth in one place |
| Vector search latency | 5–50ms | rarely your bottleneck |
| One extra LLM call in the query path | 200–500ms | always your bottleneck |
| HNSW memory | vectors + ~1.5–2× | the line item nobody forecasts |
| Typical chunk size | 200–800 tokens | a starting point, not an answer |
| RRF constant | k ≈ 60 | leans toward retriever consensus |