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.

No comments:

Post a Comment

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