Vibe Engines
YouTube
AI System Design

Design a Vector Database

Learn AI system design by building a vector database step by step.

The numbers to beatimmutablesegments, merged in bg2 writessegment + indexrebuildableindex derives from segments

The whole design, in writing

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.

Every step of the build above, written out: the problem each piece solves, the option that was taken and the ones that were not, the numbers, and how it fails in production.

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?

Applicationfind similar
New in this step: Application.

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.

What the new pieces do

Applicationclient
A service holding a query vector (an embedded question, image, or user) that needs the k most similar vectors out of millions or billions.

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?

Applicationfind similarQuery APIcoordinator
New in this step: Query API.

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

  1. Equality finds an identical vector, not a similar one — and two embeddings of the same idea are almost never bit-identical. You need ranking by distance, not exact match.

  2. A coordinator accepts the vector, k, and any metadata filters, searches the index, and returns the k nearest ids with their similarity scores.

  3. That’s exact kNN — correct but O(N) per query. Fine for 10k vectors, hopeless at a billion. It’s the baseline we’ll approximate, not ship.

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.

Why this piece earns its place

The contract is the part you cannot change later, so three things in it are worth being deliberate about. First, return ids and scores, not stored payloads. Callers will ask for payloads to save a round trip, and the moment you agree, the vector database is also a document store, with its size limits, its consistency questions and its own compaction problems. Second, the score is index-dependent. A client that hardcodes "drop anything below 0.8" has bound itself to the metric, to normalization and, once compression arrives, to the codebook, so retuning any of those stops being a change on your side and becomes a coordinated release across every caller that ever wrote a threshold down. Publish ranks, or a score carrying the metric that produced it. Third, there is no cheap pagination. This index answers "the k nearest" and nothing else, so "results 100 to 200" means re-running the whole search with k at 200 and throwing most of it away. Cost grows with the offset, not the page. Settle that infinite scroll is not a feature here before someone designs a screen assuming it is.

What the new pieces do

Query APIbackend
Accepts a query vector + k + filters, plans the search across shards, merges results, and returns the nearest neighbours with their ids and scores.

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?

ApplicationQuery APIVector SegmentsIngest / UpsertEmbeddings
New in this step: Vector Segments, Ingest / Upsert, Embeddings. · swipe to pan the diagram

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

  1. In-place edits to a graph/cell index under concurrent search cause lock contention and fragmentation. Most vector DBs write to immutable segments and merge in the background.

  2. Rebuilding per query throws away the whole point of an index. The structure must persist between queries and grow incrementally.

  3. 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.

Why this piece earns its place

Everything above describes adding. Removing is where this design grows teeth. A segment is immutable, so a delete is a tombstone, and the index cannot honour it either: pulling a node out of an HNSW graph severs the edges its neighbours used to reach that whole region, so implementations leave the node in place and filter it out of the answer. Deleted vectors therefore keep occupying RAM and keep being traversed, and an upsert of an existing id is a delete plus an insert — which makes re-embedding a corpus, the most ordinary operation there is, the worst case for both. Ask when the data is really gone and the honest answer is the merge schedule, so that schedule is a commitment made in the design rather than a knob ops picks later. The other thing worth being explicit about is that the two writes land at different times. Durable and searchable are separate guarantees here, and batching index inserts widens the gap on purpose, so the contract has to say which of the two a successful upsert has bought.

  • immutablesegments, merged in bg
  • 2 writessegment + index
  • rebuildableindex derives from segments

What the new pieces do

Vector Segmentsstore
The full-precision vectors and ids on disk/RAM, organised into immutable segments. Used to re-rank candidates exactly and to rebuild the index.
Ingest / Upsertbus
The write path: validate incoming vectors, append them to a segment, and insert them into the index (graph links or cell assignment).
Embeddingsstore
Vectors produced upstream by an embedding model (text, images, users). The vector DB stores and searches them — it doesn’t create them.

Back of the envelope

append-only segments
no in-place mutation under live search
background merge
compact small segments, drop deletes
index follows store
segment is truth; index is an accelerator
batch upserts
amortize index-insert cost over many vectors

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?

