04 Retrieval Systems 11 min read 2,371 words

Vector Databases

Vector databases are purpose-built systems for storing, indexing, and searching high-dimensional embeddings. This chapter covers the options, tradeoffs, and production considerations for choosing and operating vector databases.

vector-dbretrievalproductioncostapplied
01database

What Is a Vector Database

A vector database stores embeddings (dense vectors) and enables fast similarity search over them.

Texttext · 2 lines
12
Traditional DB:      SELECT * FROM docs WHERE category = 'tech'
Vector DB:           SELECT * FROM docs ORDER BY similarity(embedding, query_embedding) LIMIT 10
1.1

Core Capabilities

CapabilityPurpose
Vector storagePersist high-dimensional embeddings
Similarity searchFind nearest neighbors quickly
Metadata filteringCombine vector search with attribute filters
CRUD operationsUpdate embeddings as data changes
ScalingHandle millions to billions of vectors
1.2

Why Not General Databases?

Traditional databases can store vectors but lack optimized search:

ApproachSearch ComplexityPractical at Scale
Brute force (PostgreSQL pgvector)O(n * d)OK to ~1M vectors
ANN index (dedicated vector DB)O(log n) or O(1)Yes, billions
02search fundamentals

Vector Search Fundamentals

2.2

Distance Metrics

MetricFormulaRangeBest For
Cosine1 - (a . b) / (norm(a) * norm(b))[0, 2]Text embeddings
Euclidean (L2)sqrt(sum((a - b)^2))[0, inf)Image embeddings
Dot producta . b(-inf, inf)Already normalized

For text embeddings: Use cosine similarity (or dot product if pre-normalized).

2.3

Recall vs Latency Tradeoff

ANN indices trade some accuracy for speed. Tune for your requirements.

03algorithms

Indexing Algorithms

3.1

HNSW (Hierarchical Navigable Small World)

The most popular algorithm for production vector search.

How it works:

  1. Build a graph where nodes are vectors
  2. Connect to nearby neighbors
  3. Multiple layers of abstraction (hierarchical)
  4. Search: navigate from top layer down, greedy nearest neighbor

Pros:

  • Excellent recall/latency tradeoff
  • No training required
  • Supports updates natively

Cons:

  • Memory-intensive (graph structure)
  • Index size: ~1.5-2x vector data

Key parameters:

  • M: Max connections per node (16-64)
  • ef_construction: Build-time exploration (100-500)
  • ef_search: Query-time exploration (50-200)
3.2

IVF (Inverted File Index)

Partition vectors into clusters, search only relevant clusters.

How it works:

  1. Use k-means to create centroids
  2. Assign each vector to nearest centroid
  3. At query time: find nearest centroids, search those clusters

Pros:

  • Lower memory than HNSW
  • Can use quantization (IVF-PQ)

Cons:

  • Requires training
  • Updates need re-clustering or hybrid approach

Key parameters:

  • nlist: Number of clusters (sqrt(n) rule of thumb)
  • nprobe: Clusters to search at query time
3.3

Product Quantization (PQ)

Compress vectors to reduce memory and speed up comparison.

How it works:

  1. Split vector into subvectors
  2. Quantize each subvector to a codebook
  3. Store codes instead of full vectors

Memory reduction: 4-32x typical

Tradeoff: Lower accuracy due to quantization loss

3.4

Flat Index (Brute Force)

No approximation, exact search.

Use when:

  • Less than 100K vectors
  • Accuracy is critical
  • Latency budget is generous
3.5

Algorithm Comparison

AlgorithmMemoryBuild TimeQuery SpeedRecallUpdates
HNSWHighMediumVery fast95-99%Good
IVFMediumFastFast90-98%Fair
IVF-PQLowFastFast85-95%Fair
FlatLowNoneSlow100%Instant
04database comparison

Vector Database Comparison

4.1

Major Options (December 2025)

