Skip to content

Scaling Vector Search

Prerequisites

Before this lesson you should be comfortable with:

  • Module 13, Lessons 01–06 — ANN indexes, HNSW tuning, schema design, multi-tenant patterns
  • Module 13, Lesson 03 — IVF, PQ, benchmark scripts
  • Basic systems concepts — RAM, SSD, QPS, p95 latency

Most applications don't need scaling until 1M+ vectors. When you hit that wall, this lesson gives you the playbook: sharding strategies, quantization trade-offs, replication, and a performance optimization checklist with tuning tables.


What You'll Learn

Objective Time Difficulty
Identify memory, latency, and throughput bottlenecks at scale 10 min Advanced
Apply horizontal sharding and partition routing strategies 15 min Advanced
Reduce memory 4–32× with quantization techniques 10 min Advanced
Design for high availability with replication and failover 15 min Advanced

Intuition First: The City That Outgrew Its Map

Your semantic map (vector index) worked perfectly for 100K locations. Now you have 100 million. The map doesn't fit on your desk (RAM), lookups take minutes (latency), and thousands of people ask for directions simultaneously (throughput).

Scaling options: 1. Compress the map (quantization) — less detail, fits in pocket 2. Split into boroughs (sharding) — each borough has its own local map 3. Hire assistants (replication) — multiple copies handle concurrent requests 4. Store maps on shelves, not desks (disk-based indexes) — slower access but unlimited size

Production systems combine all four.


Scaling Challenges

Challenge Symptom Threshold (1536d float32)
Memory Index doesn't fit in RAM; OOM kills ~10M vectors ≈ 60 GB
Latency p95 search > 100ms Depends on index type, QPS
Throughput Query queue backs up ~500–2000 QPS per node
Ingestion Bulk upsert takes hours > 1M vectors per batch
Updates HNSW graph degrades with churn > 10% daily update rate

Memory Math

Before choosing an optimization, calculate your memory budget:

def estimate_memory(num_vectors: int, dimensions: int, bytes_per_float: int = 4) -> dict:
    raw_bytes = num_vectors * dimensions * bytes_per_float
    return {
        "raw_gb": raw_bytes / 1e9,
        "float16_gb": raw_bytes / 2 / 1e9,
        "int8_gb": raw_bytes / 4 / 1e9,
        "pq_48byte_gb": num_vectors * 48 / 1e9,
        "hnsw_overhead_gb": raw_bytes * 1.3 / 1e9,  # ~30% graph overhead
    }

for n in [1_000_000, 10_000_000, 100_000_000]:
    mem = estimate_memory(n, 1536)
    print(f"{n/1e6:.0f}M vectors: raw={mem['raw_gb']:.1f}GB, "
          f"HNSW={mem['hnsw_overhead_gb']:.1f}GB, PQ={mem['pq_48byte_gb']:.1f}GB")

Typical output:

1M vectors:  raw=6.1GB, HNSW=8.0GB, PQ=0.05GB
10M vectors: raw=61.4GB, HNSW=79.9GB, PQ=0.48GB
100M vectors: raw=614.4GB, HNSW=798.7GB, PQ=4.8GB

At 10M vectors, raw HNSW requires ~80 GB RAM. Quantization or disk-based indexes become mandatory.


Sharding Strategies

Horizontal Sharding

Split vectors across nodes; query all shards in parallel; merge top-K results.

Query → Router
          ├─ Shard 1 (0-3.3M vectors)  → Top 10
          ├─ Shard 2 (3.3M-6.6M)       → Top 10
          └─ Shard 3 (6.6M-10M)        → Top 10
              Merge → Global Top 10
def sharded_search(shards: list, query_vec, k: int = 10) -> list:
    """Query all shards and merge results by score."""
    all_results = []
    for shard in shards:
        results = shard.query(vector=query_vec, top_k=k)
        all_results.extend(results["matches"])

    all_results.sort(key=lambda x: x["score"], reverse=True)
    return all_results[:k]
Database Sharding Support Configuration
Pinecone Automatic (pods) Pod size + replicas
Qdrant Manual sharding shard_number at collection creation
Weaviate Automatic shardCount in schema
Milvus Automatic Distributed deployment

Partition Sharding (Tenant/Domain Routing)

Route queries to specific partitions based on metadata — avoids searching irrelevant data entirely:

SHARD_MAP = {
    "tenant-acme": shard_client_1,
    "tenant-globex": shard_client_2,
    "tenant-initech": shard_client_3,
}

def routed_search(tenant_id: str, query_vec, k: int = 5):
    shard = SHARD_MAP.get(tenant_id)
    if not shard:
        raise ValueError(f"Unknown tenant: {tenant_id}")
    return shard.query(vector=query_vec, top_k=k)

This is the most cost-effective sharding for multi-tenant RAG: each tenant's queries only search their own vectors.

Sharding Strategy Selection

Pattern When to Use Latency Impact Cost
Horizontal (query all shards) Single large corpus +Merge overhead Linear with shards
Partition routing Multi-tenant Minimal (single shard) Efficient
Time-based partitions Log/event data Low (recent data hot) Archive old shards to cold storage
Hash sharding Even distribution +Merge overhead Predictable

