00_how_rag_works/
Lesson 00 implements naive keyword search and demonstrates the failure modes that motivate every other lesson in the project. Same query, same documents — but synonyms break it, paraphrasing breaks it, and the only way out is semantic search.
Lesson 00 Foundational 3 sub-lessons Pure JS, no models
📚 What is this & why does it exist?
Before showing the reader the modern stack (embeddings, vector DBs, LLMs), this lesson establishes a baseline they can see fail. It implements traditional keyword retrieval — str.includes() with stop-word filtering — on a small document collection and runs queries that obviously match in meaning but don't share any literal words. The output makes the limitation viscerally obvious in 30 seconds of console output.
💡 Real-world analogy
It's the "before" photo in a renovation pitch. Until you see the cracked tile and chipped paint of keyword search — "user types 'dog', misses every document about 'puppy' or 'canine'" — you don't appreciate why the embedding section exists. The lesson resists the temptation to skip ahead; it makes you live with the broken kitchen for 200 lines so the upgrade feels earned.
🎯 Why This Component
This lesson exists because RAG tutorials that start with "and now embeddings" leave readers with a vague sense that embeddings are better without a concrete sense of why. By implementing the dumb thing first, the reader builds intuition for the gap. When lesson 02 introduces semantic similarity and asks the same query against the same documents, the reader sees synonyms work and has the "aha" moment that justifies the rest of the curriculum.
It's also fast. No model downloads, no GGUF, no GPU; node 00_how_rag_works/01_basic_search.js runs in milliseconds. This is the "kick the tires" lesson — the one a reader can complete on a phone tethered to coffee-shop wifi.
🏗️ Design Rationale
| Decision | Choice | Why |
|---|---|---|
| Pure JS implementation | No dependencies on src/, no model files | The lesson must run anywhere. Asking a learner to download a 2GB model just to see keyword search fail would defeat the point. |
| Hardcoded mini-corpus | 5–10 sentences in an array | Visible at a glance; reader can mentally trace what should match. |
| Stop-word list inline | Tiny array ['the','a','is',...] | Demonstrates the technique without pulling in a heavyweight NLP lib. |
| Three increasingly bad failures | Synonyms → paraphrasing → conceptual | Each sub-lesson tightens the screw. By the end, the reader is begging for embeddings. |
| Plain console output | Numbered ranks, score, snippet | The "aha" moment is in seeing the wrong doc rank #1. Pretty graphs would dilute it. |
Architecture
flowchart LR
Q[query string] --> N[normalize: lowercase + tokenize]
N --> S[remove stop-words]
S --> M[match query terms in documents]
M --> R[rank by hit count or TF-IDF-like score]
R --> O[print top-k]
D[hardcoded docs[]] --> M
style M fill:#3fb95033,stroke:#3fb950
Lifecycle — Why a synonym query fails
sequenceDiagram
participant U as User
participant L as 00_how_rag_works
participant Idx as in-memory docs[]
U->>L: query = "puppy training"
L->>L: tokens = ['puppy','training']
L->>Idx: for each doc, count token hits
Note over Idx: doc1: "Dogs need consistent training"
'training' hits [ok]
'puppy' miss [no] (says 'dogs')
score = 1
Note over Idx: doc2: "Cats are independent"
both miss
score = 0
Note over Idx: doc3: "How to housetrain a puppy"
'puppy' hits [ok]
'training' miss [no] (says 'housetrain')
score = 1
Idx-->>L: tied scores; arbitrary order
L-->>U: prints unhelpful ranking
Note over U: "puppy training" should clearly match
doc1 + doc3 above doc2 — but the
scoring can't see the synonym
Interactions
| From | To | Interface | Why this interaction exists |
|---|---|---|---|
| Reader / instructor | node 00_how_rag_works/01_basic_search.js | CLI | The simplest way to demonstrate failure: run the file. |
Sub-lesson 01 | hardcoded docs[] | direct array access | Keep dependencies invisible — the reader sees only the algorithm. |
Sub-lesson 02 | same docs[], harder query | direct array access | Same data, harder failure — isolates the variable. |
Sub-lesson 03 | add stop-word list | inline filter | Show that even with stop-word filtering, semantics still don't work. |
| Lesson 02 | (implicitly references this) | same query → embeddings → correct ranking | The narrative arc: this lesson sets up the failure that lesson 02 fixes. |
📝 Annotated Code Walkthrough — 01_basic_search.js
Set up the corpus
const documents = [
"Dogs need consistent training and patience.",
"Cats are independent and aloof.",
"How to housetrain a puppy: a beginner's guide.",
"Reinforcement learning trains agents through rewards.",
// ...
];
Hardcoded so the reader can scan it. Note "puppy" appears in one doc and "dog" in another — the synonym test is set up.
Tokenise the query
function tokenize(s) {
return s.toLowerCase().split(/\s+/).filter(Boolean);
}
Whitespace split, lowercased. No stemming, no lemmatisation — the reader sees how dumb the matching is.
Score each document
function score(query, doc) {
const qTokens = tokenize(query);
const dTokens = tokenize(doc);
let hits = 0;
for (const t of qTokens) {
if (dTokens.includes(t)) hits++;
}
return hits;
}
One point per query token that appears literally in the document. No TF-IDF, no IDF; bare hit counting. Replace with anything fancier and the failure mode disappears prematurely.
Rank and print
function search(query, docs) {
return docs
.map((d, i) => ({ doc: d, idx: i, score: score(query, d) }))
.sort((a, b) => b.score - a.score)
.slice(0, 5);
}
const query = "puppy training";
console.log(`Query: "${query}"`);
search(query, documents).forEach((r, rank) => {
console.log(` ${rank + 1}. (score=${r.score}) ${r.doc}`);
});
The output is the punchline: the doc about "Dogs need consistent training" scores the same as "How to housetrain a puppy", and a doc that's clearly about agentic RL ranks above the actual answer because it shares the word "training". Embeddings will fix this in lesson 02.
Deep-Dive Highlights
Why no TF-IDF? Pedagogy
TF-IDF would mask the failure — rare terms get extra weight, sometimes accidentally fixing synonym cases. The lesson's job is to show the floor, not patch it.
The synonym trap Failure Mode
"puppy" and "dog" share zero characters but mean (almost) the same thing. Keyword search has zero awareness; embedding similarity is >0.7.
What lesson 02 fixes Forward Pointer
Re-runs the same queries with cosine similarity over bge embeddings. The ranking flips correctly. The contrast is the curriculum's hook.
⚠ Edge Cases & Boundaries
| Scenario | What Actually Happens |
|---|---|
| Empty query | tokenize returns []; every doc scores 0; output is the first 5 docs unchanged. |
| Query is a stop-word ("the") | Almost every doc matches; ranking is meaningless. Sub-lesson 03 demonstrates this. |
| Multi-word phrase ("machine learning") | Treated as two independent tokens; a doc containing "learning machine" matches the same as "machine learning". |
| All zero scores | Stable JS sort returns docs in original order — misleadingly suggests doc 0 is "the answer". |
| Case sensitivity | Handled by toLowerCase(). Without it, "Puppy" and "puppy" wouldn't match. |
Directory Map
00_how_rag_works/
examples/00_how_rag_works/
├── 01_basic_search.js ← Pure keyword search; demonstrates synonym failure
├── 02_failure_modes.js ← Adds harder paraphrase queries
└── 03_stop_words.js ← Stop-word filter; shows it's not enough
Configuration Reference
| Parameter | Default | Description |
|---|---|---|
| Top-k results | 5 | Hardcoded slice in the search function. |
| Tokenizer | whitespace + lowercase | No stemming. |
| Stop-word list (sub-lesson 03) | ~30 words | Inline array; see source. |
| Documents | ~10 sentences | Hardcoded; modify to test new failure modes. |
| Sample queries | 3-5 per file | Each chosen to trigger a specific failure. |
Code Examples
Run lesson 00
cd examples/00_how_rag_works
node 01_basic_search.js
node 02_failure_modes.js
node 03_stop_words.js
Sample output (synonym failure)
Query: "puppy training"
1. (score=1) Reinforcement learning trains agents through rewards.
2. (score=1) Dogs need consistent training and patience.
3. (score=1) How to housetrain a puppy: a beginner's guide.
4. (score=0) Cats are independent and aloof.
5. (score=0) Embeddings turn text into vectors.
What lesson 02 will produce (semantic, for contrast)
Query: "puppy training"
1. (sim=0.82) How to housetrain a puppy: a beginner's guide.
2. (sim=0.74) Dogs need consistent training and patience.
3. (sim=0.41) Reinforcement learning trains agents through rewards.
4. (sim=0.18) Cats are independent and aloof.
5. (sim=0.12) Embeddings turn text into vectors.