av / dives /RAG: Exercises
source about me

Core path - 4 of 8

Exercises: make the learning stick

Reading code teaches you less than predicting what it will do and then checking. This file turns each section of the README into a few quick active-recall prompts: a thing to predict, a thing to change, and a question to answer from memory.

How to use it: work the section first, then come back. Commit to an answer before you run or reveal. The prediction is where the learning happens, even (especially) when you're wrong. Answers are hidden behind ▸ toggles.

The chunking exercises are (offline): no API call, no cost. The rest make small, cheap calls.


Section 2: Embeddings (recap)

Recall. Two sentences share no words. Can they still score a high cosine similarity? Why does that matter for retrieval?

▸ Answer

Yes. Embeddings capture meaning, not spelling, so "get my notes out" lands near "export to Markdown." That's the whole basis of retrieval: you can find the right passage even when the user's words don't appear in it.


Section 3: Chunking (offline)

Predict, then run. In examples/02_chunking.py, you chunk the same document at size 40 and size 300. Which produces more chunks? Which produces more focused ones?

▸ Answer

Size 40 produces many more, smaller, tightly-focused chunks; size 300 produces a few broad ones. More focused isn't automatically better, since a too-small chunk may not contain enough to answer on its own. That tension is the whole reason chunk size is a tunable knob.

Do (offline). Set overlap equal to chunk_size in a call to chunk_text. What happens, and why does the code forbid it?

▸ Answer

It raises ValueError. With overlap == size the window would never advance (step = size - overlap = 0), so it'd loop forever. Overlap must be smaller than the chunk so the window slides forward while still sharing a boundary.


Section 4: The vector store

Recall. VectorStore.search is described as "brute force." What does it actually do for each query, and why is that fine here but not at a billion vectors?

▸ Answer

It computes cosine similarity against every stored vector, then sorts, so O(n) per query. For hundreds or thousands of chunks that's instant. At millions/billions you switch to an approximate-nearest-neighbour index (FAISS, hnswlib) or a vector database; same idea, cleverer data structure.


Section 5: The RAG pipeline

Predict, then run. Ask examples/04_rag_pipeline.py something the corpus does NOT cover (e.g. "Who founded the company?"). What should a well-grounded system do?

▸ Answer

It should say it doesn't know, rather than invent an answer. That's the job of the grounding instruction (GROUNDED_SYSTEM): answer only from the provided context. Hallucinating a confident wrong answer is the failure RAG is meant to prevent.

Do. Open rag/pipeline.py and read build_prompt. Why are the retrieved chunks numbered [1] [2] ...?

▸ Answer

So the model can cite them: "[2]" in the answer maps back to a real source the reader can check. Numbering the context is what makes citations possible.


Section 6: Chunk size & retrieval quality

Do. In examples/05_chunk_size.py, the same query is run against small and large chunks. Pick a query where you'd expect them to differ a lot, change it, and see. When might large chunks retrieve worse?

▸ Answer

When a large chunk bundles several topics, an off-topic sentence can pull it up (or push the right chunk down) in the ranking, and you spend context-window space on irrelevant text. Small chunks are more precise but can fragment an answer. The only way to know for your data is to measure (Section 10).


Section 7: Keyword search, BM25 (offline)

Predict, then run. examples/06_keyword_search.py scores chunks for "what does error NN-413 mean?" on words alone, no embeddings. Which of BM25's three ingredients (term frequency, IDF, or length normalization) does the most work in ranking the right chunk first?

▸ Answer

IDF. "nn-413" appears in just one chunk, so its inverse-document-frequency weight is huge, so one match is decisive. Term frequency barely matters (the code occurs once) and length only breaks ties. That's BM25's whole edge over counting shared words: rare terms are worth far more than common ones. And it's all offline: keyword search needs no model, no API call, no cost.


Section 8: Hybrid retrieval

Predict, then run. examples/07_hybrid_retrieval.py asks "what does error NN-413 mean?" Which scorer, vector or keyword, do you expect to nail it, and why?

▸ Answer

Keyword. An exact code like "NN-413" carries little semantic meaning for the embedding model, but a keyword match finds it instantly. The paraphrase query ("get my notes out") is the reverse: vectors win. Hybrid combines both, which is why production retrieval is usually hybrid.


Section 9: Reranking

Recall. Reranking re-scores only the top ~8 chunks, not the whole corpus. Why is it affordable to use a slower, smarter method at that stage?

▸ Answer

Because you've already narrowed millions/thousands down to a handful with cheap vector search. A method too slow to run over everything is fine over 8 candidates. That's the point of a two-stage retrieve-then-rerank pipeline.


Section 10: Evaluation

Recall. What's the difference between "hit rate @ k" and "MRR," and why track both?

