Module 5.2 — Chunking

How you split documents determines how good your RAG system is


Start With a Simple Question

You have a 100-page PDF. You want to build a RAG system on it.

You already know you can't dump the whole PDF into the LLM prompt — too many tokens, too expensive, context window too small.

So you need to split it into smaller pieces first. These smaller pieces are called chunks.

But here's the question nobody thinks about carefully enough:

How do you split it?

This seems like a boring technical detail. It's not. It's one of the most important decisions in your entire RAG system.

The wrong chunking strategy = poor search results = wrong answers = broken RAG system.

Let's understand why.


Why Chunking Matters So Much

Think about how vector search works.

You embed a chunk → store it → later search for similar chunks using a query vector.

The chunk needs to be:

Small enough  → focused on one topic → embeds well → found precisely
Large enough  → contains enough context → LLM can understand it

Two problems with bad chunking:


Problem 1 — Chunks too small:

Original text:
"Aspirin reduces fever and pain. However patients with 
stomach ulcers should avoid it as it can cause bleeding. 
Always consult a doctor before use."

Split into tiny chunks:
Chunk A: "Aspirin reduces fever and pain."
Chunk B: "However patients with stomach ulcers should avoid it"
Chunk C: "as it can cause bleeding."
Chunk D: "Always consult a doctor before use."

User asks: "Is aspirin safe for someone with stomach ulcers?"

Vector search finds: Chunk B (best match)
→ "However patients with stomach ulcers should avoid it"

LLM gets: Only Chunk B
LLM says: "Patients with stomach ulcers should avoid it"
→ Missing the reason (bleeding) ❌
→ Missing the "consult doctor" advice ❌
→ Incomplete answer

Too small = loses context = incomplete answers.


Problem 2 — Chunks too large:

Original text: entire chapter about medications
→ 2000 words about aspirin, ibuprofen, paracetamol, 
  codeine, morphine, and 20 other drugs

Stored as ONE chunk

User asks: "What are the side effects of aspirin?"

Vector search finds: this chunk (it mentions aspirin)
But also mentions 20 other drugs

LLM gets: 2000 words — most of it irrelevant
→ LLM gets confused by irrelevant information
→ Answer quality drops
→ More tokens = more expensive
→ Might miss the specific aspirin info buried in text

Too large = noisy context = confused LLM = worse answers.


The Goldilocks Zone

Too small          Just right           Too large
────────────       ──────────────       ──────────────
loses context      focused + complete   noisy + expensive
bad answers        good answers         confused LLM

~50 words          ~200-500 words       ~2000+ words

For most use cases — 200 to 500 words per chunk is the sweet spot.


Chunking Strategy 1 — Fixed Size Chunking

What it is

Split the document every N characters or N words — no matter what.

Document:
"The quick brown fox jumps over the lazy dog. 
 The dog did not move. The fox ran away quickly.
 Later that day, the fox returned to find..."

Fixed size chunking at 50 characters:
Chunk 1: "The quick brown fox jumps over the lazy dog. The"
Chunk 2: " dog did not move. The fox ran away quickly.\n La"
Chunk 3: "ter that day, the fox returned to find..."

Simple. Fast. But has one obvious problem — it cuts mid-sentence.

Chunk 1 ends with "The" — incomplete sentence. This loses meaning.


The Solution — Chunk Overlap

To fix mid-sentence cuts — you add overlap.

Overlap means the end of one chunk is repeated at the start of the next chunk.

Document: "A B C D E F G H I J K L M N O"
Chunk size: 6 words
Overlap:   2 words

Chunk 1: A B C D E F
Chunk 2:         E F G H I J    ← E and F repeated from chunk 1
Chunk 3:                 I J K L M N
Chunk 4:                         M N O

Now even if a sentence gets split — the overlapping words give context to both chunks.

Real example:

Chunk size: 500 characters
Overlap:    100 characters

Chunk 1: "...Aspirin reduces fever and pain. It works by blocking
          prostaglandins which are chemicals that cause inflammation.
          The recommended dose for adults is 325mg to 650mg every"

Chunk 2: "The recommended dose for adults is 325mg to 650mg every
          4 to 6 hours. Do not exceed 4000mg in 24 hours. Patients
          with stomach ulcers should avoid aspirin because..."

"The recommended dose..." appears in both chunks — giving context to each.


Fixed Chunking in Code


    function fixedChunking(text, chunkSize = 500, overlap = 100) {
        // text      = full document text as one string
        // chunkSize = how many characters per chunk (default 500)
        // overlap   = how many characters to repeat between chunks (default 100)
        // example: text = "Aspirin reduces fever..."

        const chunks = [];
        // chunks = array that will hold all our text pieces
        // example final value: ["Aspirin reduces...", "...reduces fever and pain..."]

        let startIndex = 0;
        // startIndex = where current chunk starts in the full text
        // begins at 0 (start of document)

        while (startIndex < text.length) {
            // keep going until we've covered the whole document

            let endIndex = startIndex + chunkSize;
            // endIndex = where current chunk ends
            // example: startIndex=0, chunkSize=500 → endIndex=500

            if (endIndex > text.length) {
                endIndex = text.length;
                // don't go past the end of the document
            }

            // Try to end at a sentence boundary — not mid-word
            // Look for the last period, question mark, or newline before endIndex
            if (endIndex < text.length) {
                const lastPeriod = text.lastIndexOf(".", endIndex);
                const lastNewline = text.lastIndexOf("\n", endIndex);
                const lastBoundary = Math.max(lastPeriod, lastNewline);
                // Math.max = take whichever boundary is further in the text

                if (lastBoundary > startIndex + (chunkSize * 0.5)) {
                    endIndex = lastBoundary + 1;
                    // only use this boundary if it's at least halfway through the chunk
                    // prevents very tiny chunks
                    // +1 to include the period itself
                }
            }

            const chunk = text.slice(startIndex, endIndex).trim();
            // slice = extract substring from startIndex to endIndex
            // trim = remove leading/trailing whitespace
            // example: "  Aspirin reduces fever...  " → "Aspirin reduces fever..."

            if (chunk.length > 0) {
                chunks.push(chunk);
                // add this chunk to our array
            }

            startIndex = endIndex - overlap;
            // move startIndex forward — but go back by overlap amount
            // example: endIndex=500, overlap=100 → next startIndex=400
            // so next chunk starts 100 characters before this one ended
            // this creates the overlap between chunks
        }

        return chunks;
        // returns array of text strings
        // example: ["Aspirin reduces fever...", "...fever and pain. Ibuprofen..."]
    }

    // ── USAGE EXAMPLE ────────────────────────────────────────

    const sampleDocument = `
        Aspirin is commonly used to reduce fever, pain, and inflammation.
        It works by blocking prostaglandins — chemicals that cause pain signals.
        The recommended adult dose is 325mg to 650mg every 4-6 hours.
        Do not exceed 4000mg in 24 hours without medical supervision.

        Ibuprofen is a nonsteroidal anti-inflammatory drug (NSAID).
        It is effective for headaches, dental pain, menstrual cramps, and arthritis.
        Common side effects include stomach upset and kidney stress with long term use.
        Always take ibuprofen with food to reduce stomach irritation.

        Paracetamol is used for mild to moderate pain and fever.
        Unlike aspirin and ibuprofen, it does not reduce inflammation.
        It is generally safe for most people including pregnant women when used correctly.
        Overdose is dangerous and can cause permanent liver damage.
        `;

    const chunks = fixedChunking(sampleDocument, 300, 50);
    // chunkSize = 300 characters
    // overlap   = 50 characters

    console.log(`Total chunks created: ${chunks.length}\n`);

    chunks.forEach((chunk, index) => {
        console.log(`Chunk ${index + 1} (${chunk.length} chars):`);
        console.log(chunk);
        console.log("".repeat(50));
    });

