System Design · step by step

Design a Vector Database

Step 1 / 9
The numbers to beatimmutablesegments, merged in bg2 writessegment + indexrebuildableindex derives from segments

In the interview room

How you’d open this design in an interview

Before any boxes: agree what it must do, pin the qualities that shape everything, then build — naming each trade-off as you make it. The walkthrough above is that exact order.

Functional requirements

What it must do — agree on these before drawing a single box.

  • Search: given a query vector and k, return the k nearest ids with similarity scores.
  • Ingest / upsert: add vectors — keep the full copy and insert them into the index.
  • Filter: “nearest where tenant = X and recency < 30 days” — metadata-constrained search.
  • Tune recall vs speed: knobs like nprobe / efSearch trade accuracy for latency on purpose.
  • Scale to billions: serve a billion-vector index that outgrows one machine’s RAM.

Non-functional requirements

The qualities that shape the whole design — each one names the mechanism that buys it.

Sub-linear search instead of O(N·d)
An IVF index clusters vectors into cells; a query scans only the nearest nprobe cells, not all N — a tunable fraction of the data.
High recall without retraining
An HNSW small-world graph links each vector to near neighbours across layers, so search greedily hops to the region in ~log N steps.
Fit a billion vectors in RAM
Product quantization compresses each vector to a ~96-byte code so distances are estimated from codes, then the shortlist is re-ranked against full vectors.
Durable, rebuildable storage
Full-precision vectors live in immutable segments (the source of truth); the ANN index is a fast accelerator you can always rebuild from them.
Correct results under metadata filters
A selectivity-aware planner pre-filters selective predicates, over-fetches then post-filters loose ones, or pushes the predicate into the traversal.
Grow past one machine
A shard router scatters the query to every shard and gathers their local top-k; because k-NN is mergeable, the global top-k is the best of the locals. Replicas add QPS.

The trade-offs you say out loud

Senior signal isn’t the boxes — it’s naming what you gave up and why it was the right price.

Approximate nearest-neighbour (ANN)over exact brute-force kNN

Exact kNN is the correct ground truth, but it’s O(N·d) per query — a billion 768-D dot products every search, far too slow and CPU-hungry to serve online. ANN is approximately right in milliseconds, and you measure recall@k to know the price.

IVF cells (k-means)over a 1-D sort or random sampling

There’s no single ordering of points in 768-D space, and random sampling misses the true neighbours most of the time. k-means cells put nearby vectors together, so a query scans only its closest few cells.

An HNSW small-world graphover a k-d tree

k-d trees degrade badly in high dimensions — they end up visiting most of the tree — and a complete graph is O(N²) edges. A layered navigable graph reaches the neighbourhood in ~log N hops with high recall and incremental inserts, at the cost of RAM for the edges.

Product quantizationover dropping dimensions to fit RAM

Truncating dimensions throws away the information the distance depends on, so recall craters, and generic gzip needs decompressing before you can compute distance. PQ estimates distance directly from tiny codes, then re-ranks the top few against the full vectors.

A selectivity-aware plannerover naive post-filtering

Post-filtering the ANN top-k can leave 1–2 results when the predicate is selective, and pre-filtering everything is brute force when it’s loose. The planner picks by selectivity — pre-filter tight, over-fetch loose, or filter inside the traversal.

What this teaches

Learn AI system design by building a vector database step by step. An interactive guide to approximate nearest-neighbour (ANN) search — distance metrics, IVF cells, HNSW graphs, product quantization, metadata filtering, and sharding — so you can find the closest of a billion embeddings in milliseconds.

Key takeaways

  • Query API — one contract: query vector + k + filters → nearest ids
  • Segments vs index — full vectors are truth; the ANN index is a rebuildable accelerator
  • Distance metric — cosine/dot/L2 — normalize, and match the embeddings
  • Exact kNN — correct but O(N·d) — the baseline you approximate
  • IVF — k-means cells; nprobe trades recall for speed
  • HNSW — layered small-world graph; ~log N hops, efSearch dial
  • Product quantization — compress to fit RAM, re-rank exactly on the shortlist
  • Filters + sharding — a selectivity-aware planner; scatter/gather mergeable top-k

Concepts covered

  • Why a database just for vectors?
  • A query vector in, k neighbours out
  • Ingest, segments, and the index
  • Exact kNN and the distance metric
  • IVF — search a few cells, not all
  • HNSW — navigable small worlds
  • Product quantization compresses the vectors
  • Metadata filters and the recall trap
  • Shard, replicate, scale
▶  Watch it explained

Prefer a video walkthrough?

Design a Vector Database — read the full walkthrough as text

the same steps, decisions & trade-offs, for reading, reference & search

The big idea

Why a database just for vectors?