Query APIANN IndexVector SegmentsIngest / Upsert
New in this step: ANN Index. · swipe to pan the diagram

What kills brute-force exact nearest-neighbour at scale?

  1. 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.

  2. Exact kNN is, by definition, correct — it’s the ground truth other methods are measured against. Its problem is cost, not correctness.

  3. Distances compute fine in high-D; the issue is doing N of them per query. (High-D does make the geometry harder — which is why approximate indexes work so well.)

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.

Why this piece earns its place

Keeping exact search runnable is not sentiment — it is the measuring instrument for everything after it, and instruments need owners. The part teams get wrong is the query sample. Recall measured on random vectors flatters the index: a random point in 768-D space usually has one unambiguous nearest neighbour, while real queries land where the data is dense and the top k are near-ties, which is exactly where an approximate index drops one. So the sample is real traffic, frozen so that two runs are comparable, and refreshed deliberately when the query mix moves rather than whenever someone reruns a notebook. It is also the O(N·d) cost you just refused to serve, paid offline every time the corpus changes, which makes the sample size a budget line rather than a detail. One more property this step fixes permanently: the metric is a build-time choice, not a request parameter. An index built for dot product does not answer L2 questions, so picking wrong is not a config change, it is a rebuild of everything standing on it.

  • cosine / dottypical for embeddings
  • O(N·d)exact, per query
  • ground truthwhat recall is measured against

What the new pieces do

ANN Indexindex
The heart of the system: an in-memory structure (IVF cells or an HNSW graph) that answers approximate nearest-neighbour search in sub-linear time.

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?

ApplicationQuery APIANN IndexVector SegmentsIngest / UpsertEmbeddings
The system as it stands at this step. · swipe to pan the diagram

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

  1. There’s no single ordering of points in 768-D space — "sorted" has no meaning across many dimensions. You need spatial partitioning, not a 1-D sort.

  2. 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.

  3. Random sampling misses the actual neighbours most of the time. The cells aren’t random — they’re chosen so nearby vectors share a cell.

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.

Why this piece earns its place

Two properties of cells matter more than the arithmetic. They are contiguous: the vectors in a cell sit together, so probing one is a sequential read, which makes IVF the easier index to serve from disk or a memory map — not the only one, since graph indexes have been re-engineered for SSD residency, but the one that gets there without special layout work. They are also independent, so probed cells can be scanned on separate threads and the search parallelizes within a single query, which a greedy sequential walk cannot. The part that surprises people is the first stage. Finding the nearest nprobe centroids means comparing the query against every centroid, and at a billion vectors the square root of N is tens of thousands of them — a brute-force scan in miniature, the thing cells were built to avoid. So the cell count is a trade between the two stages, not a dial to turn up: more cells shorten every posting list and lengthen the centroid scan, and past a certain size you index the centroids themselves with a small graph, which is why real systems compose IVF and HNSW instead of choosing between them.

  • ~√Ncells (k-means)
  • nprobecells scanned per query
  • recall ⇄ speedset by nprobe

Back of the envelope

train centroids
k-means on a sample defines the cells
N=1M vectors → √N≈1,000 cells; nprobe=32 scans ~32,000 of them
≈3.2% of the store — about 31× fewer vectors than an exact scan
imbalanced cells hurt
periodically retrain as data drifts
measure recall@k
against an exact baseline, not by vibes

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?

ApplicationQuery APIANN Index
New in this step: ANN Index → Query API, Query API → Application. · swipe to pan the diagram

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

  1. A complete graph is O(N²) edges — impossibly large and slow to traverse. You need few, well-chosen links, not all of them.

  2. k-d trees degrade badly in high dimensions — they end up visiting most of the tree. They work in 2-D/3-D, not in 768-D embedding space.

  3. 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.

Why this piece earns its place