Output will look like:

Total chunks created: 4

Chunk 1 (298 chars):
Aspirin is commonly used to reduce fever, pain, and inflammation.
It works by blocking prostaglandins — chemicals that cause pain signals.
The recommended adult dose is 325mg to 650mg every 4-6 hours.
──────────────────────────────────────────────────

Chunk 2 (285 chars):
every 4-6 hours.
Do not exceed 4000mg in 24 hours without medical supervision.
Ibuprofen is a nonsteroidal anti-inflammatory drug (NSAID).
It is effective for headaches, dental pain, menstrual cramps...
──────────────────────────────────────────────────

Notice — "every 4-6 hours" appears at the end of chunk 1 and start of chunk 2. That's the overlap working.


Chunking Strategy 2 — Semantic Chunking

What it is

Instead of splitting by character count — split by meaning.

Keep sentences together that belong together. Split when the topic changes.

Document about medications:

Paragraph 1: About aspirin — fever, pain, dosage
Paragraph 2: About ibuprofen — inflammation, kidneys
Paragraph 3: About paracetamol — liver, overdose

Semantic chunking:
Chunk 1 = everything about aspirin (complete idea)
Chunk 2 = everything about ibuprofen (complete idea)
Chunk 3 = everything about paracetamol (complete idea)

Each chunk = one complete topic = embeds perfectly

This is better than fixed chunking because:

Fixed chunking might cut:
"...aspirin is good for fever. Ibuprofen is better for inf..."
→ Half aspirin, half ibuprofen in one chunk
→ Confusing embedding — what is this chunk even about?

Semantic chunking keeps:
"Aspirin is good for fever and pain. Dosage is 325mg..."
→ One complete idea
→ Clean embedding — clearly about aspirin
→ Found precisely when asked about aspirin

Simple Semantic Chunking — Split by Paragraph

The simplest form of semantic chunking — split on double newlines (paragraph breaks):


    function paragraphChunking(text, maxChunkSize = 1000) {
        // text         = full document text
        // maxChunkSize = if a paragraph is too long, split it further

        const paragraphs = text.split(/\n\s*\n/);
        // split on double newlines (paragraph breaks)
        // \n\s*\n = newline, optional whitespace, newline
        // example: "Para 1\n\nPara 2" → ["Para 1", "Para 2"]

        const chunks = [];
        let currentChunk = "";
        // currentChunk = text we're building up

        paragraphs.forEach((paragraph) => {
            const cleaned = paragraph.trim();
            // remove extra whitespace from paragraph edges

            if (cleaned.length === 0) return;
            // skip empty paragraphs

            if ((currentChunk + cleaned).length > maxChunkSize) {
                // adding this paragraph would make chunk too large

                if (currentChunk.length > 0) {
                    chunks.push(currentChunk.trim());
                    // save current chunk
                    currentChunk = "";
                    // start fresh
                }
            }

            currentChunk += cleaned + "\n\n";
            // add paragraph to current chunk
        });

        if (currentChunk.trim().length > 0) {
            chunks.push(currentChunk.trim());
            // save the last chunk
        }

        return chunks;
    }


Advanced Semantic Chunking — Using Embeddings

The most powerful form — actually measure semantic similarity between sentences and split when meaning changes significantly:


    async function semanticChunking(sentences, openai, threshold = 0.7) {
        // sentences = array of individual sentences
        // threshold = if similarity drops below this — start new chunk
        // example: threshold 0.7 means "if next sentence is less than
        //           70% similar to current chunk — it's a new topic"

        console.log(`Embedding ${sentences.length} sentences...`);

        // Embed all sentences
        const embeddings = await Promise.all(
            sentences.map(async (sentence) => {
                const response = await openai.embeddings.create({
                    model: "text-embedding-3-small",
                    input: sentence,
                });
                return response.data[0].embedding;
                // each sentence → array of 1536 numbers
            })
        );

        const chunks = [];
        let currentChunkSentences = [sentences[0]];
        // start first chunk with first sentence

        for (let i = 1; i < sentences.length; i++) {
            // loop from second sentence to end

            // Calculate similarity between current sentence
            // and the previous sentence
            const similarity = cosineSimilarity(embeddings[i], embeddings[i - 1]);
            // similarity = how related this sentence is to the previous one
            // example: 0.85 = very related (same topic)
            // example: 0.45 = not very related (topic changed)

            if (similarity < threshold) {
                // similarity dropped below threshold
                // → topic has changed → save current chunk → start new one

                chunks.push(currentChunkSentences.join(" "));
                // join all sentences in current chunk into one string
                currentChunkSentences = [sentences[i]];
                // start new chunk with this sentence

                console.log(`Topic change detected at sentence ${i} (similarity: ${similarity.toFixed(3)})`);
            } else {
                // topic is still the same → add to current chunk
                currentChunkSentences.push(sentences[i]);
            }
        }

        // Save the last chunk
        if (currentChunkSentences.length > 0) {
            chunks.push(currentChunkSentences.join(" "));
        }

        return chunks;
    }

    // Helper — cosine similarity (same as Module 3.4)
    function cosineSimilarity(vecA, vecB) {
        const dot = vecA.reduce((sum, val, i) => sum + val * vecB[i], 0);
        const magA = Math.sqrt(vecA.reduce((sum, val) => sum + val * val, 0));
        const magB = Math.sqrt(vecB.reduce((sum, val) => sum + val * val, 0));
        return dot / (magA * magB);
    }