DatabaseTypeBest ForPricing Model
PineconeManaged cloudEasy start, scalePer vector-hour
QdrantOpen source / CloudSelf-hosted controlPer GB (cloud) or free
WeaviateOpen source / CloudMultimodal, ML integrationPer dimension-hour
ChromaOpen sourcePrototyping, localFree
MilvusOpen source / CloudOn-prem enterpriseFree (self-host)
pgvectorPostgreSQL extensionSmall scale, existing PGCompute only
4.2

Feature Comparison

FeaturePineconeQdrantWeaviateMilvuspgvector
Hosted optionYesYesYesYes (Zilliz)Via cloud PG
Self-hostedNoYesYesYesYes
Metadata filteringGoodExcellentGoodGoodVia SQL
Hybrid searchYesYesYesYesLimited
Max vectorsBillionsBillionsBillionsBillions~10M
HNSW indexYesYesYesYesYes
4.3

Metadata Filtering

Critical for multi-tenant and filtering use cases:

Pythonpython · 19 lines
12345678910111213141516171819
# Pinecone
results = index.query(
    vector=query_embedding,
    top_k=10,
    filter={"tenant_id": "123", "category": {"$in": ["tech", "science"]}}
)

# Qdrant
results = client.search(
    collection_name="documents",
    query_vector=query_embedding,
    limit=10,
    query_filter=Filter(
        must=[
            FieldCondition(key="tenant_id", match=MatchValue(value="123")),
            FieldCondition(key="category", match=MatchAny(any=["tech", "science"]))
        ]
    )
)

Performance impact: Filtering happens during search, not after. Pre-filtered indices are faster but less flexible.

05patterns

Query Patterns

5.3

Pattern 3: Hybrid Search (Dense + Sparse)

Pythonpython · 15 lines
123456789101112131415
def hybrid_search(query: str, alpha: float = 0.5, top_k: int = 5) -> list[Document]:
    # Dense (semantic)
    dense_embedding = embed(query)
    dense_results = vector_db.search(dense_embedding, top_k=top_k * 2)
    
    # Sparse (keyword)
    sparse_results = bm25_search(query, top_k=top_k * 2)
    
    # Combine with reciprocal rank fusion
    combined = reciprocal_rank_fusion(
        [dense_results, sparse_results],
        weights=[alpha, 1 - alpha]
    )
    
    return combined[:top_k]

Some databases (Weaviate, Qdrant, Pinecone) support hybrid search natively:

Pythonpython · 5 lines
12345
# Weaviate native hybrid
results = client.query.get("Document", ["text"]).with_hybrid(
    query=query,
    alpha=0.5  # 0 = BM25 only, 1 = vector only
).with_limit(5).do()
5.4

Pattern 4: Multi-Vector Query

For parent-child or multi-aspect retrieval:

Pythonpython · 13 lines
12345678910111213
def multi_vector_search(queries: list[str], top_k: int = 5) -> list[Document]:
    all_results = []
    
    for query in queries:
        embedding = embed(query)
        results = vector_db.search(embedding, top_k=top_k)
        all_results.extend(results)
    
    # Dedupe and rerank
    unique = dedupe_by_id(all_results)
    reranked = rerank(queries[0], unique)  # Use primary query for reranking
    
    return reranked[:top_k]
06operations

Production Operations

6.1

Capacity Planning

Pythonpython · 27 lines
123456789101112131415161718192021222324252627
def estimate_resources(
    num_vectors: int,
    dimensions: int,
    metadata_size_bytes: int = 500
) -> dict:
    # Vector storage
    vector_size = dimensions * 4  # float32
    total_vector_storage = num_vectors * vector_size
    
    # Index overhead (HNSW ~1.5x)
    index_overhead = total_vector_storage * 1.5
    
    # Metadata
    metadata_storage = num_vectors * metadata_size_bytes
    
    # Total
    total_gb = (total_vector_storage + index_overhead + metadata_storage) / 1e9
    
    # QPS estimate (rough)
    qps_per_gb = 50  # depends heavily on config
    estimated_qps = total_gb * qps_per_gb
    
    return {
        "storage_gb": total_gb,
        "estimated_qps": estimated_qps,
        "recommended_replicas": max(1, int(total_gb / 50))  # ~50GB per replica
    }
