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.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 ye...