Fixed vs Semantic — When to Use Which

┌─────────────────────┬──────────────────────┬───────────────────────┐
│                     │  FIXED CHUNKING      │  SEMANTIC CHUNKING    │
├─────────────────────┼──────────────────────┼───────────────────────┤
│ How it splits       │ Every N characters   │ When topic changes    │
│ Speed               │ Very fast            │ Slower (needs embeds) │
│ Cost                │ Free                 │ Costs API calls       │
│ Quality             │ Good                 │ Better                │
│ Best for            │ Most use cases       │ High quality RAG      │
│ Structured docs     │ Great                │ Overkill              │
│ Narrative text      │ OK                   │ Much better           │
│ Code files          │ Split by function    │ Not needed            │
└─────────────────────┴──────────────────────┴───────────────────────┘

For most applications — start with fixed chunking + overlap. It works well and is simple to implement. Switch to semantic chunking when you need higher quality and can afford the extra API calls.


Chunking Rules — Practical Guide

Here are the rules you'll follow in real projects:

Rule 1 — Match chunk size to your content type:

Short FAQs / Q&A docs    → 100-200 words per chunk
General documents        → 200-400 words per chunk
Technical documentation  → 300-500 words per chunk
Legal / medical docs     → 400-600 words per chunk

Rule 2 — Always use overlap:

Overlap = 10-20% of chunk size is a good default
Chunk size 500 chars → overlap 50-100 chars
Chunk size 1000 chars → overlap 100-200 chars

Rule 3 — Store metadata with every chunk:


    // Every chunk should know where it came from
    {
        text: "Aspirin reduces fever and pain...",
        metadata: {
            source: "medical_handbook.pdf",  // which file
            page: 12,                         // which page
            chunkIndex: 3,                    // which chunk on that page
            totalChunks: 47,                  // total chunks in document
        }
    }

Rule 4 — Don't split mid-sentence if possible:

Bad:  "Aspirin reduces fever and pa | in and inflammation"
Good: "Aspirin reduces fever and pain." | "It also reduces inflammation."

Rule 5 — Test your chunking before building the full system:


    // Print your chunks before embedding them
    // Make sure they make sense as standalone pieces
    chunks.forEach((chunk, i) => {
        console.log(`Chunk ${i}: ${chunk.substring(0, 100)}...`);
        console.log(`Length: ${chunk.length} chars`);
    });
    // If chunks look weird — fix chunking before wasting API money


A Real Life Analogy — Library Book Index

Think of chunking like creating an index for a textbook.

Bad index (too granular):

"the" → pages 1,2,3,4,5,6... (every page)
→ useless — too specific

Bad index (too broad):

"medicine" → pages 1-500 (entire book)
→ useless — too vague

Good index (just right):

"aspirin side effects" → pages 23-24
"ibuprofen dosage"     → pages 31-32
"paracetamol overdose" → pages 47-48
→ useful — specific enough to find, broad enough to be complete

Your chunks are like index entries. Each one should be specific enough to be found accurately, and complete enough to be useful when found.


3-Line Summary

  1. Chunks too small lose context and give incomplete answers — chunks too large add noise and confuse the LLM — the sweet spot is 200 to 500 words with 10-20% overlap between chunks.
  2. Fixed chunking splits every N characters (fast, simple, works well for most cases) — semantic chunking splits when topic changes by measuring embedding similarity between sentences (slower, more expensive, better quality).
  3. Always store metadata with every chunk (source file, page number, chunk index) — this lets you filter searches, cite sources in answers, and debug when results are wrong.

Module 5.2 — Complete ✅

Coming up — Module 5.3 — Retrieval & Context Injection

You know how to store chunks. Now we cover what happens when a user asks a question — how retrieval actually works, how you inject retrieved chunks into a prompt correctly, and the exact prompt structure that makes LLMs give accurate sourced answers.

Module 5.1 — What is RAG & Why it Exists

The most valuable skill in AI engineering right now


Start With a Real Problem

Imagine you're a lawyer at a big firm.

Your firm has 10 years of case files — thousands of PDFs, contracts, legal documents, internal memos. All stored on your company's servers.

You get a new case. You need to find relevant precedents fast.

You open ChatGPT and ask:

"What similar cases has our firm handled involving 
 breach of contract in software agreements?"

ChatGPT's response:

"I don't have access to your firm's internal 
 case files. I can only provide general information 
 about breach of contract law..."

Useless. ChatGPT knows nothing about your firm's private documents.

Now you think — okay, let me just paste the documents in:

You try to paste 10 years of case files into ChatGPT
→ 10,000+ pages
→ Millions of tokens
→ Context window: 128,000 tokens max
→ Doesn't fit ❌
→ Even if it did — costs hundreds of dollars per query ❌

This is the problem RAG solves.


What RAG Stands For

R → Retrieval    — find relevant information first
A → Augmented    — add that information to the prompt
G → Generation   — LLM generates answer using that information

RAG = find relevant docs first → inject into prompt → LLM answers from those docs.


The Core Idea — One Sentence

Instead of making the LLM memorize everything — give it only the relevant information it needs, right when it needs it.

Like an open book exam.

Closed book exam (normal LLM):
→ Student must memorize everything beforehand
→ If something wasn't memorized — student guesses
→ Guessing = hallucination

Open book exam (RAG):
→ Student has access to books during the exam
→ Looks up relevant pages for each question
→ Answers from what's written — no guessing needed
→ Answer is accurate and sourced

Why RAG Exists — Three Problems It Solves

Problem 1 — LLMs Have a Knowledge Cutoff

Every LLM was trained on data up to a certain date.

GPT-4 training cutoff: early 2024
User asks in July 2026: "What happened in the stock market last week?"
→ GPT-4 has no idea ❌
→ It might hallucinate an answer ❌

RAG solution:

Fetch last week's stock data → inject into prompt → LLM answers from it ✅

Problem 2 — LLMs Don't Know Your Private Data

Your company's internal data:
→ Employee handbook
→ Product documentation
→ Customer support history
→ Legal contracts
→ Financial reports

