# Les 15 — RAG + Embeddings
## Slide Overzicht (Klas A — 3 uur fysiek, demo-driven)

**Lesvorm:** Tim demonstreert klassikaal. Studenten kijken. Zelf bouwen = huiswerk.
**Demo-app:** Nieuwe PDF Q&A app from scratch
**Vervolg op:** Les 14 — Agents
**Aansluit op:** Les 16 — MCP servers

---

## Slide 1: Title
### Les 15 — RAG + Embeddings

**Visual:**
- Background: CREAM
- "Les 15" in BLUE
- "RAG + Embeddings" in BLACK
- Subtitle: "AI laten antwoorden op basis van jouw eigen documenten"

---

## Slide 2: Terugblik
### Waar staan we?

**Vorige lessen:**
- Les 12: Tool Calling — AI kiest welke functie
- Les 13: Agents — autonoom 20-50 stappen
- Les 15: Externe APIs + Vercel deploy

**Het probleem dat we nu oplossen:**

AI weet veel, maar weet NIET wat in jouw documenten staat:
- Interne kennisbank
- Klantcontracten
- Productdocumentatie
- 200-pagina PDF die vandaag binnenkwam

**Twee opties:**
- ❌ Alle 200 pagina's elke vraag meesturen — niet schaalbaar
- ✅ **RAG** — alleen de relevante stukjes ophalen per vraag

**Visual:** Pijl van "alle docs context" naar "RAG → top 5 chunks".

---

## Slide 3: Planning
### Vandaag — 180 minuten

| Onderwerp | Duur |
|-----------|------|
| Welkom + Terugblik | 10 min |
| Theorie: Wat is een embedding? | 20 min |
| Theorie: RAG pipeline (index + query) | 15 min |
| Theorie: pgvector + chunking | 15 min |
| **Live Demo 1** — pgvector setup + embed pipeline | 25 min |
| **Live Demo 2** — PDF upload + index | 20 min |
| **Pauze** | 15 min |
| **Live Demo 3** — Query pipeline + AI antwoord | 20 min |
| **Live Demo 4** — RAG-tool in een agent | 15 min |
| Wanneer RAG wel/niet | 10 min |
| Lesopdracht + Huiswerk | 10 min |
| Vragen + Afsluiting | 5 min |

---

## Slide 4: Wat is een embedding?
### Tekst als een punt in de ruimte

**Het idee:**
- Stop een stuk tekst in een embedding-model
- Krijg een **vector** terug — array van ~1536 nummers
- Vergelijkbare betekenissen = vergelijkbare vectors

```
"De kat zit op de mat"      → [0.12, -0.45, 0.88, ...]
"Een poes ligt op het tapijt" → [0.14, -0.41, 0.85, ...]
"Voetbal in Nederland"        → [-0.73, 0.21, -0.32, ...]
```

Eerste twee zijn **dichtbij in vector-space**. Derde is ver weg. Niet omdat woorden overlappen — omdat **betekenis** vergelijkbaar is.

**Welk model:**
- OpenAI `text-embedding-3-small` — 1536 dim, snel + goedkoop
- OpenAI `text-embedding-3-large` — 3072 dim, beter maar duurder
- Open-source: `nomic-embed-text`, `mxbai-embed-large`

**Visual:** 2D plot met 3 punten — twee dichtbij, één ver weg.

---

## Slide 5: Vector similarity
### Hoe meet je 'dichtbij'?

**Drie metrics:**

| Metric | Wat | Wanneer |
|--------|-----|---------|
| **Cosine similarity** | Hoek tussen vectors (–1 tot 1) | Default — werkt op semantic search |
| **Dot product** | Sum van producten | Sneller — als vectors genormaliseerd zijn |
| **Euclidean distance** | Afstand in ruimte | Zelden voor text — meer voor images |

**Cosine similarity = 1** → exact gelijke betekenis
**Cosine similarity = 0** → ongerelateerd
**Cosine similarity = -1** → tegengesteld

