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
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
- 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.
- Querying Chroma returns documents ranked by cosine distance — convert to similarity with
1 - distance— lower distance means higher similarity means more relevant result. - 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