GPT-4 has never seen any of this.
It cannot answer questions about it. ❌

RAG solution:

Store your private docs in vector DB
→ Find relevant chunks for each question
→ Inject into prompt
→ LLM answers from your actual data ✅

Problem 3 — Hallucination

LLMs sometimes confidently generate wrong information. They don't know what they don't know.

User: "What is the dosage of MedicinX for children?"

LLM without RAG:
→ "The recommended dosage is 5mg twice daily..."
→ Completely made up ❌ — dangerous in medical context

LLM with RAG:
→ Retrieves actual MedicinX dosage guide from DB
→ "According to the MedicinX prescribing guide page 4:
    dosage for children under 12 is 2.5mg once daily"
→ Sourced, accurate, verifiable ✅

RAG grounds the LLM in real information. It can't hallucinate about what's written in the document.


How RAG Works — The Complete Flow

RAG has two separate phases:

Phase A — Indexing (Done Once)

This is the setup phase. You do this when you first build the system or when documents are updated.

Your Documents (PDFs, Word files, websites, etc.)
          ↓
Load the documents
          ↓
Split into chunks
(each chunk = ~500 words)
          ↓
Embed each chunk
(OpenAI API → 1536 numbers)
          ↓
Store in Vector Database
(Chroma / Pinecone)
          ↓
Done — system is ready

Phase B — Querying (Every Time User Asks)

This happens in real time — every time a user asks a question.

User types a question
          ↓
Embed the question
(same OpenAI embedding model)
          ↓
Search Vector Database
(find top 3-5 most similar chunks)
          ↓
Get back relevant text chunks
          ↓
