Lesson 10: Vector Databases | AI Engineering
Module 3: RAG Pipeline

Vector Databases

Store, index, and query millions of embeddings with sub-millisecond retrieval using purpose-built vector databases.

Lesson 10 ~25 min Intermediate

Why Vector Databases?

The Problem

Brute-force cosine similarity over 1 million 1536-d vectors requires ~6 billion floating-point operations per query. At 10ms per query? Unacceptable for production. Vector databases solve this with Approximate Nearest Neighbor (ANN) algorithms that trade a tiny accuracy loss for 100–1000x speedup.

A vector database is not just a "store" — it's an indexed search engine for high-dimensional vectors. It handles:

  • Efficient ANN search (sub-millisecond at million-scale)
  • Metadata storage and filtering
  • CRUD operations (add, update, delete vectors)
  • Persistence and replication
  • Hybrid search (vector + keyword)

ANN Indexing Algorithms

The magic behind vector databases is their indexing strategy. Two dominant approaches:

HNSW (Hierarchical Navigable Small World)

Builds a multi-layer graph where each node connects to its approximate nearest neighbors. Search starts at the top layer (sparse, long-range connections) and descends to lower layers (dense, short-range connections). Think of it like a skip list for vectors.

  • Pros: Best recall/speed tradeoff, no training needed, supports dynamic inserts
  • Cons: High memory usage (stores graph in RAM), slow to build initially
  • Used by: Qdrant, ChromaDB, pgvector, Weaviate

IVF (Inverted File Index)

Clusters vectors into N partitions (using k-means). At query time, only searches the closest K partitions. Fewer partitions checked = faster but less accurate.

  • Pros: Lower memory, can be combined with quantization (IVF-PQ)
  • Cons: Requires training step, less accurate for dynamic data
  • Used by: FAISS, Pinecone (internal)

The Vector DB Pipeline

Pipeline Stages:

  1. Ingest: Documents are chunked and embedded, producing vectors + metadata
  2. Index: Vectors are inserted into the ANN index (HNSW graph is updated)
  3. Query: Query text is embedded, ANN search finds top-K similar vectors
  4. Filter: Optional metadata filters narrow results (e.g., date range, source)
  5. Return: Original text chunks + metadata + similarity scores returned

Vector Database Comparison

Feature ChromaDB Pinecone Qdrant pgvector
Type Embedded / Client-Server Managed Cloud Self-hosted / Cloud PostgreSQL Extension
Index HNSW Proprietary (IVF-based) HNSW + quantization HNSW / IVFFlat
Max Vectors ~1M (embedded) Billions Billions Millions
Metadata Filter ✅ Basic ✅ Rich ✅ Rich + nested ✅ Full SQL
Hybrid Search ✅ Sparse+Dense ✅ BM25+Vector ✅ via tsvector
Pricing Free (OSS) $0.096/hr (pod) Free (OSS) / Cloud Free (extension)
Best For Prototyping, small apps Production at scale Self-hosted production Existing Postgres apps
Setup Complexity ⭐ (pip install) ⭐⭐ (API key) ⭐⭐⭐ (Docker) ⭐⭐ (extension)
🎯 Recommendation: Start with ChromaDB for learning and prototyping. Move to Qdrant or Pinecone for production. Use pgvector if you already have PostgreSQL infrastructure.

ChromaDB: Complete Walkthrough

ChromaDB is the fastest way to get started. It runs in-process (no server needed) and persists to disk.

import chromadb
from chromadb.utils import embedding_functions

# --- Setup ---
# Persistent storage (survives restarts)
client = chromadb.PersistentClient(path="./chroma_db")

# Use OpenAI embeddings (or default all-MiniLM-L6-v2)
openai_ef = embedding_functions.OpenAIEmbeddingFunction(
    api_key="sk-...",
    model_name="text-embedding-3-small"
)

# Create or get a collection
collection = client.get_or_create_collection(
    name="knowledge_base",
    embedding_function=openai_ef,
    metadata={"hnsw:space": "cosine"}  # distance metric
)

print(f"Collection has {collection.count()} documents")
# --- Ingestion ---
documents = [
    "Python was created by Guido van Rossum in 1991",
    "JavaScript was created by Brendan Eich in 1995",
    "Rust focuses on memory safety without garbage collection",
    "Go was designed at Google for concurrent systems",
    "TypeScript adds static types to JavaScript",
    "React is a JavaScript library for building user interfaces",
    "FastAPI is a modern Python web framework",
    "Docker containers package applications with dependencies",
]

# Add documents with metadata
collection.add(
    documents=documents,
    ids=[f"doc_{i}" for i in range(len(documents))],
    metadatas=[
        {"category": "language", "year": 1991},
        {"category": "language", "year": 1995},
        {"category": "language", "year": 2010},
        {"category": "language", "year": 2009},
        {"category": "language", "year": 2012},
        {"category": "framework", "year": 2013},
        {"category": "framework", "year": 2018},
        {"category": "tool", "year": 2013},
    ]
)

print(f"Added {len(documents)} documents")
# --- Query: Basic Similarity Search ---
results = collection.query(
    query_texts=["What languages focus on safety?"],
    n_results=3
)

print("Top 3 results:")
for doc, dist in zip(results["documents"][0], results["distances"][0]):
    print(f"  [{1 - dist:.3f}] {doc}")

