00_how_rag_works/

The motivation lesson — why we need embeddings

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

3
sub-lessons
~250
LOC total
0
model deps
100%
runs offline

📚 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

DecisionChoiceWhy
Pure JS implementationNo dependencies on src/, no model filesThe lesson must run anywhere. Asking a learner to download a 2GB model just to see keyword search fail would defeat the point.
Hardcoded mini-corpus5–10 sentences in an arrayVisible at a glance; reader can mentally trace what should match.
Stop-word list inlineTiny array ['the','a','is',...]Demonstrates the technique without pulling in a heavyweight NLP lib.
Three increasingly bad failuresSynonyms → paraphrasing → conceptualEach sub-lesson tightens the screw. By the end, the reader is begging for embeddings.
Plain console outputNumbered ranks, score, snippetThe "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

FromToInterfaceWhy this interaction exists
Reader / instructornode 00_how_rag_works/01_basic_search.jsCLIThe simplest way to demonstrate failure: run the file.
Sub-lesson 01hardcoded docs[]direct array accessKeep dependencies invisible — the reader sees only the algorithm.
Sub-lesson 02same docs[], harder querydirect array accessSame data, harder failure — isolates the variable.
Sub-lesson 03add stop-word listinline filterShow that even with stop-word filtering, semantics still don't work.
Lesson 02(implicitly references this)same query → embeddings → correct rankingThe 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

ScenarioWhat Actually Happens
Empty querytokenize 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 scoresStable JS sort returns docs in original order — misleadingly suggests doc 0 is "the answer".
Case sensitivityHandled 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

ParameterDefaultDescription
Top-k results5Hardcoded slice in the search function.
Tokenizerwhitespace + lowercaseNo stemming.
Stop-word list (sub-lesson 03)~30 wordsInline array; see source.
Documents~10 sentencesHardcoded; modify to test new failure modes.
Sample queries3-5 per fileEach 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.