Embeddings turn text, images and users into points in a few-hundred-dimensional space, where nearest = most similar. The whole game is one query: "given this vector, find the k closest out of a billion." A normal database indexes values you can sort and equal-match — but "closest in 768-D space" isn’t a B-tree lookup. So how do you build an index for proximity?

A vector database is built around approximate nearest-neighbour (ANN) search: specialized in-memory indexes (IVF, HNSW) that return the closest vectors in milliseconds by being approximately right instead of exhaustively exact.

How to read this: Each step opens with a real design decision — you make the call before I show you what ships. Watch the diagram grow, hover any box, replay the flow. At the end, starve the search to feel ANN’s quiet failure. Hit Begin.

Step 1 · The skeleton

A query vector in, k neighbours out

A service has a query vector and wants the k most similar stored vectors. What sits between the request and the answer?

Design decision: What’s the minimal shape of a vector-search request?

The call: Send {query vector, k, filters} to a Query API that ranks by distance. — A coordinator accepts the vector, k, and any metadata filters, searches the index, and returns the k nearest ids with their similarity scores.

A Query API takes a query vector, k, and optional filters, plans the search, and returns the k nearest ids + scores. Everything else — indexes, segments, shards — hangs off this one contract.

Search is the product: A vector DB is read-optimized: writes feed an index whose only job is to make "k nearest" fast. Keep that contract in mind — every later piece exists to serve it.

Step 2 · Store what you search

Ingest, segments, and the index

Vectors arrive from an embedding pipeline. You need to both keep them (to return and re-rank) and index them (to search). How does the write path work?

Design decision: A new batch of vectors arrives. Where do they go?

The call: Append the full vectors to a segment, and insert them into the index. — The ingest path does both: the raw vectors land in an immutable segment (for exact re-rank and rebuilds), and each vector is inserted into the ANN index (a cell assignment or graph links).

An Ingest / Upsert path writes each vector twice: the full vector into an immutable Vector Segment (for exact re-ranking and index rebuilds), and a pointer into the ANN Index (graph links or a cell assignment). Segments merge in the background, so live search never blocks on writes.

Index vs. store: Two jobs, two structures: the segment is the durable source of truth (full precision); the index is a fast, rebuildable accelerator. You can always rebuild the index from segments — never the other way around.

Step 3 · The naive baseline

Exact kNN and the distance metric

To rank by similarity you need a number for "how close." And the obvious algorithm — compare the query to every vector — is correct. Why can’t you just ship that?

Design decision: What kills brute-force exact nearest-neighbour at scale?

The call: It’s O(N·d) per query — a billion 768-D dot products every search. — Exact search compares the query to every vector. At a billion vectors × hundreds of dims, that’s billions of multiply-adds per query — far too slow and CPU-hungry to serve online.

Similarity is a distance metric — usually cosine (angle) or dot product for embeddings, L2 for some. Exact kNN computes it against every vector and keeps the top k: correct, and O(N·d) per query. That’s the baseline — and why everything past here is about avoiding it.

Pick one metric, normalize: The metric must match how the embeddings were trained (cosine vs dot vs L2). Normalize vectors so cosine becomes a dot product, and so every later shortcut measures the same "close."

Step 4 · Partition the space

IVF — search a few cells, not all

Exact search looks at all N vectors. But the answer is almost always near the query. How do you skip the 99% of vectors that are obviously far away?

Design decision: How do you avoid scanning vectors that can’t be the answer?

The call: Cluster vectors into cells; at query time, scan only the nearest few cells. — IVF runs k-means to make ~√N centroids ("cells"). A query finds its closest centroids and scans only the vectors in those nprobe cells — a fraction of N.

IVF (inverted file) clusters the vectors with k-means into many cells, each with a centroid. A query measures distance to the centroids, picks the closest nprobe cells, and scans only those. Visit more cells → higher recall, slower; fewer → faster, lower recall.

nprobe is the speed/recall dial: IVF turns "scan N" into "scan N·(nprobe/ncells)." The trade-off is explicit: nprobe is a knob you tune against a measured recall target — the same knob the chaos button will yank.

Step 5 · Walk a graph instead

HNSW — navigable small worlds

IVF is great, but its recall plateaus and it needs retraining as data shifts. What if, instead of partitioning space, you could walk straight to the neighbourhood from any starting point?

Design decision: How can you reach a query’s neighbours in a few hops, from anywhere?

The call: Link each vector to its near neighbours in a multi-layer graph, then greedily hop closer. — HNSW builds a layered "small-world" graph: sparse long-range links up top for big jumps, dense local links below for precision. Search greedily hops to ever-closer nodes.

HNSW (Hierarchical Navigable Small World) links each vector to a handful of near neighbours across layers: a sparse top layer for long jumps, denser lower layers for fine approach. Search enters at the top, greedily hops to closer nodes, and descends — reaching the neighbourhood in a few hops. efSearch controls how many candidates it keeps in flight: the speed/recall dial.