```sql
-- In pgvector
SELECT * FROM chunks
ORDER BY embedding <=> '[0.12, -0.45, ...]'::vector
LIMIT 5;
```

`<=>` is cosine distance. Sorteren op `<=>` = dichtstbijzijnde eerst.

**Visual:** Drie vectors met hoeken — kleine hoek = similar.

---

## Slide 6: RAG pipeline
### Index time vs query time

**Index time (eenmalig of bij upload):**

```
PDF / docs
  ↓
Parse + chunk (500 tokens per chunk)
  ↓
Embed elke chunk
  ↓
Store: chunk_text + embedding in pgvector
```

**Query time (elke vraag):**

```
User vraag
  ↓
Embed vraag
  ↓
Similarity search → top 5 chunks
  ↓
Geef chunks als context aan LLM
  ↓
LLM antwoordt op basis van context
```

**Belangrijkste inzicht:** AI ziet nooit de hele DB. Alleen 5 relevante chunks per vraag.

**Visual:** Twee parallelle flow-diagrammen — index vs query.

---

## Slide 7: pgvector
### Postgres met vector-support

**Wat is het:**
- Postgres extension — voegt `vector` datatype toe
- Cosine, euclidean, dot product operators
- Schaalbaar tot ~1M+ vectors per tabel
- Ingebouwd in Supabase — één click activeren

**Schema voorbeeld:**

```sql
create extension if not exists vector;

create table chunks (
  id          bigserial primary key,
  source      text not null,
  page        int,
  content     text not null,
  embedding   vector(1536),
  created_at  timestamp default now()
);

create index on chunks
  using hnsw (embedding vector_cosine_ops);
```

`hnsw` index = snelle approximate nearest neighbor search. Voor production met >10k rows.

---

## Slide 8: Chunking strategieën
### Hoe knip je een document op?

**Drie aanpakken:**

| Strategie | Hoe | Wanneer |
|-----------|-----|---------|
| **Fixed size** | 500 tokens per chunk, 50 overlap | Default, simpelste |
| **Recursive** | Splits op paragraaf → zin → woord | Behoudt structuur |
| **Semantic** | Embed zinnen, group similar | Beste kwaliteit, duurder |

**Wat is een goede chunk:**
- ~200-500 tokens (~1000-2500 chars)
- Genoeg context maar niet te groot
- **Overlap** (10-15%) zodat info aan grenzen niet verdwijnt

**Te kleine chunks** → fragmentatie, AI mist context
**Te grote chunks** → ruis, irrelevant info verdunt het antwoord

**Visual:** Document opgeknipt in chunks met overlap.

---

## Slide 9: Wat we vandaag bouwen
### PDF Q&A from scratch

**Doel:** App waar je een PDF uploadt en daarna vragen kunt stellen over de inhoud.

**Stack:**
- Next.js 16 + TypeScript + Tailwind
- Supabase + pgvector
- AI SDK `embedMany` + `generateText`
- OpenAI `text-embedding-3-small`
- `unpdf` voor PDF parsing

**Twee endpoints:**

| Route | Wat | Wanneer |
|-------|-----|---------|
| `POST /api/index` | PDF upload → chunks → embeddings | Eén keer per document |
| `POST /api/ask` | Vraag → embed → search → answer | Per vraag |

**Bonus:** RAG-tool in een ToolLoopAgent — combineer met Les 13.

**Visual:** Index + query pipelines uitgelegd in 1 diagram.

---

## Slide 10: LIVE DEMO 1 — pgvector + embed pipeline
### ~25 min

**Wat ik laat zien:**
1. `pnpm create next-app pdf-qa` + `pnpm add ai @ai-sdk/openai @supabase/supabase-js unpdf`
2. **Supabase:** activeer `pgvector` extension (één SQL command)
3. Schema: `chunks` tabel met `vector(1536)` kolom + HNSW index
4. `lib/embeddings.ts`:
   ```typescript
   import { embed, embedMany } from "ai";
   import { openai } from "@ai-sdk/openai";

   const model = openai.textEmbeddingModel("text-embedding-3-small");

   export async function embedText(text: string) {
     const { embedding } = await embed({ model, value: text });
     return embedding;
   }
   ```
