Pacific Design/ artificial intelligence

RAG & Embeddings · entry 02/04

Vector search

Exact nearest-neighbor search stops scaling long before your ambitions do; approximate indexes buy speed with a little recall — measure how much you spent.

Brute force first

Nearest-neighbor search sounds like it needs infrastructure. Under a few hundred thousand vectors, it does not. Comparing a query against every stored embedding is one matrix multiply:

import numpy as np

# docs: (N, d) matrix, rows unit-normalized; q: query vector
scores = docs @ q
top_k = np.argsort(-scores)[:10]

That is exact, dependency-free, and runs in single-digit milliseconds for a hundred thousand vectors on ordinary hardware. A remarkable number of "we need a vector database" projects are forty lines of numpy wearing a costume. Start here; move when measured latency says to, not before.

HNSW, the workhorse

Exact search costs O(N) per query, and at hundreds of millions of vectors that stops being funny. Approximate nearest-neighbor (ANN) indexes fix it, and the dominant design is HNSW — Hierarchical Navigable Small World. Picture a graph where every vector links to its close neighbors, topped by sparser layers that act as highways. A query enters at the sparse top, greedily hops toward its target, drops down a layer, refines, and reaches the right neighborhood after touching a few thousand nodes instead of all of them. Search becomes roughly logarithmic; the price is an index that is slow to build, lives in RAM, and returns the true nearest neighbors only most of the time.

The tradeoff triangle

Every ANN index trades among recall (the fraction of true neighbors it finds), latency, and memory. Build and search parameters — links per node, beam width — slide you along that surface, and library defaults are tuned for benchmarks, not for your data. The discipline that matters: take a sample of real queries, compute ground truth by brute force, and measure recall@k like any other model metric. Unmeasured indexes routinely run at 0.7 recall while everyone involved assumes 1.0.

Filters, and where the index lives

Real queries carry conditions — this tenant, this language, newer than March. Filtering interacts badly with graph traversal: apply the filter first and the graph you walk is full of holes; apply it after and a selective filter can empty your top-k. Engines differ in how gracefully they handle this, so test at your real selectivity. On placement: pgvector or sqlite-vec keeps vectors inside the database you already run, joins and transactions included, and comfortably serves millions of rows. A dedicated vector database earns its complexity at billion-vector scale — sharding, quantization, rolling rebuilds — not as the default first move.

Failure mode

Silent recall collapse. A broken SQL query throws an error; a degraded ANN index returns ten plausible neighbors that simply omit the right one, so nothing crashes and no alert fires. The symptom surfaces far downstream — a RAG pipeline confidently citing the wrong document — and gets blamed on the model. Measure recall when you deploy, and again after heavy inserts and deletes: graph indexes decay as they churn, and the only thing that notices is your eval.