The distinction to hold onto is which of these parameters you can still change after launch. efSearch is per request: a query that comes back unconvincing can be retried wider, and two callers can buy different accuracy from one index. M and the construction effort cannot — they are baked into the edges, so changing your mind means building the graph again. And building it is expensive in a particular way: construction is essentially one search per inserted vector, so a rebuild is the same order of work as answering every query in the corpus once. That is what makes HNSW’s headline property load-bearing rather than merely convenient. Taking inserts without retraining matters because the escape hatch of starting over is priced out of reach. The edges also set the floor on memory, and they will not page gracefully — consecutive hops land in unrelated parts of the adjacency lists, so there is nothing to prefetch. They are per node, and no amount of vector compression touches them, so once the vectors themselves are squeezed the graph decides how large a machine has to be. At a billion vectors the question stops being whether the vectors fit and becomes whether their links do.

  • ~O(log N)hops to the region
  • M linksper node per layer
  • efSearchcandidates in flight

Back of the envelope

M ≈ 16–64
more links = better recall, more memory
N=1M vectors → log₂(N) ≈ 20 hops to the neighbourhood
versus touching all 1,000,000 vectors in an exact scan
efSearch tunes recall
higher = more accurate + slower
great for updates
insert links incrementally, no retrain
RAM-hungry
the graph edges live in memory

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?

ANN IndexHNSW + PQVector Segmentsfull vectorsQuantizerPQ codes
New in this step: Quantizer.

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

  1. Truncating dimensions throws away the information the distance depends on — recall craters. You want to compress, not amputate.

  2. 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.

  3. Generic compression saves disk but you must decompress to compute distance — no speedup for search. PQ lets you estimate distance directly from the compressed 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.

Why this piece earns its place

The first question is whether you need this at all. Below the memory ceiling it buys nothing but risk, and the cheaper move most systems should make first is scalar quantization — store each dimension as an int8 rather than a float32 for a flat four-fold saving, with no codebook and far less distortion than an aggressive PQ code. It is not free of training, though: fixing the range each dimension maps onto takes a calibration pass over the data, and that range ships with the codes, because it is the only thing that says what an int8 means. Reach for PQ when four-fold is not enough, and understand that you are taking on the same problem in a larger form. The codebook is the only thing that can interpret the codes, so it must be versioned and shipped with the segments it describes; a codebook restored from a different training run decodes the same bytes into different vectors, and nothing errors. Then there is the read side. Pulling full vectors back means a random read per candidate, so re-rank depth is priced in seeks rather than compute — a reason to lay segments out so a shortlist is a few sequential reads instead of a hundred scattered ones.

  • ~3 KB → ~96 Bfloat32 → PQ code
  • fits in RAMbillions of codes
  • re-rankexact, on the top few

What the new pieces do

Quantizerservice
Product quantization: compress each vector into a short code so billions fit in RAM, at the cost of a little precision (recovered by an exact re-rank).

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?

Query APIcoordinatorANN IndexHNSW + PQMetadata Storefilters
New in this step: Metadata Store.

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

  1. Post-filtering: if few of the top-k match, you’re left with 1–2 results — you asked for k and the filter ate them. Fine only when the filter is loose.

  2. Pre-filtering: correct, but if the filter still matches millions you’re back to brute force. Great for very selective filters, costly for loose ones.

  3. 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.

Why this piece earns its place

The first thing to settle is which attributes can be filtered at all, because that is decided when the index is built. Testing a predicate mid-traversal only works if the attribute sits beside the vectors, as a bitmap or a small column inside the index process; anything living only in the metadata store costs a lookup per candidate, which a post-filter over a shortlist absorbs and a walk cannot. So the fast filters are the ones you chose to copy in, and adding one later is an index rebuild, not a schema migration. The second is that filtering inside a graph fights the graph. Its links were chosen over the whole dataset, and restricting the walk to matching nodes deletes most of them; a small-world graph missing most of its edges is no longer navigable, so the search strands in a region with no eligible neighbours. Implementations keep walking through non-matching nodes as stepping stones and collect only the matching ones. All of which makes the filter language an API commitment: a closed set of indexed attributes stays fast for years, while arbitrary predicates promise a planner, and the statistics to feed it, for as long as the product lives.

What the new pieces do

Metadata Storestore
Per-vector attributes (tenant, source, recency, tags) used to constrain a search — "nearest, but only within these rows".

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"?