5. Test: `console.log` een embedding — array van 1536 nummers tussen -1 en 1

**Visual:** Console-log met vector + Supabase tabel.

---

## Slide 11: LIVE DEMO 2 — PDF upload + index
### ~20 min

**Wat ik laat zien:**
1. `app/api/index/route.ts` — POST endpoint accepteert file upload
2. `unpdf`: extract text per page
3. **Chunking**: split text in ~500 char chunks met 50 char overlap
4. `embedMany`: batch embed alle chunks (sneller dan loop)
5. Insert in Supabase met `embedding` als vector

```typescript
const chunks = chunkText(fullText, 500, 50);
const { embeddings } = await embedMany({ model, values: chunks });
await supabase.from("chunks").insert(
  chunks.map((c, i) => ({
    source: filename,
    content: c,
    embedding: embeddings[i],
  }))
);
```

6. Demo: upload Polderfest 2027 line-up PDF — 30 chunks in DB
7. Check Supabase tabel — chunks zichtbaar, embedding kolom gevuld

---

## Slide 12: Pauze
### 15 minuten

---

## Slide 13: LIVE DEMO 3 — Query pipeline
### ~20 min

**Wat ik laat zien:**
1. `app/api/ask/route.ts` — POST endpoint accepteert vraag
2. Embed vraag (zelfde model als index!)
3. Supabase RPC of raw SQL: cosine similarity search

```typescript
const { embedding } = await embed({ model, value: question });
const { data: chunks } = await supabase.rpc("match_chunks", {
  query_embedding: embedding,
  match_count: 5,
});

const context = chunks.map((c) => c.content).join("\n\n");
const { text } = await generateText({
  model: openai("gpt-4o-mini"),
  prompt: `Beantwoord op basis van deze context:\n\n${context}\n\nVraag: ${question}`,
});
```

4. Supabase function `match_chunks` definiëren (SQL):

```sql
create function match_chunks(query_embedding vector(1536), match_count int)
returns table (content text, similarity float)
language sql as $$
  select content, 1 - (embedding <=> query_embedding) as similarity
  from chunks
  order by embedding <=> query_embedding
  limit match_count;
$$;
```

5. Test: vraag "Wie zijn de headliners?" → AI geeft accuraat antwoord op basis van PDF

---

## Slide 14: LIVE DEMO 4 — RAG-tool in een agent
### ~15 min

**Wat ik laat zien:**

Combineer Les 13 (Agents) + Les 15 (RAG):

```typescript
import { ToolLoopAgent, tool, stepCountIs } from "ai";

const ragSearch = tool({
  description: "Zoek in geüploade documenten",
  inputSchema: z.object({ query: z.string() }),
  execute: async ({ query }) => {
    const { embedding } = await embed({ model, value: query });
    const { data } = await supabase.rpc("match_chunks", {
      query_embedding: embedding,
      match_count: 5,
    });
    return data;
  },
});

const docAgent = new ToolLoopAgent({
  model: openai("gpt-4o-mini"),
  system: "Je beantwoordt vragen door eerst in documenten te zoeken.",
  tools: { ragSearch },
  stopWhen: stepCountIs(10),
});
```

**Demo vraag:** "Vergelijk de jazz en rock-headliners op Polderfest" — agent doet 2-3 RAG searches, vergelijkt, antwoordt.

**Krachtig:** RAG + Agent = AI met geheugen + redenering.

---

## Slide 15: Wanneer RAG wel/niet?
### Niet alles is RAG