Build a prompt:
  System: "Answer using only the context below"
  Context: [chunk 1] [chunk 2] [chunk 3]
  Question: [user's question]
          ↓
Send to LLM (GPT-4o)
          ↓
LLM reads the context and generates answer
          ↓
User gets accurate, sourced answer

Visualizing the Complete System

┌─────────────────────────────────────────────────────────┐
│                    RAG SYSTEM                           │
│                                                         │
│  INDEXING PIPELINE (runs once):                         │
│                                                         │
│  [PDF] [Word] [Web]                                     │
│       ↓                                                 │
│  Document Loader                                        │
│       ↓                                                 │
│  Text Splitter → [chunk1] [chunk2] [chunk3] ...         │
│       ↓                                                 │
│  Embedding Model (OpenAI)                               │
│       ↓                                                 │
│  Vector Database ← stores vectors + text + metadata     │
│                                                         │
│  ─────────────────────────────────────────────────────  │
│                                                         │
│  QUERY PIPELINE (runs every request):                   │
│                                                         │
│  User Question                                          │
│       ↓                                                 │
│  Embedding Model (OpenAI)                               │
│       ↓                                                 │
│  Vector Database → similarity search → top 5 chunks     │
│       ↓                                                 │
│  Prompt Builder                                         │
│  ┌──────────────────────────────────┐                   │
│  │ System: Answer from context only │                   │
│  │ Context: [chunk1][chunk2][chunk3]│                   │
│  │ Question: [user question]        │                   │
│  └──────────────────────────────────┘                   │
│       ↓                                                 │
│  LLM (GPT-4o)                                           │
│       ↓                                                 │
│  Answer (grounded in real documents)                    │
└─────────────────────────────────────────────────────────┘

RAG vs Fine-Tuning — Common Confusion

People often ask — why not just fine-tune the LLM on your data instead of using RAG?

Great question. Here's the difference:

FINE-TUNING:
→ Train the model further on your data
→ Model "memorizes" your data into its weights
→ Expensive — costs thousands of dollars
→ Takes days to train
→ When data changes — retrain again
→ Model can still hallucinate
→ Good for: teaching style, tone, format

RAG:
→ Keep base model as is
→ Retrieve relevant data at query time
→ Cheap — just API calls
→ Real time — works instantly
→ When data changes — just update vector DB
→ Model answers from actual retrieved text
→ Good for: knowledge, facts, private data

For most real applications — RAG is the right choice. Fine-tuning is for when you want to change HOW the model responds, not WHAT it knows.

Fine-tune when:
→ "I want the model to always respond like a pirate"
→ "I want the model to output only JSON"
→ "I want the model to match our brand voice"

RAG when:
→ "I want the model to know our company's documents"
→ "I want the model to answer from our product manual"
→ "I want the model to have up-to-date information"

Real World RAG Applications

RAG is everywhere right now:

Customer Support Bot:
→ Index: company FAQs + product docs + past tickets
→ Query: customer asks a question
→ Answer: from actual company documentation

Legal Research Tool:
→ Index: thousands of case files + contracts
→ Query: lawyer asks about similar cases
→ Answer: from actual firm documents

Medical Assistant:
→ Index: medical handbooks + drug guides + research papers
→ Query: doctor asks about drug interactions
→ Answer: from verified medical sources

Internal Company Chatbot:
→ Index: HR policies + engineering docs + meeting notes
→ Query: employee asks any internal question
→ Answer: from actual company knowledge base

Code Documentation Bot:
→ Index: your codebase + README files + API docs
→ Query: developer asks how something works
→ Answer: from your actual code and docs

Every single one of these is the same RAG pattern. Learn it once — build anything.


What You'll Build in Phase 5

By the end of this phase you will have built a complete RAG system:

Module 5.1 → What is RAG (this module)
Module 5.2 → Chunking — how to split documents properly
Module 5.3 → Retrieval + Context Injection
Module 5.4 → Hallucination and Grounding
Module 5.5 → Project — Full PDF Chatbot
             Upload any PDF → Ask any question → Get accurate answers

The PDF chatbot will be a real, working application. Not a toy. Something you can actually show in a portfolio or use in a real project.


3-Line Summary

  1. RAG exists because LLMs have knowledge cutoffs, don't know your private data, and hallucinate — RAG solves all three by retrieving relevant documents first and injecting them into the prompt before the LLM generates an answer.
  2. RAG has two phases — indexing (split documents into chunks, embed them, store in vector DB — done once) and querying (embed the question, find similar chunks, inject into prompt, LLM answers — done every request).
  3. RAG is better than fine-tuning for most real applications — it's cheaper, faster to update, works instantly with new data, and grounds the LLM in actual retrieved text instead of memorized weights.

Module 5.1 — Complete ✅

Coming up — Module 5.2 — Chunking

This is more important than most people realize. How you split your documents directly determines how good your RAG system is. Split too small — you lose context. Split too large — you lose precision. We cover fixed chunking, semantic chunking, overlap, and exactly how to decide chunk size for different use cases.

Module 4.4 — Hands-on: Chroma Local Vector Database

Stop imagining vector databases — let's build one and use it


Project Structure

chroma-explorer/
├── .env              ← API keys
├── package.json      ← Node.js config
├── index.js          ← Main script (add + search documents)
├── viewData.js       ← Viewer script (see what's stored)
└── chroma_env/       ← Python virtual environment (auto-created)

Part 1 — Python Setup

Step 1 — Install Python 3.11

Go to https://www.python.org/downloads/release/python-3119/

Scroll down to "Files" section and download:

Windows installer (64-bit)
→ python-3.11.9-amd64.exe

During installation — check this box:

☑ Add Python 3.11 to PATH

Verify installation:

py -3.11 --version
# Should print: Python 3.11.9

Step 2 — Create Virtual Environment

A virtual environment is an isolated Python setup. Packages installed here don't affect your system Python or any other project.

# Go to your project folder
cd C:\Users\yourname\Desktop\Code\chroma-explorer

# Create virtual environment using Python 3.11 specifically
py -3.11 -m venv chroma_env

This creates a chroma_env folder. Inside it:

chroma_env/
├── Scripts/          ← activate scripts + installed executables
├── Lib/              ← all installed packages live here
└── pyvenv.cfg        ← config file

Step 3 — Activate Virtual Environment

chroma_env\Scripts\activate

Your terminal prompt changes to show the environment name:

# Before activation:
PS C:\Users\yourname\Desktop\Code\chroma-explorer>

# After activation:
(chroma_env) PS C:\Users\yourname\Desktop\Code\chroma-explorer>

The (chroma_env) prefix means the virtual environment is active. Now any pip install goes into this isolated environment — not your system Python.


Step 4 — Install Chroma Inside Virtual Environment

pip install chromadb

This installs Chroma and all its dependencies inside chroma_env using Python 3.11 — which is fully compatible.

Verify:

python -c "import chromadb; print(chromadb.__version__)"
# Should print: 0.x.x or 1.x.x (some stable version)

Step 5 — Start Chroma Server

chroma run --path ./chroma_data

You should see:

Running Chroma server at http://localhost:8000

Keep this terminal open. Chroma server must stay running while you use it.


Part 2 — Node.js Setup

Open a second terminal window for all Node.js work. Keep the first terminal (Chroma server) running.

Step 6 — Create Project


    mkdir chroma-explorer
    cd chroma-explorer
    npm init -y

Update package.json — add "type": "module":


  {
    "name": "chroma-explorer",
    "version": "1.0.0",
    "type": "module",
    "scripts": {
      "start": "node index.js"
    }
  }


Step 7 — Install Node.js Packages


    npm install chromadb @chroma-core/openai openai dotenv

What each package does:
chromadb              → JavaScript client to talk to Chroma server
@chroma-core/openai   → connects Chroma with OpenAI for embeddings
openai                → OpenAI API client for generating embeddings
dotenv                → reads .env file for API keys

Step 8 — Create .env File


    # .env
    OPENAI_API_KEY=sk-proj-your-actual-key-here

Rules for .env file:

✅ OPENAI_API_KEY=sk-proj-abc123
❌ OPENAI_API_KEY = sk-proj-abc123   (no spaces around =)
❌ OPENAI_API_KEY="sk-proj-abc123"   (no quotes)

Part 3 — The Code

index.js — Main Script


    // ─────────────────────────────────────────
    // IMPORTS
    // ─────────────────────────────────────────

    import { ChromaClient } from "chromadb";
    // ChromaClient = connects to the running Chroma server at localhost:8000

    import { OpenAIEmbeddingFunction } from "@chroma-core/openai";
    // OpenAIEmbeddingFunction = tells Chroma to use OpenAI for embeddings
    // Note: in older chromadb versions this was inside "chromadb" package itself
    // In current version it moved to "@chroma-core/openai" — always use this import

    import OpenAI from "openai";
    // OpenAI client — used to generate embeddings for search queries

    import * as dotenv from "dotenv";
    // reads .env file

    dotenv.config();
    // loads OPENAI_API_KEY into process.env


    // ─────────────────────────────────────────
    // SETUP CLIENTS
    // ─────────────────────────────────────────

    const chroma = new ChromaClient();
    // connects to Chroma server running at http://localhost:8000
    // server must be running (started in Terminal 1)

    const openai = new OpenAI({
        apiKey: process.env.OPENAI_API_KEY,
        // example: "sk-proj-abc123..."
    });

    const embedder = new OpenAIEmbeddingFunction({
        apiKey: process.env.OPENAI_API_KEY,
        // OpenAI key for Chroma to use when embedding documents

        modelName: "text-embedding-3-small",
        // embedding model — produces 1536 numbers per text
    });


    // ─────────────────────────────────────────
    // SAMPLE DATA
    // 8 documents across 3 categories
    // Think of these as chunks from real PDFs
    // ─────────────────────────────────────────

    const documents = [
        {
            id: "med_001",
            // unique ID — like primary key in SQL
            text: "Aspirin is commonly used to reduce fever, pain, and inflammation. Common side effects include stomach irritation, nausea, and heartburn.",
            // text that gets embedded — this is what LLM reads when answering
            metadata: {
                source: "medical_handbook.pdf",  // which file
                category: "medication",          // for filtering
                topic: "aspirin",                // specific topic
                page: 12,                        // page number
            },
        },
        {
            id: "med_002",
            text: "Ibuprofen is a nonsteroidal anti-inflammatory drug. It reduces hormones that cause inflammation and pain. Side effects may include stomach pain and kidney issues.",
            metadata: { source: "medical_handbook.pdf", category: "medication", topic: "ibuprofen", page: 18 },
        },
        {
            id: "med_003",
            text: "Paracetamol (acetaminophen) is used to treat pain and fever. Unlike aspirin and ibuprofen, it does not reduce inflammation. Overdose can cause serious liver damage.",
            metadata: { source: "medical_handbook.pdf", category: "medication", topic: "paracetamol", page: 24 },
        },
        {
            id: "tech_001",
            text: "React is a JavaScript library for building user interfaces. It uses a component-based architecture and a virtual DOM to efficiently update the UI.",
            metadata: { source: "tech_guide.pdf", category: "technology", topic: "react", page: 5 },
        },
        {
            id: "tech_002",
            text: "Node.js is a JavaScript runtime built on Chrome V8 engine. It allows JavaScript to run on the server side and is great for building APIs.",
            metadata: { source: "tech_guide.pdf", category: "technology", topic: "nodejs", page: 10 },
        },
        {
            id: "tech_003",
            text: "PostgreSQL is a powerful open source relational database. It supports advanced SQL features, JSON storage, and full-text search capabilities.",
            metadata: { source: "tech_guide.pdf", category: "technology", topic: "postgresql", page: 15 },
        },
        {
            id: "food_001",
            text: "Pizza Margherita originated in Naples Italy in 1889. It consists of tomato sauce, mozzarella cheese, and fresh basil representing the Italian flag colors.",
            metadata: { source: "food_history.pdf", category: "food", topic: "pizza", page: 3 },
        },
        {
            id: "food_002",
            text: "Pasta is a staple of Italian cuisine made from durum wheat. Common varieties include spaghetti, penne, and rigatoni. Cooking time varies by pasta thickness.",
            metadata: { source: "food_history.pdf", category: "food", topic: "pasta", page: 7 },
        },
    ];


    // ─────────────────────────────────────────
    // FUNCTION 1 — setupCollection
    // Creates or resets the Chroma collection
    // Like DROP TABLE + CREATE TABLE in SQL
    // ─────────────────────────────────────────

    async function setupCollection() {
        console.log("🗄️  Setting up Chroma collection...\n");

        try {
            await chroma.deleteCollection({ name: "knowledge_base" });
            // delete if exists — start fresh
            // like DROP TABLE IF EXISTS knowledge_base
            console.log("   Deleted existing collection");
        } catch (e) {
            // collection didn't exist yet — that's fine
            console.log("   No existing collection found — starting fresh");
        }

        const collection = await chroma.createCollection({
            name: "knowledge_base",
            // name of this collection — like table name in SQL

            embeddingFunction: embedder,
            // tells Chroma to use OpenAI to auto-embed text
            // when we call collection.add() with text — Chroma calls OpenAI for us

            metadata: { "hnsw:space": "cosine" },
            // use cosine similarity for distance calculation
            // best choice for text embeddings
        });

        console.log("   ✅ Collection 'knowledge_base' created");
        console.log("   Using cosine similarity for search\n");

        return collection;
        // return collection object — used in all other functions
    }


    // ─────────────────────────────────────────
    // FUNCTION 2 — addDocuments
    // Embeds and stores all documents in Chroma
    // Like INSERT INTO in SQL
    // ─────────────────────────────────────────

    async function addDocuments(collection) {
        console.log("📥 Adding documents to collection...\n");

        await collection.add({
            ids: documents.map((doc) => doc.id),
            // example: ["med_001", "med_002", "med_003", ...]

            documents: documents.map((doc) => doc.text),
            // Chroma auto-embeds each text using OpenAI
            // example: ["Aspirin is commonly...", "Ibuprofen is..."]

            metadatas: documents.map((doc) => doc.metadata),
            // example: [{ source: "medical_handbook.pdf", category: "medication" }, ...]
        });

        console.log(`   ✅ Added ${documents.length} documents\n`);
        console.log("   Documents stored:");
        documents.forEach((doc) => {
            console.log(`   → [${doc.id}] ${doc.text.substring(0, 60)}...`);
        });
        console.log();
    }


    // ─────────────────────────────────────────
    // FUNCTION 3 — showStats
    // Shows collection info
    // Like SELECT COUNT(*) in SQL
    // ─────────────────────────────────────────

    async function showStats(collection) {
        const count = await collection.count();
        // total number of stored documents
        // example: 8

        console.log("📊 Collection Stats:");
        console.log(`   Total documents stored: ${count}`);
        console.log("   Categories: medication(3), technology(3), food(2)\n");
    }


    // ─────────────────────────────────────────
    // FUNCTION 4 — basicSearch
    // Semantic search — no filters
    // Searches ALL documents by meaning
    // ─────────────────────────────────────────

    async function basicSearch(collection, query, k = 3) {
        // query = search question as string
        // k     = how many results to return

        console.log(`🔍 Search: "${query}"`);
        console.log(`   Returning top ${k} results\n`);

        // embed the query using OpenAI
        const queryEmbedding = await openai.embeddings.create({
            model: "text-embedding-3-small",
            // MUST be same model used when storing documents
            input: query,
        });

        const queryVector = queryEmbedding.data[0].embedding;
        // queryVector = [0.23, -0.87, 0.41, ...] (1536 numbers)

        // search Chroma with the query vector
        const results = await collection.query({
            queryEmbeddings: [queryVector],
            // array because Chroma supports batch queries
            // we only have one query so array has one item

            nResults: k,
            // return top k most similar documents

            include: ["documents", "metadatas", "distances"],
            // distances = how far each result is from query
            // lower distance = more similar
        });

        // print results
        const ids = results.ids[0];
        const docs = results.documents[0];
        const metas = results.metadatas[0];
        const distances = results.distances[0];
        // [0] because results are wrapped in array (batch query support)

        ids.forEach((id, index) => {
            const similarity = (1 - distances[index]).toFixed(4);
            // Chroma returns distance — convert to similarity
            // similarity = 1 - distance
            // example: distance 0.05 → similarity 0.95

            const bar = "".repeat(Math.round(similarity * 10)) +
                "".repeat(10 - Math.round(similarity * 10));

            console.log(`   ${index + 1}. [${id}] Similarity: ${similarity} ${bar}`);
            console.log(`      Category : ${metas[index].category}`);
            console.log(`      Topic    : ${metas[index].topic}`);
            console.log(`      Source   : ${metas[index].source}, Page ${metas[index].page}`);
            console.log(`      Text     : ${docs[index].substring(0, 100)}...`);
            console.log();
        });
    }


    // ─────────────────────────────────────────
    // FUNCTION 5 — filteredSearch
    // Semantic search WITH metadata filter
    // Only searches within a specific category
    // Like WHERE clause in SQL
    // ─────────────────────────────────────────

    async function filteredSearch(collection, query, categoryFilter, k = 3) {
        console.log(`🔍 Filtered Search: "${query}"`);
        console.log(`   Filter: category = "${categoryFilter}"`);
        console.log(`   Returning top ${k} results\n`);

        const queryEmbedding = await openai.embeddings.create({
            model: "text-embedding-3-small",
            input: query,
        });
        const queryVector = queryEmbedding.data[0].embedding;

        const results = await collection.query({
            queryEmbeddings: [queryVector],
            nResults: k,

            where: { category: categoryFilter },
            // metadata filter — only search documents matching this condition
            // example: { category: "medication" }
            // only medication documents get searched — food and tech are skipped

            include: ["documents", "metadatas", "distances"],
        });

        const ids = results.ids[0];
        const docs = results.documents[0];
        const metas = results.metadatas[0];
        const distances = results.distances[0];

        if (ids.length === 0) {
            console.log("   No results found\n");
            return;
        }

        ids.forEach((id, index) => {
            const similarity = (1 - distances[index]).toFixed(4);
            console.log(`   ${index + 1}. [${id}] Similarity: ${similarity}`);
            console.log(`      Topic : ${metas[index].topic}`);
            console.log(`      Text  : ${docs[index].substring(0, 100)}...`);
            console.log();
        });
    }


    // ─────────────────────────────────────────
    // MAIN
    // ─────────────────────────────────────────

    async function main() {
        console.log("🚀 CHROMA VECTOR DATABASE EXPLORER\n");
        console.log("=".repeat(55));

        const collection = await setupCollection();
        await addDocuments(collection);
        await showStats(collection);

        console.log("=".repeat(55));
        console.log("\n📌 EXPERIMENT 1: Basic Semantic Search");
        console.log("=".repeat(55) + "\n");

        await basicSearch(collection, "What medication helps with pain and fever?", 3);
        await basicSearch(collection, "How do I build a web application backend?", 3);
        await basicSearch(collection, "Tell me about databases", 3);

        console.log("=".repeat(55));
        console.log("\n📌 EXPERIMENT 2: Metadata Filtered Search");
        console.log("=".repeat(55) + "\n");

        await filteredSearch(collection, "What are the side effects?", "medication", 3);
        await filteredSearch(collection, "Tell me about Italian food", "food", 2);
        await filteredSearch(collection, "JavaScript runtime environment", "technology", 2);

        console.log("=".repeat(55));
        console.log("\n📌 EXPERIMENT 3: Semantic Power Test");
        console.log("=".repeat(55) + "\n");

        await basicSearch(collection, "medicine that can damage your liver if you take too much", 1);
        await basicSearch(collection, "component based frontend framework", 1);

        console.log("=".repeat(55));
        console.log("\n✅ All experiments complete!");
    }

    main().catch(console.error);


viewData.js — Viewer Script


    import { ChromaClient } from "chromadb";
    import { OpenAIEmbeddingFunction } from "@chroma-core/openai";
    import * as dotenv from "dotenv";

    dotenv.config();

    // connects to already running Chroma server
    // data jo index.js ne store kiya — wahi dikhayega
    const chroma = new ChromaClient();

    const embedder = new OpenAIEmbeddingFunction({
        apiKey: process.env.OPENAI_API_KEY,
        modelName: "text-embedding-3-small",
    });

    async function main() {
        console.log("\n👀 CHROMA DATA VIEWER");
        console.log("=".repeat(90));

        // getCollection = existing collection open karo
        // createCollection nahi kar rahe — sirf open kar rahe hain
        // like USE database_name in SQL
        const collection = await chroma.getCollection({
            name: "knowledge_base",
            embeddingFunction: embedder,
        });

        const count = await collection.count();
        console.log(`\n📋 Total documents: ${count}`);
        console.log(`   SQL: SELECT COUNT(*) FROM knowledge_base\n`);

        // fetch all stored data
        // SQL: SELECT id, text, metadata FROM knowledge_base
        const allData = await collection.get({
            include: ["documents", "metadatas"],
            // embeddings nahi liye — 1536 numbers per row unreadable hote hain
        });

        // ── TABLE VIEW ───────────────────────────────────────────
        console.log("📊 TABLE VIEW");
        console.log("   SQL: SELECT * FROM knowledge_base\n");

        console.log("" + "".repeat(88) + "");
        console.log(
            "" + "ID".padEnd(12) +
            "" + "CATEGORY".padEnd(12) +
            "" + "TOPIC".padEnd(12) +
            "" + "PAGE".padEnd(6) +
            "" + "TEXT (first 40 chars)".padEnd(40) + ""
        );
        console.log("" + "".repeat(88) + "");

        allData.ids.forEach((id, index) => {
            const meta = allData.metadatas[index];
            const text = allData.documents[index];
            console.log(
                "" + id.padEnd(12) +
                "" + meta.category.padEnd(12) +
                "" + meta.topic.padEnd(12) +
                "" + String(meta.page).padEnd(6) +
                "" + text.substring(0, 40).padEnd(40) + ""
            );
        });

        console.log("" + "".repeat(88) + "");

        // ── GROUP BY ─────────────────────────────────────────────
        console.log("\n📊 GROUP BY category:");
        console.log("   SQL: SELECT category, COUNT(*) GROUP BY category\n");

        const groups = {};
        allData.metadatas.forEach((meta) => {
            if (!groups[meta.category]) groups[meta.category] = 0;
            groups[meta.category]++;
        });

        Object.entries(groups).forEach(([category, count]) => {
            const bar = "".repeat(count * 4);
            console.log(`   ${category.padEnd(14)} ${bar} (${count} docs)`);
        });

        // ── WHERE FILTER ─────────────────────────────────────────
        console.log("\n🔎 WHERE category = 'medication'");
        console.log("   SQL: SELECT * FROM knowledge_base WHERE category = 'medication'\n");

        allData.ids.forEach((id, index) => {
            if (allData.metadatas[index].category === "medication") {
                const meta = allData.metadatas[index];
                const text = allData.documents[index];
                console.log(`   ID    : ${id}`);
                console.log(`   Topic : ${meta.topic}`);
                console.log(`   Text  : ${text.substring(0, 80)}...`);
                console.log();
            }
        });

        // ── FULL ROW DETAIL ──────────────────────────────────────
        console.log("🔍 FULL ROW DETAIL");
        console.log("   SQL: SELECT * FROM knowledge_base (full text)\n");

        allData.ids.forEach((id, index) => {
            const meta = allData.metadatas[index];
            const text = allData.documents[index];

            console.log(`   ┌─ Row ${index + 1} ─────────────────────────────────────`);
            console.log(`   │ ID       : ${id}`);
            console.log(`   │ Category : ${meta.category}`);
            console.log(`   │ Topic    : ${meta.topic}`);
            console.log(`   │ Source   : ${meta.source}`);
            console.log(`   │ Page     : ${meta.page}`);
            console.log(`   │ Text     : ${text}`);
            console.log(`   │ Embedding: [1536 numbers — hidden for readability]`);
            console.log(`   └────────────────────────────────────────────────────`);
            console.log();
        });

        console.log("=".repeat(90));
        console.log("✅ Done\n");
    }

    main().catch(console.error);


Part 4 — Running Everything

Every time you work on this project — follow this exact order:

Terminal 1 — Start Chroma Server


    cd chroma-explorer
    chroma_env\Scripts\activate
    chroma run --path ./chroma_data

Keep this running. Never close it while coding.

Terminal 2 — Run Node.js Code


    cd chroma-explorer
    node index.js      # store documents + run searches
    node viewData.js   # view what's stored in Chroma


Complete Setup Checklist

FIRST TIME SETUP:
□ Download Python 3.11 from python.org
□ Install with "Add to PATH" checked
□ py -3.11 -m venv chroma_env
□ chroma_env\Scripts\activate
□ pip install chromadb
□ npm init -y
□ Add "type": "module" to package.json
□ npm install chromadb @chroma-core/openai openai dotenv
□ Create .env with real OpenAI API key
□ Create index.js
□ Create viewData.js

EVERY TIME YOU WORK:
□ Terminal 1: activate venv → chroma run
□ Terminal 2: node index.js OR node viewData.js

Why Virtual Environment

Without venv:                    With venv:
─────────────────────────────    ─────────────────────────
System Python 3.14               System Python 3.14
  └── chromadb ← BROKEN          └── (untouched)

                                 chroma_env (Python 3.11)
                                   └── chromadb ← WORKS ✅

One project's packages           Each project has its own
affect everything                isolated packages

Think of it like node_modules in Node.js — each project has its own dependencies, completely isolated from everything else.


What You Should See

🚀 CHROMA VECTOR DATABASE EXPLORER
=======================================================
🗄️  Setting up Chroma collection...
   No existing collection found — starting fresh
   ✅ Collection 'knowledge_base' created
   Using cosine similarity for search

📥 Adding documents to collection...
   ✅ Added 8 documents
   Documents stored:
   → [med_001] Aspirin is commonly used to reduce fever, pain...
   → [med_002] Ibuprofen is a nonsteroidal anti-inflammatory...
   → [med_003] Paracetamol (acetaminophen) is used to treat...
   → [tech_001] React is a JavaScript library for building...
   → [tech_002] Node.js is a JavaScript runtime built on...
   → [tech_003] PostgreSQL is a powerful open source relational...
   → [food_001] Pizza Margherita originated in Naples Italy...
   → [food_002] Pasta is a staple of Italian cuisine made...

📊 Collection Stats:
   Total documents stored: 8

=======================================================
📌 EXPERIMENT 1: Basic Semantic Search
=======================================================

🔍 Search: "What medication helps with pain and fever?"
   Returning top 3 results

   1. [med_001] Similarity: 0.8923 ████████
      Category: medication
      Source: medical_handbook.pdf, Page 12
      Text: Aspirin is commonly used to reduce fever, pain...

   2. [med_003] Similarity: 0.8654 ████████
      Category: medication
      Source: medical_handbook.pdf, Page 24
      Text: Paracetamol (acetaminophen) is used to treat pain...

   3. [med_002] Similarity: 0.8123 ████████
      Category: medication
      Source: medical_handbook.pdf, Page 18
      Text: Ibuprofen is a nonsteroidal anti-inflammatory drug...

=======================================================
📌 EXPERIMENT 3: Cross-Category Semantic Power
=======================================================

🔍 Search: "medicine that can damage your liver if you take too much"
   Returning top 1 results

   1. [med_003] Similarity: 0.8834 ████████
      Category: medication
      Source: medical_handbook.pdf, Page 24
      Text: Paracetamol (acetaminophen) is used to treat pain...

What Just Happened — Reading the Results

Experiment 1 proves semantic search works:

Query: "What medication helps with pain and fever?"

Found: Aspirin (0.89), Paracetamol (0.87), Ibuprofen (0.81)
All three are pain/fever medications ✓
No food or tech docs appeared ✓

Experiment 2 proves metadata filtering works:

Query: "What are the side effects?"
Filter: category = "medication"

Only medical documents returned ✓
Technology and food docs completely excluded ✓
Even though "side effects" could relate to other things

Experiment 3 proves meaning over keywords:

Query: "medicine that can damage your liver if you take too much"
Found: Paracetamol doc

The Paracetamol doc says: "Overdose can cause serious liver damage"
Your query says: "damage your liver if you take too much"

Different words. Same meaning. Correctly found. ✓

The Bridge to RAG

What you just built is the storage and search layer of a RAG system.

What you have now:
✅ Documents stored with embeddings
✅ Semantic search working
✅ Metadata filtering working

What RAG adds on top:
→ Take the search results (text chunks)
→ Inject them into an LLM prompt
→ LLM answers based on the retrieved context

Phase 5 adds exactly that final step. You're almost there.


3-Line Summary

  1. Chroma is a local vector database — you add documents with text and metadata, it automatically embeds them using OpenAI, and stores everything on disk ready for fast semantic search.
  2. Querying Chroma returns documents ranked by cosine distance — convert to similarity with 1 - distance — lower distance means higher similarity means more relevant result.
  3. Metadata filters in Chroma narrow the search space before similarity search runs — where: { category: "medication" } means only medical documents are searched — critical for building multi-category or multi-user applications.

Module 4.4 — Complete ✅

Phase 4 is done. 🎉

You now have hands-on experience with a real vector database:

✅ Understood why SQL fails for semantic search
✅ Learned how ANN + HNSW makes search fast
✅ Understood Top-K, score thresholds, metadata filtering
✅ Built and queried a real local vector database with Chroma

Coming Up — Phase 5: RAG

Module 5.1 — What is RAG and Why it Exists

This is the most valuable skill in AI engineering right now. Every company building AI products uses RAG. You have all the foundations — embeddings, vector databases, LLMs, prompts. Now we put it all together into a complete system. Starting next module.

Module 5.2 — Chunking

How you split documents determines how good your RAG system is Start With a Simple Question You have a 100-page PDF. You want to build a RAG...