Vector Databases
Store, index, and query millions of embeddings with sub-millisecond retrieval using purpose-built vector databases.
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:
- Ingest: Documents are chunked and embedded, producing vectors + metadata
- Index: Vectors are inserted into the ANN index (HNSW graph is updated)
- Query: Query text is embedded, ANN search finds top-K similar vectors
- Filter: Optional metadata filters narrow results (e.g., date range, source)
- 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) |
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
dimensionsparam (e.g., 512 instead of 1536) if recall@10 stays acceptable
• 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:
- Install ChromaDB:
pip install chromadb openai - Collect 30+ text snippets (use Wikipedia paragraphs, blog posts, or documentation)
- Create a persistent ChromaDB collection with metadata (source URL, category, date)
- Ingest all snippets with proper IDs and metadata
- Implement three query modes: basic search, filtered search (by category), filtered search (by date range)
- Measure query latency and recall (manually verify top-3 results make sense)
- 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