**Wanneer WEL RAG:**
- Veel documenten (>50 pagina's totaal)
- Documenten veranderen (klantcontracten, kennisbank)
- Semantic search nodig (niet alleen keyword)
- Privacy: data blijft in jouw DB

**Wanneer NIET RAG:**
- Klein document (<10 pagina's) — gewoon meesturen
- Exacte data (prijzen, IDs) — gebruik tool-calls / DB query
- Structured data (tabellen, records) — SQL is beter
- 1 keer per dag — overhead niet de moeite

**Alternatieven:**

| Probleem | Beter dan RAG |
|----------|---------------|
| 5-pagina manual | Volledige tekst in system prompt |
| 10k product records | SQL tool-calls (Les 12) |
| Live data | API tool-call |
| Code-base navigatie | Tree-sitter / grep, geen embeddings |

**Hybrid is vaak best:** semantic search + keyword filter.

---

## Slide 16: Lesopdracht + Huiswerk
### PDF Q&A app deployen

**Lesopdracht (in-class, 30 min):**
- PDF Q&A app opzetten (volg demo)
- pgvector in Supabase actief
- 1 PDF indexed
- 3 vragen werkend

**Huiswerk (voor Les 16):**
- **A:** Eigen PDF (interesse, studie, hobby) — minimaal 20 pagina's
- **B:** UI met chat-interface (`useChat` van Les 12)
- **C:** RAG-tool in een ToolLoopAgent
- **D:** `RAG.md` met:
  - Beschrijving van je PDF + chunks count
  - 5 vragen + antwoorden + welke chunks werden opgehaald
  - 1 vraag waar RAG het **fout** had — analyse waarom
  - Eén chunking-strategie geprobeerd + resultaat

**Bonus:**
- Hybrid search (semantic + keyword via Postgres full-text)
- Re-ranking met `cohere-rerank` of LLM
- Streaming antwoord met source-citations

---

## Slide 17: Volgende les + Afsluiting
### Vragen?

**Vandaag gezien:**
- Embeddings — tekst als vectors in semantic space
- Cosine similarity voor 'dichtbij'
- RAG pipeline — index time vs query time
- pgvector in Supabase met HNSW index
- Chunking strategieën
- RAG-tool in een agent

**Volgende les (Les 16): MCP — Model Context Protocol**
- Agent = LLM in een loop met tools
- ToolLoopAgent — system, tools, stopWhen, prepareStep
- 4 stop-condities: stepCountIs, hasToolCall, isLoopFinished, custom
- Combo: RAG-tool in een agent (combo Les 15 + 15)

**Daarna in deze leerlijn:**
- Les 16: MCP — eigen Model Context Protocol server bouwen
- Les 17: Externe APIs in diepte (OAuth, webhooks, paid APIs)
- Les 18: Supabase Auth + RLS — multi-user apps

**Vragen? Feedback?**

---

## Slide Summary

| # | Title | Type |
|---|-------|------|
| 1 | Title | Opening |
| 2 | Terugblik | Recap |
| 3 | Planning | 180-min |
| 4 | Wat is een embedding | Theorie |
| 5 | Vector similarity | Theorie |
| 6 | RAG pipeline | Theorie |
| 7 | pgvector | Theorie |
| 8 | Chunking strategieën | Theorie |
| 9 | Wat we bouwen | Intro demo |
| 10 | **LIVE DEMO 1** — pgvector + embed | Demo |
| 11 | **LIVE DEMO 2** — PDF upload + index | Demo |
| 12 | Pauze | Break |
| 13 | **LIVE DEMO 3** — Query pipeline | Demo |
| 14 | **LIVE DEMO 4** — RAG-tool in agent | Demo |
| 15 | Wanneer RAG wel/niet | Reflectie |
| 16 | Lesopdracht + Huiswerk | Praktijk |
| 17 | Afsluiting + Les 16 preview | Closing |

---

## Bronnen

- **AI SDK Embeddings:** https://ai-sdk.dev/docs/ai-sdk-core/embeddings
- **OpenAI Embeddings:** https://platform.openai.com/docs/guides/embeddings
- **pgvector:** https://github.com/pgvector/pgvector
- **Supabase pgvector:** https://supabase.com/docs/guides/database/extensions/pgvector
- **unpdf:** https://github.com/unjs/unpdf
- **Anthropic — contextual retrieval:** https://www.anthropic.com/news/contextual-retrieval
- **LlamaIndex RAG concepts:** https://docs.llamaindex.ai/en/stable/getting_started/concepts/