Query APIcoordinatorShard Routerscatter / gatherANN IndexHNSW + PQ
New in this step: Shard Router.

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

  1. Metadata sharding helps multi-tenant isolation but skews load and doesn’t help a single huge tenant. The general answer is to partition the vectors themselves and search all shards.

  2. 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.

  3. Vertical scaling hits a RAM ceiling and a single-point-of-failure wall. Past a point you must distribute the index across machines.

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.

Why this piece earns its place

The sentence to have ready is that sharding buys memory, not throughput. Because every query is scattered to every shard, each shard sees the full query rate however many shards there are; doubling the shard count halves the vectors per machine and changes the queries per machine not at all, so the fleet is shards multiplied by replicas. The second consequence is the tail. A scatter-gather response is as slow as its slowest shard, so p99 is a maximum over shards rather than an average, and it gets worse as you add them — the familiar reason a well-behaved service degrades as it grows. What rescues it here is that k-nearest is unusually forgiving: a shard that misses its deadline costs recall, not correctness, so you can hedge the request, or merge without that shard and report the degradation, which beats making every user wait on the unlucky machine. The precondition nobody writes down is that merging assumes the scores are comparable. Best-of-the-locals holds only while every shard shares one metric, one normalization and one generation of the index — which makes an index rebuild a fleet-wide event rather than something you roll one machine at a time.

  • scatter / gatherquery all shards, merge
  • replicasQPS + failover
  • mergeableglobal top-k = best of locals

What the new pieces do

Shard Routerservice
Fans the query out to every shard, then merges their local top-k into a global top-k. Lets the index grow past one machine’s RAM.

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.

ApplicationQuery APIShard RouterANN IndexMetadata StoreVector SegmentsQuantizerIngest / UpsertEmbeddings
The finished design, end to end. · swipe to pan the diagram

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."

Everything you assembled, in order

  • 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

Deep cut · 20:25

It never finds the nearest vector — and it is not trying to

The interactive build above walks the index: distance metrics, IVF cells, the HNSW graph, product quantization, filtering and shards. This film starts from the search that opens eight cells out of four thousand, watches the genuinely closest document sit one border line away in a cell that never opens, and then prices every knob — nprobe, efSearch, bytes per vector — as what it actually is: a bill for accuracy.

  • See the silent miss: nothing errors, nothing is logged, and the assistant reading those ten documents answers without the one that held the answer — sounding exactly as certain either way.
  • Take it into the interview: justify approximate search from the three terabytes an exact one would stream, then defend nprobe, efSearch and the rerank on real numbers as three separate purchases.

Where an interviewer pokes next

Getting the boxes right is the easy half. These are the questions that separate a candidate who drew the diagram from one who has run the thing. Answer each one out loud before you open it.

  1. The data distribution shifts significantly after IVF’s centroids were trained — new content clusters that didn’t exist before. What happens to recall?

    Recall degrades because the k-means cells no longer match where the data actually lives — new clusters of vectors get scattered awkwardly across cells trained on the old distribution, some cells become oversized (defeating the point of partitioning) while others sit nearly empty. This is why IVF needs periodic retraining as data drifts, unlike HNSW, which absorbs new data incrementally without ever needing to "retrain" its structure from scratch.

  2. HNSW supports incremental inserts without retraining. Does inserting into a live graph under concurrent search need locking?

    Yes, at least locally — inserting a new node means writing new edges into its nearby neighbors’ adjacency lists, and a concurrent search traversing through exactly those nodes could read a half-updated edge list. Production implementations use fine-grained locking (per-node, not a global lock) or lock-free/copy-on-write techniques so inserts don’t stall the whole graph’s search traffic, but "incremental" doesn’t mean "free of concurrency concerns."

  3. Is there one shared PQ codebook for the whole dataset, or a separate codebook per cluster?

    Both variants exist, with a real tradeoff: one global codebook is simpler and cheaper to store, but a single set of codebook centroids has to represent the whole vector space’s variation, which loses precision if the data has very different characteristics in different regions. Per-cluster (or per-IVF-cell) codebooks specialize to local data statistics for better compression accuracy, at the cost of more codebooks to store and manage — it’s the same "one global model vs. many local models" tradeoff that shows up everywhere in ML systems.

  4. For post-filtering with over-fetch (grab k′ ≫ k, then filter), how do you pick k′ — and what happens if even k′ doesn’t leave enough matches?

    k′ is typically set based on the filter’s estimated selectivity from the metadata store’s statistics (a filter expected to keep 10% of rows might over-fetch 10× the requested k), but if the actual selectivity is far worse than estimated — or the true matches simply aren’t in the ANN’s approximate top candidates at all — post-filtering can still return fewer than k results. That failure mode is exactly why very selective filters route to pre-filtering instead: over-fetching has no guarantee, pre-filtering (searching only the eligible rows) does.

  5. Semantic search combines dense vectors with BM25 lexical search. Does a vector database natively support that hybrid, or is it bolted on separately?

    It varies by product — some vector databases have added a built-in sparse/lexical index alongside the dense ANN index specifically to support hybrid search natively (fusing results server-side), while others stay dense-only and expect the application layer to run a separate lexical search system and fuse results itself, the way the semantic-search system design’s architecture does. There’s no universal answer here; it’s a real feature-set difference between vector database products worth checking before assuming hybrid is built in.