6.2

Index Maintenance

Pythonpython · 31 lines
12345678910111213141516171819202122232425262728293031
class VectorDBMaintenance:
    def __init__(self, client):
        self.client = client
    
    def add_documents(self, documents: list[Document]):
        """Upsert documents with batching."""
        batch_size = 100
        for i in range(0, len(documents), batch_size):
            batch = documents[i:i + batch_size]
            embeddings = embed_batch([d.text for d in batch])
            
            self.client.upsert([
                {
                    "id": doc.id,
                    "vector": embedding,
                    "payload": doc.metadata
                }
                for doc, embedding in zip(batch, embeddings)
            ])
    
    def delete_documents(self, doc_ids: list[str]):
        """Delete by document ID."""
        self.client.delete(ids=doc_ids)
    
    def update_metadata(self, doc_id: str, metadata: dict):
        """Update metadata without re-embedding."""
        self.client.set_payload(
            collection_name="documents",
            payload=metadata,
            points=[doc_id]
        )
6.3

High Availability

Key patterns:

  • Leader-follower for writes
  • Read replicas for query scaling
  • Async replication for HA
6.4

Monitoring

Pythonpython · 26 lines
1234567891011121314151617181920212223242526
VECTOR_DB_METRICS = [
    "query_latency_p50",
    "query_latency_p99",
    "queries_per_second",
    "index_size_gb",
    "vector_count",
    "filter_latency",
    "upsert_latency",
    "cache_hit_rate"
]

def alert_rules():
    return {
        "query_latency_p99_high": {
            "condition": "query_latency_p99 > 500ms",
            "severity": "warning"
        },
        "query_latency_p99_critical": {
            "condition": "query_latency_p99 > 2000ms",
            "severity": "critical"
        },
        "low_recall": {
            "condition": "bench_recall < 0.90",
            "severity": "warning"
        }
    }
07analysis

Cost Analysis

7.1

Managed Service Pricing (December 2025, verify current)

ProviderModelExample: 10M vectors, 1536 dims
PineconePod-based or Serverless~$70-150/month serverless
Qdrant CloudPer GB~$50/month (20GB)
Weaviate CloudPer dimensions~$100/month
Zilliz (Milvus)Per CU~$75/month
7.2

Self-Hosted Costs

Pythonpython · 22 lines
12345678910111213141516171819202122
def estimate_self_hosted_cost(
    vectors: int,
    dimensions: int,
    cloud: str = "aws"
) -> dict:
    storage_gb = (vectors * dimensions * 4 * 2.5) / 1e9  # 2.5x for index
    
    # Instance sizing
    if storage_gb < 50:
        instance = "r6g.large"  # 16 GB RAM, ~$60/month
    elif storage_gb < 200:
        instance = "r6g.xlarge"  # 32 GB RAM, ~$120/month
    else:
        instance = "r6g.2xlarge"  # 64 GB RAM, ~$240/month
    
    return {
        "storage_gb": storage_gb,
        "instance": instance,
        "monthly_compute": instance_pricing[instance],
        "monthly_storage": storage_gb * 0.10,  # EBS
        "total_monthly": instance_pricing[instance] + storage_gb * 0.10
    }
7.3

Decision: Managed vs Self-Hosted

FactorManagedSelf-Hosted
Ops overheadLowHigh
Cost at small scaleHigherLower
Cost at large scaleVariableOften lower
ControlLessFull
ComplianceDependsFull control
Vendor lock-inYesNo (if open source)
08framework

Selection Framework

8.1

Decision Tree

8.2

Evaluation Criteria

CriterionWeightQuestions to Ask
ScaleHighHow many vectors now? In 1 year?
LatencyHighWhat are p99 requirements?
Ops capacityHighCan we operate this?
CostMediumBudget constraints?
FeaturesMediumHybrid search? Multimodal?
Lock-in riskLow-MediumOpen source preferred?
8.3

Proof of Concept Checklist

