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
nprobeand 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
tsvectoror 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/ IVFnprobeon 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
Last updated September 11, 2026
Discover more from CheatCoders
Subscribe to get the latest posts sent to your email.