# Output:
#   [0.847] Rust focuses on memory safety without garbage collection
#   [0.712] Go was designed at Google for concurrent systems
#   [0.634] TypeScript adds static types to JavaScript
# --- Query: Filtered Search ---
# Only search within frameworks
results = collection.query(
    query_texts=["web development tools"],
    n_results=3,
    where={"category": "framework"}  # metadata filter
)

print("Frameworks for web dev:")
for doc in results["documents"][0]:
    print(f"  • {doc}")

# --- Query: Combined Filters ---
# Languages created after 2008
results = collection.query(
    query_texts=["modern programming"],
    n_results=5,
    where={
        "$and": [
            {"category": "language"},
            {"year": {"$gte": 2008}}
        ]
    }
)

# --- Update a document ---
collection.update(
    ids=["doc_0"],
    documents=["Python was created by Guido van Rossum in 1991. It emphasizes readability."],
    metadatas=[{"category": "language", "year": 1991, "updated": True}]
)

# --- Delete documents ---
collection.delete(ids=["doc_7"])

Qdrant: Production-Grade Alternative

For production workloads that need filtering performance, Qdrant excels:

from qdrant_client import QdrantClient
from qdrant_client.models import (
    VectorParams, Distance, PointStruct,
    Filter, FieldCondition, MatchValue
)
from openai import OpenAI

# Connect (local Docker: docker run -p 6333:6333 qdrant/qdrant)
qdrant = QdrantClient(host="localhost", port=6333)
openai_client = OpenAI()

# Create collection
qdrant.create_collection(
    collection_name="documents",
    vectors_config=VectorParams(
        size=1536,       # text-embedding-3-small dimensions
        distance=Distance.COSINE
    )
)

# Helper: embed text
def embed(texts):
    response = openai_client.embeddings.create(
        input=texts, model="text-embedding-3-small"
    )
    return [d.embedding for d in response.data]

# Insert
texts = ["Python is great for AI", "Rust is fast and safe"]
vectors = embed(texts)

qdrant.upsert(
    collection_name="documents",
    points=[
        PointStruct(id=i, vector=v, payload={"text": t, "lang": "en"})
        for i, (v, t) in enumerate(zip(vectors, texts))
    ]
)

# Search with filter
query_vector = embed(["systems programming"])[0]
results = qdrant.search(
    collection_name="documents",
    query_vector=query_vector,
    query_filter=Filter(
        must=[FieldCondition(key="lang", match=MatchValue(value="en"))]
    ),
    limit=5
)

for hit in results:
    print(f"[{hit.score:.3f}] {hit.payload['text']}")

Performance & Scaling Tips

🔑 Production Considerations

  • Batch inserts: Insert 100–1000 vectors per call, never one-by-one
  • Index tuning: HNSW parameters (M=16, efConstruction=200) trade build-time for search quality
  • Quantization: Scalar/binary quantization cuts memory 4–32x with ~2% recall loss
  • Pre-filter vs post-filter: Pre-filtering is faster but may miss results; post-filtering is more accurate
  • Sharding: For 10M+ vectors, shard across multiple nodes
  • Embedding cache: Store raw text hash → embedding mapping to avoid re-computing
  • Dimensionality: Use OpenAI's dimensions param (e.g., 512 instead of 1536) if recall@10 stays acceptable
📊 Benchmarks (1M vectors, 1536-d, single machine):
• ChromaDB: ~50ms p95 latency, ~2GB RAM
• Qdrant: ~5ms p95 latency, ~3GB RAM
• pgvector (HNSW): ~10ms p95 latency, uses shared buffers
• Pinecone: ~20ms p95 latency (includes network)

🛠️ Mini-Project: Vector DB Setup

Set up a persistent vector database and build a searchable knowledge base from web content.

Steps:

  1. Install ChromaDB: pip install chromadb openai
  2. Collect 30+ text snippets (use Wikipedia paragraphs, blog posts, or documentation)
  3. Create a persistent ChromaDB collection with metadata (source URL, category, date)
  4. Ingest all snippets with proper IDs and metadata
  5. Implement three query modes: basic search, filtered search (by category), filtered search (by date range)
  6. Measure query latency and recall (manually verify top-3 results make sense)
  7. Bonus: Compare results between ChromaDB's default model and OpenAI embeddings
"""Vector DB Setup - Starter Code"""
import chromadb
import time

client = chromadb.PersistentClient(path="./my_vectordb")
collection = client.get_or_create_collection("knowledge")

# TODO: Add your documents with metadata
# TODO: Implement filtered search
# TODO: Measure latency

def benchmark_query(query: str, n_results: int = 5):
    start = time.perf_counter()
    results = collection.query(query_texts=[query], n_results=n_results)
    elapsed = (time.perf_counter() - start) * 1000
    print(f"Query: '{query}' → {elapsed:.1f}ms")
    for doc, dist in zip(results["documents"][0], results["distances"][0]):
        print(f"  [{1-dist:.3f}] {doc[:80]}...")
    return results

📋 Key Takeaways

  • Vector databases use ANN algorithms (HNSW, IVF) to search millions of vectors in milliseconds
  • ChromaDB is ideal for prototyping; Qdrant/Pinecone for production workloads
  • Metadata filtering is essential — you'll almost always filter by source, date, or category
  • Batch inserts and proper index tuning are critical for production performance
  • The vector DB is the "memory" of your RAG system — get this right and everything downstream improves