Before committing to a vector database:

  • Load representative data volume
  • Benchmark query latency at target QPS
  • Test metadata filtering performance
  • Verify update/delete performance
  • Test failure recovery
  • Evaluate monitoring and observability
  • Calculate total cost of ownership
09questions

Interview Questions

Q: How would you choose between Pinecone and a self-hosted solution?

Strong answer: Decision depends on several factors:

Choose Pinecone when:

  • Team lacks ops capacity for stateful infrastructure
  • Need to move quickly (days not weeks)
  • Scale is moderate (under 100M vectors)
  • Budget allows managed service premium
  • Compliance allows cloud-vendor dependency

Choose self-hosted (Qdrant, Milvus) when:

  • Have Kubernetes and ops expertise
  • Cost sensitivity at scale
  • Need full control over data
  • Specific compliance requirements
  • Want to avoid vendor lock-in

For most startups, I would start with Pinecone or Qdrant Cloud for velocity, then evaluate migration if costs become prohibitive at scale. The switching cost is moderate since vector DBs have similar APIs.

Q: Explain how HNSW works and when you would not use it.

Strong answer: HNSW builds a hierarchical graph of vectors:

How it works:

  1. Insert vectors as nodes in a multi-layer graph
  2. Higher layers have fewer nodes, larger jumps
  3. Search: start at top layer, greedily navigate to nearest neighbor
  4. Descend layers until bottom (all vectors)

Why it is good:

  • O(log n) query complexity
  • No training required
  • Supports real-time updates
  • Excellent recall/latency tradeoff

When not to use:

  • Very small datasets (<10K): brute force is fine
  • Extremely memory constrained: HNSW uses 1.5-2x vector size for graph
  • Need exact search: HNSW is approximate
  • Heavy update workload with tight latency: updates can cause temporary degradation

Alternatives:

  • IVF-PQ for memory constraints
  • Flat index for exact search
  • LSH for very high-dimensional sparse vectors

Q: How do you handle multi-tenancy in a vector database?

Strong answer: Three main approaches:

1. Metadata filtering (most common):

Pythonpython · 4 lines
1234
results = db.search(
    vector=query,
    filter={"tenant_id": current_tenant}
)
  • Pros: Simple, single index
  • Cons: All tenants share resources, potential for bugs exposing data

2. Collection per tenant:

Pythonpython · 1 line
1
results = db.collection(f"tenant_{tenant_id}").search(vector=query)
  • Pros: Strong isolation, per-tenant scaling
  • Cons: Many collections, operational overhead

3. Namespace per tenant (Pinecone):

Pythonpython · 1 line
1
results = index.query(vector=query, namespace=tenant_id)
  • Pros: Isolation within single index
  • Cons: Vendor-specific

I would choose:

  • Metadata filtering for most cases (simple, cost-effective)
  • Separate collections for high-security requirements
  • Never post-filter (retrieve all, filter after) due to leakage risk
10references

References


Previous: Chunking Strategies | Next: Hybrid Search

summary · added by this rebuild

Key takeaways

01

ANN buys speed by giving up recall

Brute force is O(n*d) and workable to roughly 1M vectors; the comparison table puts HNSW at 95-99% recall, IVF-PQ at 85-95%, and only flat search at 100%.

02

HNSW has three tuning dials

M sets connections per node (16-64), ef_construction sets build-time exploration (100-500), and ef_search sets query-time exploration (50-200), which is the live recall-versus-latency knob.

03

Budget about 2.5x your raw vector bytes

The capacity planner counts dimensions times four bytes per vector, adds roughly 1.5x again for the HNSW graph plus metadata, and suggests one replica per 50 GB.

04

pgvector caps out around 10M vectors

The feature table gives every dedicated store billions of vectors but limits pgvector to about 10M, which is the practical line between reusing Postgres and adopting a vector database.

05

Managed pricing is closer than expected

For 10M vectors at 1536 dimensions the page quotes roughly $70-150 a month on Pinecone serverless and $50 on Qdrant Cloud, so ops capacity usually decides, not price.