Six degrees, for vectors: Small-world graphs give short paths between any two points. HNSW exploits that: O(log N)-ish hops to the neighbourhood, high recall, no retraining — at the cost of more RAM for the edges.

Step 6 · Make a billion fit

Product quantization compresses the vectors

A billion 768-D float32 vectors is ~3 TB — it won’t fit in RAM, and the index needs RAM to be fast. How do you shrink the vectors without losing the ability to rank them?

Design decision: How do you fit billions of vectors in memory and still rank by distance?

The call: Split each vector into sub-vectors and replace each with a learned codebook id (PQ). — Product quantization chops the vector into m sub-vectors, k-means-clusters each subspace, and stores tiny codebook ids. A 768-D float32 vector (3 KB) becomes ~64–96 bytes — distances estimated from the codes.

Product Quantization (PQ) splits each vector into m sub-vectors, learns a small codebook per subspace, and stores only the codebook ids — turning a 3 KB vector into ~64–96 bytes. Search estimates distances from the codes (fast, approximate), then re-ranks the top candidates against the full vectors in the segments for precision.

Compress to search, store to verify: Two tiers: compressed codes (in RAM) make the index huge-but-fast and approximate; the full vectors (in segments) re-rank the shortlist exactly. It’s the recall/precision split again — applied to memory.

Step 7 · Nearest, but filtered

Metadata filters and the recall trap

Real queries aren’t just "nearest" — they’re "nearest where tenant = X and recency < 30 days." Bolt a filter onto ANN search naively and recall quietly tanks. Why?

Design decision: You want the nearest vectors that also match a metadata filter. What goes wrong?

The call: Combine them — pre-filter selective queries, over-fetch + post-filter loose ones, or filter inside the index. — There’s no single winner: the planner picks a strategy by selectivity, often over-fetching (k′ > k) before filtering, or evaluating the predicate during graph/cell traversal.

The Query API consults a Metadata Store and picks a strategy by selectivity: pre-filter when the predicate is selective (search only matching rows), over-fetch then post-filter when it’s loose (grab k′ ≫ k so enough survive), or push the predicate into the traversal. Naive post-filtering is the classic way to silently return too few, wrong results.

Filtering is a planner problem: The right plan depends on how many rows the filter keeps. That’s why production vector DBs ship a little query planner — "nearest + filtered" is two problems that fight, and selectivity decides the winner.

Step 8 · Past one machine

Shard, replicate, scale

One billion-vector index outgrows a single box’s RAM, and one replica can’t serve the QPS. How do you grow past one machine without breaking "k nearest"?

Design decision: How do you scale a vector index beyond one machine’s memory?

The call: Partition vectors across shards; scatter the query to all, gather and merge top-k. — A Shard Router fans the query out to every shard, each returns its local top-k, and the coordinator merges them into the global top-k. Add read replicas per shard for QPS and availability.

A Shard Router partitions vectors across shards and runs scatter/gather: every shard searches its slice and returns a local top-k; the coordinator merges them into the global top-k. Replicate each shard for QPS and failover. Because nearest-neighbour is mergeable, the global answer is just the best of the locals.

Top-k merges cleanly: Unlike a global SQL sort, k-NN distributes beautifully: the global top-k is always contained in the union of per-shard top-ks. Scatter wide, gather, merge — done.

The payoff

You built a vector database

From "find the closest of a billion vectors" to a system that does it in milliseconds: a read-optimized Query API, an ingest path feeding immutable segments and an ANN index, IVF cells or an HNSW graph for sub-linear search, product quantization to fit RAM, a filter-aware planner, and scatter/gather sharding.

Now starve the search — drop nprobe/efSearch to the floor — and watch recall collapse with no error at all: the index returns k vectors, just not the nearest ones. That’s why you measure recall@k against an exact baseline instead of trusting that "it returned results."

  • Query API — one contract: query vector + k + filters → nearest ids
  • Segments vs index — full vectors are truth; the ANN index is a rebuildable accelerator
  • Distance metric — cosine/dot/L2 — normalize, and match the embeddings
  • Exact kNN — correct but O(N·d) — the baseline you approximate
  • IVF — k-means cells; nprobe trades recall for speed
  • HNSW — layered small-world graph; ~log N hops, efSearch dial
  • Product quantization — compress to fit RAM, re-rank exactly on the shortlist
  • Filters + sharding — a selectivity-aware planner; scatter/gather mergeable top-k
built to be reasoned about, not memorized — make the calls, starve the search, run the quiz.
Finished this one? 0 / 61 AI System Designs done

Explore the topic

See this alongside everything else on the same subject — handbooks, system designs, challenges and tools, in one place.

More AI System Designs