Day 4: Vector Indexes Without Magical Thinking

Day 4: Vector Indexes Without Magical Thinking

Vector indexes are approximate nearest-neighbor structures with explicit recall/latency tradeoffs, filter semantics, and operational cost. Day 4 kills magical thinking: define SLOs, measure on your corpus, then choose HNSW, IVF flat/PQ, or “Postgres is enough.”

⚡ TL;DR: HNSW usually wins latency/recall at moderate scale but eats RAM. IVF scales cheaper if you tune nprobe and retrain. pgvector is often enough. Always apply metadata filters in the engine — unfiltered top-k across tenants is a security incident waiting to happen.

The real decision surface

Index family Strengths Watch-outs
HNSW High recall, low query latency Memory; build time; graph churn
IVF / IVF-PQ Huge corpora, compression Train quality; nprobe; recall loss with PQ
Exact / flat Ground truth for eval Only tiny sets or offline
pgvector SQL filters, simple ops Tune lists/ef_search; autovacuum; size
-- ✅ Filter-first mental model (engine syntax varies)
SELECT id, content
FROM chunks
WHERE tenant_id = $1
  AND doc_type = 'runbook'
  AND env = 'prod'
ORDER BY embedding <=> $2
LIMIT 20;

❌ Run global ANN, then filter in the application. You spend recall budget on wrong-tenant neighbors and risk leaks if one code path forgets the filter.

HNSW knobs that matter in production

  • M / efConstruction: build quality versus RAM and build time.
  • efSearch: query latency versus recall — this is your primary runtime dial.
# ✅ Sweep efSearch against a labeled set; lock an SLO
for ef in (32, 64, 128, 256):
    recall, p95_ms = bench(index, cases, ef_search=ef)
    print({"ef": ef, "recall@10": recall, "p95_ms": p95_ms})

Choose the smallest efSearch that meets Recall@10 under your p95 latency budget. Raising efSearch forever is how teams buy GPUs to hide a bad chunking strategy.

IVF without folklore

IVF partitions the vector space into coarse cells. Too-small nprobe misses the right cells; too-large burns latency. If you trained centroids on Wikipedia and query Terraform, clusters are wrong — retrain on production-like embeddings.

Product quantization (PQ) saves memory at the cost of recall. Measure the loss on your labeled set; do not accept vendor defaults as destiny. Schedule centroid refresh when corpus distribution shifts (new product line, new doc template).

When SQL and hybrid beat a bigger ANN

Selective predicates often shrink the candidate set so hard that a modest index wins:

  • service = 'checkout' AND env = 'prod' AND updated_at > now() - interval '90 days'
  • Exact symbol / error-code match via tsvector or BM25 fused later (Day 21)

A 10M-vector HNSW over “everything we ever wrote” is slower and noisier than 80k vectors after tenant + service filters. Index size is not a flex; answer faithfulness is.

Operationally, prefer the simplest store that hits SLOs: many teams get far with Aurora PostgreSQL + pgvector + good metadata until concurrency or corpus size forces OpenSearch k-NN or a dedicated vector service.

Closing checklist

  • [ ] Write Recall@k and p95 latency targets before shopping indexes
  • [ ] Enforce tenant and ACL predicates inside the query engine
  • [ ] Benchmark HNSW efSearch / IVF nprobe on your embeddings
  • [ ] Prefer pgvector until scale or latency forces a move
  • [ ] Never app-filter after unscoped ANN for multi-tenant data
  • [ ] Retrain IVF centroids when corpus distribution shifts

Worked example: filter beats brute force

A global top-50 over 5M chunks returns 48 wrong-tenant or wrong-service neighbors before one useful runbook. The same query with tenant_id + service=checkout hits a 40k partition and returns the right chunk at rank 1 with lower efSearch.

-- Prove it with EXPLAIN / engine profiling in staging
-- Compare p95 and Recall@10 with and without predicates

Capacity planning should start from filtered cardinality, not vanity total vector counts.

Failure modes to watch

  • App-side filtering after ANN (recall + security risk).
  • Untuned IVF nprobe after corpus growth.
  • HNSW rebuilds during peak traffic without blue/green.
  • pgvector under-vacuumed indexes after bulk deletes.

Field notes from production

OpenSearch k-NN and pgvector both need backup/restore drills. Graph rebuilds after AZ loss should be a practiced runbook. Measure recall after restore; silent empty indexes look like ‘the model got dumb.’ Prefer blue/green index aliases so cutover is one atomic pointer move.

Implementation sketch

-- Implementation sketch: forced tenant predicate in a security definer view
CREATE VIEW tenant_chunks AS
SELECT * FROM chunks WHERE tenant_id = current_setting('app.tenant_id');

Operator addendum

Load-test filtered queries, not only unfiltered ANN. Filters change candidate distribution and cache behavior. Your p95 from a blog benchmark on open datasets will not match tenant-scoped production traffic.

Capacity planning worksheet

Before you commit to a managed vector service, write down: filtered working set size (vectors after typical predicates), peak QPS, p95 latency SLO, and recall floor. Multiply working set by bytes-per-vector (dim × dtype × graph overhead factor ≈ 1.5–2× for HNSW). If the RAM bill exceeds an Aurora instance that already holds your relational data, try pgvector first. If p95 cannot be met after tuning ef_search/nprobe, then specialize. Revisit quarterly — corpus growth is a capacity event, not a surprise.

Also practice restore: snapshot, delete, restore, re-measure Recall@10. Indexes that cannot be rebuilt from S3 sources on a documented timer are liabilities during AZ issues.

Series navigation

← Day 3 · Day 5 →

Last updated September 11, 2026


Discover more from CheatCoders

Subscribe to get the latest posts sent to your email.

Comments

No comments yet. Why don’t you start the discussion?

Leave a Reply