Memory Optimization: Quantization

Quantization reduces vector precision to save memory with minimal recall loss.

Technique Bytes/Vector (1536d) Compression Recall@10 Loss Implementation
Float32 (raw) 6,144 0% Default
Float16 3,072 ~0.1% Qdrant, Milvus
Int8 scalar 1,536 ~0.5-1% Qdrant, Pinecone
Product Quantization 48–192 32–128× ~2-5% Faiss, Milvus, Qdrant
Binary 192 32× ~4-8% Qdrant, experimental

Qdrant Scalar Quantization

from qdrant_client.models import ScalarQuantization, ScalarQuantizationConfig, ScalarType

client.update_collection(
    collection_name="documents",
    quantization_config=ScalarQuantization(
        scalar=ScalarQuantizationConfig(type=ScalarType.INT8, always_ram=True)
    ),
)

Matryoshka + Quantization Combined

From Lesson 2: truncate to 256 dimensions (6× savings) then apply int8 quantization (another 4×) = 24× total memory reduction with ~3-6% recall loss. Often the best cost/quality trade-off for large deployments.


Disk-Based Indexes

When RAM < dataset size, store indexes on SSD:

Algorithm Provider Latency vs RAM Scale
DiskANN Milvus, Azure AI Search 2-5× slower 100M–1B vectors
Vamana Milvus 2.x 2-4× slower 100M+ vectors
Qdrant mmap Qdrant 1.5-3× slower Configurable

DiskANN keeps a compressed graph on SSD with a small in-memory cache for hot nodes. p95 latency typically 10–50ms vs 1–5ms for RAM-resident HNSW — acceptable for many RAG workloads.


Replication for High Availability

                Writes
          ┌───────┼───────┐
          ▼       ▼       ▼
      [Primary] [Replica] [Replica]
          │       │         │
          └───────┼─────────┘
              Read queries
           (load balanced)
Component Role Failover
Primary Handles writes, replicates Promote replica on failure
Replica Serves read queries Automatic in managed services
Load balancer Distributes read QPS Health-check based routing

Managed services (Pinecone, Qdrant Cloud, Weaviate Cloud) handle replication automatically. Self-hosted: configure at least 2 replicas for production.


Performance Optimization Checklist

# Optimization Expected Impact Effort
1 Tune HNSW ef to minimum meeting recall target -20-50% latency Low
2 Pre-filter by tenant/category before ANN -30-70% search space Low
3 Scalar quantization (int8) 4× memory, ~1% recall loss Low
4 Matryoshka 256d truncation 6× memory, ~3% recall loss Low
5 Partition sharding by tenant Eliminates cross-tenant search Medium
6 Cache frequent query embeddings -20ms per cached query Medium
7 Batch concurrent queries +2-5× throughput Medium
8 DiskANN for RAM-constrained datasets Enables 10× larger corpus High

Latency Budget Template

Stage Target Notes
Query embedding 15–30 ms Cache for repeated queries
ANN search 1–20 ms Depends on index type and ef
Metadata fetch 1–5 ms Included in most DB responses
Reranking (optional) 20–100 ms Cross-encoder, Lesson 8
Total retrieval 30–150 ms Before LLM call

Failure Modes at Scale

Hot shard. One tenant with 90% of vectors dominates a shared shard; their queries are fast, others wait. Fix: dedicated shards for large tenants.

Merge bottleneck. Horizontal sharding with 10+ shards creates merge overhead at query time. Fix: partition routing or reduce shard count.

Quantization recall cliff. Aggressive PQ (m=16 on 1536d) drops recall 8-10%. Fix: benchmark on eval set before deploying; use scalar int8 first.

Replication lag. Writes to primary not yet visible on replicas. Fix: read-your-writes consistency mode or query primary for recent upserts.

OOM during bulk ingest. Loading 10M vectors into HNSW build exceeds RAM. Fix: batch index build, use IVF+PQ, or disk-based index.


Production Notes

  • Benchmark with your actual data at target scale before choosing architecture — synthetic benchmarks lie.
  • Start with a single well-tuned HNSW index; shard only when memory or QPS demands it.
  • Monitor p50/p95/p99 latency, QPS, and index size weekly. Set alerts at 2× baseline.
  • Plan capacity 3–6 months ahead: embedding ingestion rate × chunk size × growth rate.

Key Takeaways

  • Memory is the first wall at ~10M vectors (60+ GB for 1536d float32). Quantization and sharding are the exits.
  • Partition sharding by tenant is the most cost-effective multi-tenant scaling pattern.
  • Scalar int8 quantization gives 4× memory savings with ~1% recall loss — deploy this before exotic optimizations.
  • DiskANN enables billion-scale when RAM is insufficient; accept 2-5× latency increase.
  • Always measure recall on your eval set after any scaling optimization.

Resources


Next Lesson

Lesson 8: Hybrid Search with Vector Databases — Combine BM25 and vector search with RRF, benchmark alpha sweeps, and production hybrid pipelines.