RAG First Principles: 3 Best-Practice Myths Debunked by Data
Building and measuring a RAG pipeline on 600 financial disclosures shows a smaller embedding model, mean pooling loss, and equal-weight hybrid search all breaking the standard advice.
Most explanations of retrieval-augmented generation show you a diagram: chunk, embed, retrieve, generate. This walkthrough shows the measurements instead, building a RAG pipeline from scratch over 600 financial disclosures, earnings reports, risk factors, merger terms, and surfacing three results that directly contradict the field's standard advice.
The problem RAG solves, stated precisely
The starting scenario is concrete: someone asks what a bank's estimated merger cost synergies are. A language model can't answer from a set of documents it was never trained on, and you can't paste 600 disclosures into a single prompt without blowing the context window and paying for every token in it. The fix is finding the three most relevant pieces of text and putting only those into the prompt, which is the entire job RAG exists to do.
Chunking: the detail that actually matters
Documents get cut into chunks before anything else happens, and the naive version of this is a short piece of code. The detail that matters most isn't chunk size, it's step size, which is chunk size minus overlap. That subtraction is what makes consecutive chunks overlap instead of sitting edge to edge, and the overlap exists for one reason: so a fact that lands on a chunk boundary doesn't get cut in half. A fixed-size splitter is blind to this; it cuts at a fixed character count whether that lands on a sentence end or in the middle of a word. Recursive splitting instead tries paragraphs first, then lines, then sentences, then words, which produces more chunks for the same settings because it stops at natural boundaries instead of packing text to the limit. In practice, chunk size behaves as a ceiling, not a target, since real documents mix short headers with long paragraphs.
The financial token tax
Before a model reads a chunk, the text becomes tokens, subword units drawn from a fixed vocabulary. A plain sentence like "the cat sat on a mat" tokenizes into six tokens because every word is already in the vocabulary. The same character count of financial text tokenizes far less efficiently. EBITDA alone becomes 13 tokens because the word fractures, and numbers shred the same way. Financial text tokenizes two to three times less efficiently than plain prose, which directly affects cost, context limits, and the risk of silent truncation once a chunk exceeds a model's token limit.
Surprising result 1: a smaller embedding model won
The transformer produces one vector per token, not one vector per chunk, so a seven-token chunk produces seven vectors. Mean pooling averages all of those into a single vector, in this case 384 numbers, that stands in for the entire chunk. Against that backdrop, the first surprising result is that a 384-dimension BGE model beat a 768-dimension MPNet model, 88 percent hit rate versus 76 percent, while using half the storage and building three times faster. Dimension count turns out to be a capacity ceiling, not a quality guarantee. What actually mattered was the training objective: BGE is trained specifically for retrieval using hard negatives, while MPNet is a general-purpose similarity model.
Surprising result 2: mean pooling quietly destroys information
Batching matters for speed, embedding 64 chunks as one batch multiply took roughly a millisecond, twelve times faster than 64 separate multiplications, because the weight matrix loads from memory once and gets reused across all 64 rows rather than because there's less arithmetic. But batch size has a ceiling: a batch of 8 was fastest, while a batch of 128 was slower, because chunks of different lengths all have to pad to the length of the longest one in the batch, and a majority of a short chunk's compute can end up being padding that gets calculated and then thrown away.
The deeper finding is about mean pooling itself. Testing the same fact, "the cost synergies are 22 million," embedded inside surrounding text of different lengths shows cosine similarity dropping from 0.73 at 53 characters of surrounding filler to 0.47 at 1,300 characters, a 35 percent drop, even though the fact itself is present word for word in every version. The averaging step dilutes the signal as filler grows around it. The reason retrieval still works despite this is that the vector isn't the content, it's an address, closer to a card catalog entry than the book itself. The raw text sits untouched next to the vector, and the language model reads that raw text, not the vector, so pooling dilution hurts retrieval, not the final answer, as long as retrieval still finds the right chunk.
A real bug worth knowing about
One structural detail surfaced in testing: a partition operation used to find the top K results guarantees the K largest values land in the last K slots, but it does not sort them against each other. Left unsorted, results come back in ascending order, worst first, which can look completely fine until someone actually checks the numbers. The fix is a single extra sort step applied only to those K results.
Surprising result 3: equal-weight hybrid search made things worse
Two metrics matter for evaluating retrieval quality: hit rate at K, a binary measure of whether the right document appeared anywhere in the top K results, and MRR, which scores how high the right document ranked, a full point for rank one, a half point for rank two, a third for rank three. Fixed and recursive chunking both hit 100 percent on hit rate, but recursive chunking placed the answer at rank one every time, while fixed chunking buried it at rank two twice, a difference hit rate alone completely hides.
That distinction sets up the third surprising result. Combining dense embeddings with BM25 keyword search at equal weight is standard advice, based on the assumption that their failure modes are complementary. In this test, dense-only retrieval scored 76 percent, BM25-only scored 84 percent, and equal-weight hybrid search scored 80 percent, worse than BM25 alone. Averaging a weaker signal with a stronger one drags the stronger one down. Weighting the combination correctly instead, favoring dense embeddings appropriately rather than splitting evenly, pushed MRR to 0.810, the best result of the whole project. The mechanism matters here: hit rate stayed flat at 84 percent as dense weight increased from 0 to 0.3, while MRR climbed from 0.75 to 0.81. Dense retrieval wasn't finding new documents, it was reordering the ones BM25 already found, contributing ranking signal rather than additional recall.
Where the time actually goes
A full latency breakdown reframes where optimization effort should go: embedding the query takes about 40 milliseconds, vector search itself takes about 4 milliseconds, two-tenths of one percent of total latency, ranking takes about 300 milliseconds, and generation takes about 1,300 milliseconds, 320 times the cost of the search step. A cache hit, by contrast, takes about two microseconds, roughly 800,000 times faster than a cold request because it skips the pipeline entirely. The recommended optimization order follows directly from these numbers: cache first, exact match, then semantic match, then route easy queries to a cheaper model, then trim the retrieved context, and only then worry about the vector math, which is the last thing worth touching, not the first.
At larger scale, brute-force comparison against every stored vector eventually breaks down; at 10 million vectors it becomes impractical. Approximate nearest neighbor methods fix that. HNSW builds a layered proximity graph, sparse at the top for long jumps across the space and dense at the bottom for precision, giving logarithmic rather than linear search time. IVF instead uses k-means-style routing, partitioning the vector space and searching only the nearest partition to a query.
Key takeaways
- Chunking overlap (chunk size minus step size) prevents facts from being split across a chunk boundary; recursive splitting that respects natural text boundaries beats naive fixed-size splitting.
- A smaller, retrieval-trained embedding model (384-dimension BGE) can outperform a larger, general-purpose one (768-dimension MPNet) with less storage and faster build time.
- Mean pooling dilutes a fact's signal as surrounding filler text grows, even though the retrieved raw text (not the vector) is what the language model ultimately reads.
- Equal-weight hybrid search can score worse than the better of its two components alone; correctly weighting dense and keyword search instead produced the best result in the project.
- Vector search itself is a tiny fraction of total latency; caching, then exact and semantic matching, then model routing, deliver far more speed improvement than optimizing the vector math.
Try it yourself
Point this same pipeline at your own corpus, sweep the hybrid weight from 0 to 1 against your own validation set, and read where MRR actually peaks rather than assuming it will land at the commonly cited default. This masterclass for the RAG First Principles project comes from the Humanitarians AI Fellows program, presented by Liam on behalf of Ameya Deshmukh.
Chapters
- 0:00Intro: RAG diagrams vs. actual physical measurements
- 0:45Surprising Result 1: Why a 384-dimension model beat a 768-dimension model
- 1:30How chunking overlap prevents splitting facts in half
- 2:15The financial token tax: Why EBITDA and numbers shred
- 3:00Surprising Result 2: How mean pooling destroys up to 35% of information
- 3:50Batch size vs. padding overhead in transformer matrices
- 4:40Surprising Result 3: Why equal-weight hybrid search fails (and how to fix it)
- 5:45Latency optimization: Why vector search is only 0.2% of your performance
Full transcript(auto-generated, with timestamps)
Intro: RAG diagrams vs. actual physical measurements
[0:00]Namast, this is Liam in for Aimia. Build it with Claude, then take it apart. Most RAG explainers show you the diagram. This one shows you the measurements, including three results that contradict the received wisdom. Let's build a retrieval system from scratch and measure every single stage. Three things all measured, all counterintuitive. One, a 384 dimension embedding model beat a 768. Two, mean pooling. The step that turns your text into a vector destroys information. And I'll show you the exact percentage. Three, the standard just use hybrid search advice made retrieval worse than doing nothing. 600 financial disclosures, earnings, risk factors, merger terms. Someone asks, "What are Bluewater Bank's estimated merger cost synergies? The model can't answer those documents aren't in its training data,
Surprising Result 1: Why a 384-dimension model beat a 768-dimension model
[0:45]And you can't paste 600 of them into a prompt. You blow the context window and pay for every token. So instead, find the three most relevant pieces and put only those in. First, cut the documents into chunks. The naive version is 12 lines. One detail matters more than the rest. The step size. It isn't chunk size. It's chunk size minus overlap. That subtraction is what makes chunks overlap instead of sitting edge to edge. Watch it on 10 characters. Window four overlap one. Each window starts three later. So one character repeats at every boundary. That overlap exists for one reason. So a fact that lands on a boundary isn't cut in half. But that splitter is blind. It cuts at 400 characters. Whether that's a sentence end or the middle of a word. Recursive splitting tries paragraphs first then lines then sentences then words. Same
How chunking overlap prevents splitting facts in half
[1:31]Settings more chunks. Eight becomes 10 because it stops at boundaries instead of packing to the limit. The real distribution is by model. Short headers long paragraphs. Chunk size is a ceiling not a target. Before the model reads a chunk. Text becomes tokens. Subword units from a fixed vocabulary. The cat sat on a mat. Six tokens. Each word already in the vocabulary. The same character count of financial text. dollar figures. EBIT toa 13 tokens because ebitita fractures and numbers shred. Financial text tokenizes two to three times less efficiently. That's cost context limits and silent truncation past 384 tokens. Now the tokens go through the model. And here's what most people get wrong. The transformer produces one vector per token, not one per chunk. Seven tokens, seven vectors. Mean pooling averages all
The financial token tax: Why EBITDA and numbers shred
[2:17]Seven into a single vector of 384 numbers. That one vector now stands for the entire chunk. You could embed one chunk at a time. Almost nobody does. 64 separate matrix multiplies 50 milliseconds. One batch multiply 1 millisecond 12 times faster doing the identical 75 million operations. The speed up isn't less math. It's the weight matrix loaded from memory once and reused across all 64 rows. Memory bandwidth is the bottleneck, not arithmetic. But the real system didn't get 12 times. Batch 8 was fastest. Batch 128 was slower than eight because chunks have different lengths and to batch them into one rectangular tensor, they all pad to the longest one. 64% of a short rows compute is padding calculated then thrown away. The attention mask marks
Surprising Result 2: How mean pooling destroys up to 35% of information
[3:01]Which positions are real. Ignore it and the padding corrupts the vector. 1500 vectors, 768 numbers each, 4.4 megabytes. And here's what surprises people. Nothing links a chunk to its vector except position. Chunk 7 corresponds to embedding seven. two parallel lists, no shared ID. The whole system rests on one assumption that encode returns vectors in input order. It does, but if that ever broke, you'd retrieve the right vector and cite the wrong document silently with no error. A question comes in, embed it one vector and compare it against all 1500 with cosine similarity, the angle between vectors, ignoring magnitude. One matrix vector product computes all 1500 at once in optimized C, no Python loop. And if the vectors are pre-normalized, every norm is one and this collapses to a plain dotproduct. Then take the top K. And here's a real bug. Our partition guarantees the K largest land in the
Batch size vs. padding overhead in transformer matrices
[3:51]Last K slots, but it does not sort them against each other. This returned results in ascending order. Worst first and look completely fine until someone checked the numbers. The fix is one line sort just those K. Now the uncomfortable part. Mean pooling is lossy compression. Same fact the cost synergies are 22 million. Same query only the surrounding filler grows 53 characters cosine.73 1300 characters 47 a 35% drop and the fact is word for word present in every one of those chunks it just gets averaged away so how does it still work because the vector isn't the content it's an address a card catalog entry you use it to find the book then you read the actual book the raw text sits right next to the vector untouched and the language model reads that text not the vector dilution hurts retrieval not the answer provided retrieval found the right chunk. You can't improve what you
Surprising Result 3: Why equal-weight hybrid search fails (and how to fix it)
[4:41]Don't measure. Two metrics. Hit rate at K. Was the right document anywhere in the top? K. Binary. MRR. How high was it? Rank one scores one. Rank two a half. Rank three a third. Fixed and recursive chunking both hit 100%. But recursive put the answer at rank one every time. Fixed buried it at rank two twice. Hit rate saturates and hides that. Where the conventional wisdom breaks. Surprise one. The 384 dimension BGE model beat the 768 dimension MPET. 88% hit rate to 76 half the storage three times faster to build. Dimension is a capacity ceiling not a quality guarantee. What matters is the training objective. BGE is trained for retrieval with hard negatives and PNET is general purpose similarity. Surprise 2 hybrid search made it worse. Combine dense embeddings with BM25 keyword search at equal weight. The received wisdom because their failure modes look complimentary. Dense only 76, BM25 only, 84. Equal weight hybrid 80 below BM25 alone. Fusion assumes both retrievers are comparably good. Average and a weaker signal and you drag the stronger one down. Surprise three, weight it
Latency optimization: Why vector search is only 0.2% of your performance
[5:45]Correctly and hybrid wins. MRR810, the best result in the whole project, but watch the mechanism. Hit rate stays flat at 84 from 0 to 0.3. Dense weight MR climbs from 75 to81. Dense retrieval isn't finding new documents. It's reordering the ones BM25 already found. It contributes ranking signal, not recall. Where does the time actually go? Embed the query. 40 milliseconds. Vector search. The part everyone obsesses over. 4 milliseconds, 2/10 of 1% of the total. Ranking 300. Generation 1300 milliseconds, 320 times the cost of the search. If you're optimizing vector math, you're optimizing 2% of the problem. And a cache hit two microsconds 800,000 times faster than a cold request because it skips everything. So optimize in order cache first
Exact then semantic then route easy queries to a cheaper model then trim the retrieved context. The vector math is the last thing worth touching not the first. Everything so far was brute force compare against everything exact but linear and at 10 million vectors it dies. Approximate nearest neighbor fixes it. HNSW builds a layered proximity graph sparse at the top for long jumps, dense at the bottom for precision, logarithmic instead of linear. IVF is just K means routing partition the space search only the nearest partition and storage is chunks times dimensions times four bytes. Which is why having dimensions mattered twice over. Three things to carry away. One, the defaults are frequently wrong. Bigger embeddings lost, equal weight fusion lost,
And both are standard advice. Two, hit rate saturates once it's near 100%. MRR is the metric that still tells you something. Three, vector search is two ten of a percent of your latency. If you're optimizing it before you've added a cache, you're solving the wrong problem. Your turn. Take this prompt. Point the pipeline at your own corpus and sweep the hybrid weight from 0 to one against your own val set. Then read where MRR peaks. It will not be 0.3. It'll be wherever your data lives. Watch whether dense weight adds recall or just reorders. That single sweep is the difference between shipping the default and shipping the right answer. measure. Do not assume the defaults are someone else's corpus.
More from Humanitarians AI Fellows
2:13Why Backtesting on Revised Financial Data is "Look-Ahead Bias"
3:01RAG Silent Failure: The 4 Invisible Gaps Killing Your AI Apps
2:06Why Correlation Doesn't Equal a Tradable Signal (Spurious Correlation)
2:49Why Creativity is a Bug in Financial LLMs (Understanding Temperature)
3:20What Actually Makes an AI "Agentic"? (Cutting Through the Hype)
3:36