Check yourself — the answers, and why

Nine steps in, these are the calls you should be able to make cold. Pick one, then read why.

  1. A vector database exists mainly to…

    It’s built around approximate nearest-neighbour search — embeddings are produced upstream; the DB stores and searches them fast.

  2. Exact brute-force kNN is rejected at scale because…

    Exact kNN is the correct ground truth; its problem is cost — a distance against every vector, every query.

  3. In IVF, raising nprobe…

    nprobe is the speed/recall dial: more cells visited means more of the true neighbours found, at more compute.

  4. HNSW finds neighbours quickly by…

    A multi-layer navigable graph gives ~log N hops to the neighbourhood; efSearch trades recall for speed.

  5. Product quantization helps because it…

    PQ stores tiny codebook ids and estimates distance from them; the full vectors in segments re-rank the shortlist.

  6. ANN’s signature failure mode is…

    Too small an nprobe/efSearch returns ranked results with no error that simply aren’t the closest — measure recall@k to catch it.

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.

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.

The qualities that shape everything

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 graph over 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 quantization over 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 planner over 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.

The answer, out loud

What a strong answer to “Design a Vector Database” sounds like, first question to last trade-off. It is about 7 minutes of talking; the whiteboard and the interviewer fill the rest of the 45. Read it aloud once, then close the page and give it yourself.

  1. 0–3 min

    Pin down the one query

    Before I draw anything: what are we building? An embedding model upstream turns text, images or users into vectors — points in a space of a few hundred dimensions, where nearest means most similar. We store those vectors and answer one query: given this vector, return the k closest, with ids and scores, often filtered by tenant or recency. I’d plan for a billion vectors, answered in milliseconds. So the real question is how much accuracy we’ll trade for that speed — and how we’ll know what we traded.

  2. 3–8 min

    One contract, two writes

    The skeleton is a query API in front of an index: a vector, k and filters in, the k nearest ids out. Not a SQL equality match — two embeddings of the same idea are almost never bit-identical, so I rank by distance. Each incoming vector is written twice: the full-precision copy is appended to an immutable segment, and it’s inserted into the index. Editing a live index in place means lock contention, so segments merge in the background. The segment is the truth; the index is an accelerator I can always rebuild.

    Built in step 2: Ingest, segments, and the index
  3. 8–13 min

    The honest baseline

    Next, a definition of close: usually cosine similarity or dot product, matching how the embedding model was trained. I’d normalize the vectors so cosine becomes a dot product and every later shortcut measures the same thing. The obvious algorithm — compare the query to every vector, keep the top k — is exact search, and it’s correct. Its problem is cost: a billion 768-dimensional dot products per query. So I keep it as ground truth, and define recall at k as the share of the true k nearest my fast index actually returns.

    Built in step 3: Exact kNN and the distance metric
  4. 13–19 min

    Only open the nearby cells

    I want to skip vectors that are obviously far from the query. There’s no single ordering of points in 768 dimensions to binary-search, and random sampling misses the real neighbors. The first index is IVF, an inverted file: k-means clustering groups vectors into cells around center points. A query scans only its closest few cells; how many is a knob called nprobe. With a million vectors, a thousand cells and nprobe at 32, I scan about 3 percent. The cost: a true neighbor just across a cell border is missed with no error, and the cells need retraining as the data drifts.

    Built in step 4: IVF — search a few cells, not all
  5. 19–25 min

    Or walk a graph

    IVF’s recall plateaus, so I’d also offer HNSW — a hierarchical navigable small-world graph. Each vector links to a few near neighbors across layers: sparse long jumps on top, dense local links below. Search enters at the top and greedily hops closer, reaching the neighborhood in roughly log N hops — about 20 for a million vectors. A k-d tree ends up visiting most of the tree in high dimensions, and linking everything to everything is N-squared edges. The dial is efSearch, how many candidates stay in flight. HNSW takes inserts without retraining; it costs RAM for the edges.

    Built in step 5: HNSW — navigable small worlds
  6. 25–31 min

    Make a billion fit in memory

    That memory bill matters: a billion 768-dimensional float vectors is about three terabytes. Dropping dimensions throws away what the distance depends on, and gzip must be decompressed before you can compare anything. So I’d use product quantization: split each vector into sub-vectors, learn a small codebook of typical pieces for each, and store only the codebook ids — around 64 to 96 bytes instead of 3 kilobytes. Distances estimated from those codes are approximate, so the top candidates are re-ranked against the full vectors in the segments.

    Built in step 6: Product quantization compresses the vectors
  7. 31–37 min

    Filters, then more machines

    Real queries add a filter — nearest where tenant is X. Filter after searching, and a selective filter can leave one or two results when I asked for ten; filter first, and a loose filter puts me back at brute force. So the query API checks a metadata store and picks by selectivity: pre-filter when it’s tight, over-fetch then filter when it’s loose, or test the predicate during traversal. Past one machine, a shard router scatters every query to all shards. Nearest-neighbor results merge cleanly, so the global top k is the best of the locals, and replicas add throughput.

    Built in step 7: Metadata filters and the recall trap
  8. 37–42 min

    What I’d watch, and how it fails

    On the dashboard: the latency distribution, and recall at k measured against exact search on a sample. The awkward thing about recall is that it has no live signal, so that offline job is part of the service, with an owner and an alert, not something we run after a complaint. I’d also version the index. Every rebuild or parameter change is a new generation, scored on the same frozen query sample before it takes traffic, so a change that costs us recall gets caught by the release rather than by a user. And I’d resist tuning the knobs once at launch and assuming the settings survive the collection growing underneath them.

  9. 42–45 min

    Close on the trade-off

    To close, the design in one breath: one query contract, immutable segments plus an index, IVF cells or an HNSW graph, product quantization with an exact re-rank, a selectivity-aware filter planner, and scatter-gather shards. Every knob — nprobe, efSearch, bytes per vector — trades recall for speed or memory, and recall at k is how I price it. With more time I’d want to talk about the dependency this system doesn’t own: the embedding model upstream chooses the metric and produces every vector we store, so an upgrade there isn’t a deploy, it’s re-embedding the corpus and rebuilding the index behind it. I’d want that path rehearsed on a schedule rather than discovered during one.

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
RUN IT YOURSELF

The nearest vector was in a cell nobody opened

One IVF index over 480 toy vectors, searched two ways — exact brute force, and approximate with an nprobe budget — running for real in your browser. It prints recall@k beside the cost, so you watch accuracy being bought and see the cheapest search hand back a full, ranked page of k while half the true neighbours are missing. Change the nprobe list (or NCELLS) and hit Run.

HOW TO READ THE CODE — 4 IDEAS
  1. Cells partition the corpus: a vector in an unopened cell is not ranked low, it is never compared at all (step 4).
  2. nprobe=1 compares under 9% of the corpus and loses half the true neighbours — with no error, no exception and no warning of any kind.
  3. recall@k only exists because the exact O(N·d) baseline ran too (step 5) and something scored the two against each other (step 7). Production traffic cannot compute it.
  4. Open every cell and recall reaches 1.00, but the distance count now exceeds brute force — the centroid scan is pure overhead. That is starve the search, in both directions.
CPython · WebAssembly
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