▸ Answer

Hit rate @ k asks did the right chunk appear in the top k at all? MRR asks how high? (1/rank of the first correct hit). Two systems can have the same hit rate but different MRR, and the one that ranks the right chunk #1 instead of #4 gives the model better context. Retrieval can also succeed while the answer is still wrong, which is why the example measures answer correctness too.

Do. Change K in examples/09_evaluation.py from 4 to 1, then to 8. What happens to the hit rate, and what's the catch with just cranking k up?

▸ Answer

Hit rate usually rises with k (more chances to include the right chunk). But every extra chunk costs context-window space and tokens, dilutes the prompt with less-relevant text, and can lower answer quality. Bigger k is not free. It's a tradeoff you measure, not maximize.


Going further: more retrieval techniques

Recall (query transformation, 10). HyDE embeds a hypothetical answer instead of the question. Why does that help retrieval?

▸ Answer

A question and its answer often share few words, so the question's vector sits far from the passage that answers it. A drafted answer lives in "answer space", much closer to the real passage, so embedding it pulls the right chunk up the ranking. Multi-query gets there differently: more phrasings = more chances to match.

Predict (contextual retrieval, 11). The example embeds context + chunk but stores only chunk. Why embed one thing and show the model another?

▸ Answer

The prepended context exists only to make the embedding findable (it carries the "which document / what plan" words a bare chunk lacks). The model should still read the clean chunk, not the synthetic context, so you embed the augmented text but store and display the original.

Recall (metadata & parent-doc, 12). Name one relevance reason and one security reason to filter retrieval by metadata. And what tension does small-to-big resolve?

▸ Answer

Relevance: only search the docs that could answer (e.g. billing). Security: never return a doc this user isn't allowed to see. Small-to-big resolves the chunk-size tension: small chunks match precisely, but you return the parent so the model reads complete context.

Do (chunking strategies, 13). The first part runs offline. Why does splitting on Markdown headings beat a blind word-window, and what's the one thing it doesn't fix?

▸ Answer

Heading-split sections are each about one topic, so a chunk doesn't straddle two ideas the way a fixed window can (that merged "Import/Export" chunk that tripped up hybrid search in Section 8). The heading also becomes metadata you can filter on and cite ("Billing > Refunds"). What it doesn't fix: a vocabulary gap. If the query says "get my notes out" and the doc says "export," better chunking can't connect them. That's query transformation's job (10).

Predict (ingestion, 14). Real corpora are PDFs and HTML, not tidy Markdown. What single shape does every parser reduce a document to, so the rest of the pipeline never has to care about the original format?

▸ Answer

(source, text): clean text plus where it came from. HTML through an stdlib parser, PDFs through pdfplumber/pypdf, Word through python-docx: each just produces text, which then flows through the same heading-split -> embed -> retrieve path as everything else. Ingestion is format-in, (source, text)-out.

Predict (approximate index, 15). The IVF index scans only the clusters nearest the query. Set n_probe=1 (one cluster) and it scans ~2% of the vectors. Will recall be near-perfect, near-zero, or somewhere in between, and why?

▸ Answer

In between (~0.76 on the run). Because the data clusters, most of a query's true neighbours live in its single nearest cluster, so one probe already finds the bulk of them. But some genuine neighbours sit just over a cluster border, in the second nearest bucket you didn't scan, so you miss them. That miss rate is recall, and it climbs fast as you probe more (~0.97 at ~7% scanned). If the vectors had no cluster structure (uniform noise), one probe would miss almost everything. ANN only works because real embeddings cluster.

Recall. When should you not reach for an approximate index?

▸ Answer

When brute force is fast enough, which, for the few thousand chunks in a normal corpus, it is. ANN adds a recall risk and a dial to tune (against an eval, §10) in exchange for speed you may not need. It earns its place at millions of vectors, not before. Reach for store.py first.


Capstone: ask_docs.py

Do. Run secrun python hands_on/ask_docs.py once, then again. The second run is much faster. Why? Then add a fact to a file in corpus/, run with --rebuild, and ask about your new fact.

▸ Answer

The first run embeds the corpus and caches the index to .rag_index.json; later runs load it instead of re-embedding (embedding is the slow, paid step). Editing the corpus means the cache is stale, so --rebuild re-embeds. The cache also auto- rebuilds if you change provider or chunk settings, since vectors aren't comparable across embedding models.

Stretch. Ask a question, then run again with --show-context and read the chunks the answer was built from. Do the citations [n] in the answer actually match the right sources? This is how you audit a RAG system for trust.


Where to take it next

Invent your own. Pick a real folder of your own notes or docs, drop them in corpus/, --rebuild, and ask. The moment it answers something only your documents know, with a citation you can check, RAG has clicked.