Design problems
AI-era systemsHardSDE-3 40 min

Design a vector similarity search service

Asked at
AmazonFlipkartGoogleMeeshoMetaMicrosoft

The interviewer asks

“Design an internal vector search service (like Pinecone or Milvus) that teams use for semantic search, recommendations and RAG. It must store 1 billion 768-dimension embeddings across many collections, answer top-k queries with metadata filters in under 50 ms, and make new or updated vectors searchable within seconds.”

What a strong answer looks like

A strong answer starts from the memory math, because it drives everything: 1B × 768 × 4 bytes is about 3 TB of raw floats, so you cannot simply hold full-precision HNSW for everything in RAM. Choose the index per collection: HNSW for latency-critical, mid-size collections; IVF-PQ or a disk-based graph (DiskANN-style) with compressed vectors in memory and full vectors on SSD for re-ranking at billion scale. Architecture borrows from search engines and LSM trees: writes go to a log (Kafka), land in a small mutable segment, and are sealed into immutable indexed segments built by separate index nodes and stored in S3; query nodes load segments and scatter-gather across shards, merging top-k. Deletes are tombstones cleaned by compaction. The hard parts are filtered search (pre-, post- and in-graph filtering), recall vs latency tuning (ef_search, nprobe) measured against exact ground truth, tail latency in scatter-gather, and multi-tenancy. Senior candidates separate storage, indexing and query compute so each scales on its own.

Concepts to mention

memory math: vectors × dims × bytesHNSW vs IVF-PQ vs disk-based graphquantization and re-ranking with full vectorsrecall measured against exact searchpre-filter vs post-filter vs in-graph filteringwrite-ahead log with growing and sealed segmentstombstones and compactionscatter-gather with deadlines and hedgingstorage, indexing and query compute separatednamespaces or partitions per tenant

1 Requirements

Functional

  • Create collections with a fixed dimension, distance metric (cosine, dot, L2) and metadata schema
  • Upsert and delete vectors by id with metadata (tenant, category, price, timestamps)
  • Top-k query by vector with metadata filters (equality, ranges, set membership)
  • Hybrid query support: combine with keyword scores supplied by the caller or a sparse vector
  • Per-collection choice of index type and recall/latency settings
  • Snapshots and restore; namespaces per tenant inside a collection

Non-functional

  • 1B vectors total, largest collection 500M; 768 dims
  • Query latency p99 under 50 ms at 5,000 QPS total (top-10, with filters)
  • Recall@10 at least 0.95 versus exact search on the default settings
  • Upserts at 10,000/s sustained; searchable within 5 s (freshness)
  • Durability: no acknowledged write lost; 99.95% query availability
  • Cost: serve the billion-scale tier without holding all full vectors in RAM

Out of scope (say it out loud)

  • Generating embeddings (callers send vectors)
  • Full-text search engine features (BM25 index, analyzers)
  • Training custom embedding models

2 Estimates

Raw vector size~3 TB1B × 768 dims × 4 B (float32) = 3.07 TB
HNSW in RAM (full precision)~3.2-3.5 TB RAMVectors 3 TB + graph links: M=16 → ~32 neighbours at layer 0 × 4 B ≈ 128 B/vector ≈ 128 GB, plus metadata; ~30 nodes with 128 GB RAM before replication. Expensive
Compressed tier~100 GB RAM + 3 TB SSDProduct quantization to 96 bytes per vector (96 sub-vectors × 8 bits): 1B × 96 B = 96 GB in RAM; full vectors on NVMe for re-ranking the top ~100 candidates
Query fan-out~5,000 QPS × 16 shards = 80k shard queries/sLargest collection split into 16 shards of ~31M vectors; each query hits all shards of its collection
Per-shard work~2-5 ms CPU per queryHNSW with ef_search=100 visits a few thousand vectors; at 768 dims with SIMD each distance is ~100 ns → ~1 ms compute plus cache misses
Write throughput10k upserts/s ≈ 31 MB/s10,000 × (3 KB vector + ~200 B metadata) into the log; segment seal every ~1M vectors or 60 s
Index build~1-2 hours per 30M-vector segment set on one nodeHNSW build at 768 dims runs very roughly 5-20k inserts/s on a 32-core node (depends on M and ef_construction): 30M ÷ ~7k/s ≈ 70 min; re-indexing 1B vectors needs a parallel build fleet

3 API

EndpointWhat it does
POST/v1/collectionsCreate a collection: name, dimension, metric, index type (hnsw|ivf_pq|disk), metadata schema, shard count, replicas.
POST/v1/collections/{c}/upsertBatch upsert up to 1,000 vectors with ids and metadata. Returns after the write is durable in the log (with a sequence number).
POST/v1/collections/{c}/deleteDelete by ids or by filter (e.g. tenant_id = X).
POST/v1/collections/{c}/queryBody: vector, top_k, filter, namespace, ef_search/nprobe override, include_metadata, min_sequence (read-your-writes).
POST/v1/collections/{c}/snapshotsCreate a point-in-time snapshot (list of segment files in S3).
GET/v1/collections/{c}/statsVector count, segment count, index build lag, recall probe results.

4 Data model

collections metadataetcd / Postgres
collection_iddimmetricindex_paramsschemashard_mapsegment list per shard with state (growing|sealed|indexed|compacted)

Small, strongly consistent catalogue read by all nodes; changes via the coordinator.

write-ahead logKafka (one topic per collection, partition per shard)
seqop (upsert|delete)idvectormetadata

Durability point for writes. Shards are assigned by hash(id) so an id's upserts and deletes stay ordered.

growing segmentQuery node memory
flat array of recent vectors + metadatabrute-force searched

Holds the last ~1M vectors per shard so new writes are searchable within seconds before any index is built.

sealed segmentsS3 (segment files) + local NVMe cache
segment_idvectors (raw)PQ codesindex (HNSW graph or IVF lists)metadata columnsbitmap indexestombstone bitmap

Immutable files, like Lucene segments. Query nodes download and memory-map them; rebuilding a node is just reloading files.

id → location mapRocksDB on the shard leader
vector_id → segment_idoffset

Needed to turn an upsert of an existing id into tombstone + new insert.

5 Architecture

CLIENTSEDGESERVICESASYNCDATAquery / upsertappend upsertscatter querytail into growing segseal → build indexindexed segmentmerge, drop deletessegment statesmmap segmentsload on assignshard mapSearch / RAG / recsappsQuery proxy (router)Coordinator +catalogueQuery nodes (shards)Index builder nodesCompaction workersKafka write-ahead logetcd: shard map, segsS3: segment filesLocal NVMe cache
ClientEdge / gatewayService / workerQueue / streamCacheDatabase / storageAI / modelThird partyrequestasync (event / queue)

Hover a box to highlight its connections.

6 Request flows

Filtered top-10 query

  1. 1The product-search service sends a query vector, top_k=10 and filter category='shoes' AND price < 3000 to the proxy.
  2. 2The proxy reads the cached shard map for the collection (16 shards, 2 replicas each) and sends the query to one replica of each shard in parallel, with a 40 ms deadline.
  3. 3Each query node estimates filter selectivity from its bitmap indexes: 'shoes AND price < 3000' matches 4% of vectors, so it uses in-graph filtering in HNSW (skip non-matching nodes but keep traversing), with ef_search raised for selective filters.
  4. 4Each node also brute-force scans its growing segment (recent writes) with the same filter, merges, and returns its local top 10 with distances.
  5. 5For PQ-compressed segments the node first finds top 100 by approximate distance, then re-ranks them with full vectors read from NVMe.
  6. 6The proxy merges 16 lists of 10 into a global top 10 and returns ids, scores and requested metadata; total ~20 ms p50, ~45 ms p99.

Upsert becomes searchable

  1. 1A caller upserts 500 vectors; the proxy hashes each id to a shard and appends to that Kafka partition; once Kafka acks (replicated), the API returns with the sequence number.
  2. 2Query nodes serving that shard tail the partition and append the vectors to the growing segment, marking older versions of the same id as deleted in their tombstone bitmap. Searchable in ~1-2 s.
  3. 3When the growing segment reaches 1M vectors or 60 s, it is sealed; an index builder reads it, builds HNSW or IVF-PQ, writes segment files to S3 and updates etcd.
  4. 4Query nodes load the new indexed segment and drop the growing copy; queries switch atomically by segment version.

7 Deep dives

Choosing the index: HNSW, IVF-PQ, disk graphs

HNSW is a layered proximity graph: search starts at the top layer and greedily moves to closer neighbours, descending layers. It gives the best recall-latency trade-off in memory (recall 0.95+ at ~1-5 ms), supports inserts incrementally, but needs full vectors plus graph links in RAM and handles deletes poorly. IVF clusters vectors with k-means into, say, 65,536 lists; a query scans only the nprobe nearest lists. Combined with product quantization (each vector compressed to 64-128 bytes, distances computed from lookup tables), it fits a billion vectors in ~100 GB of RAM, at the cost of lower raw recall, which you recover by re-ranking the top 100-200 candidates with full vectors from SSD. Disk-based graphs (DiskANN-style) keep compressed vectors in RAM and the graph plus full vectors on NVMe, giving good recall at billion scale with a few SSD reads per query. Scalar int8 quantization is a cheap middle step: 4× smaller with ~1% recall loss. So pick per collection: HNSW float or int8 for collections under ~100M with tight latency, IVF-PQ or disk graph with re-ranking for the 500M collection. Always report recall measured against exact search, never just latency.

Filtering without losing results

Real queries carry filters: tenant, category, in-stock, language. Post-filtering (search top 100, then drop non-matching) fails when the filter is selective: if only 1% match, the top 100 may contain one valid result. Pre-filtering (compute the matching id set, then search only those) is exact but HNSW cannot be restricted cheaply, so with few matches you brute-force them, which is fast for up to ~50-100k candidates. In-graph filtering checks the filter during traversal and keeps walking past non-matching nodes; it works well for moderate selectivity but the graph can become disconnected for very selective filters. A good engine has a small query planner: estimate selectivity from bitmap or inverted indexes on metadata, then choose brute-force on the filtered set (very selective), in-graph filtering (moderate), or plain search with a larger k and post-filter (filter matches almost everything). For hard partitions like tenant, do better than filtering: use namespaces or partition keys so each tenant's vectors live in their own segments. Test recall separately per filter selectivity bucket, because averages hide the failures.

Writes, deletes and freshness: an LSM-style design

Graph indexes are expensive to update in place, and concurrent inserts into one big HNSW graph limit throughput. So treat the index like an LSM tree or Lucene. Writes are appended to a durable log (Kafka) partitioned by shard; that ack is the durability guarantee. Query nodes tail the log into a small growing segment searched by brute force, which makes writes visible in seconds. Sealed segments are indexed by separate builder nodes so heavy index builds never steal CPU from queries, and the resulting immutable files go to S3. An upsert of an existing id becomes 'tombstone old + insert new'; queries skip tombstoned ids through a bitmap. Compaction merges small segments and physically drops deleted vectors, keeping segment count low (too many segments means too many searches per query). For read-your-writes, the upsert returns a sequence number and the caller can pass min_sequence on a query; the node waits until it has consumed that offset. Because segments are immutable and stored in S3, adding a replica or recovering a crashed node means downloading files, not rebuilding indexes, and snapshots are just a list of segment ids.

Sharding, scatter-gather and tail latency

With random (hash-of-id) sharding, every query must ask every shard, and the query is as slow as the slowest of 16 nodes. If each shard has p99 of 20 ms, the chance that at least one of 16 is slow is high, so tail latency grows with fan-out. Mitigations: keep shard count moderate by using bigger nodes; hedge requests (send to a second replica if the first has not answered in the p95 time); give each shard a deadline and return partial results with a flag rather than timing out; and keep segment counts low through compaction. Ask each shard for slightly more than k (e.g. 1.5k) only if results will be filtered further. Alternatively, partition by a semantic key (cluster centroids, like IVF across machines) so a query visits only the nearest few shards; this cuts fan-out but creates hot shards for popular regions of the space. For multi-tenant collections, partitioning by tenant gives the best of both: a tenant's query touches only its shards. Separate compute roles (proxy, query nodes, builders) let you scale QPS by adding query replicas without touching ingestion.

8 Trade-offs

Recall vs latency vs cost

Raising ef_search or nprobe increases recall and latency together; PQ lowers memory cost and recall. Expose these as per-collection settings with sane defaults, and publish a measured curve (recall@10 vs p99) so teams choose consciously. RAG with a reranker after retrieval can tolerate 0.90 recall; deduplication of near-identical images may need 0.99.

Build vs use pgvector, OpenSearch or a managed vector DB

Most teams under ~50M vectors are well served by pgvector or OpenSearch k-NN. A dedicated service is justified at billion scale, many teams, strict latency, or cost pressure on RAM. In an interview, say when you would not build it.

Immutable segments vs in-place index updates

Immutable segments give simple replication, snapshots and recovery, and no locking on the hot graph, at the cost of compaction work and slightly higher query cost across several segments. In-place updates give fewer segments but complex concurrency and slow recovery.

9 Bottlenecks

Re-embedding a whole collection

When a team switches embedding models, 500M vectors must be rewritten and re-indexed. Create a new collection, bulk-load through builders (bypassing the growing segment), dual-write during the switch, then swap an alias. Budget builder capacity for it.

Hot collection or tenant

One collection can take most of the QPS during a sale. Add query replicas for its shards only, and enforce per-collection QPS quotas at the proxy so it cannot starve others.

10 Failure handling

Query node crash

The proxy routes to the other replica of each shard; the coordinator assigns the shard to a new node, which downloads segment files from S3 and replays the log from the last sealed offset. Recovery takes minutes, with no data loss.

Index builder falls behind

Growing segments get larger, brute-force cost rises and latency climbs. Alert on build lag, cap growing segment size, and apply backpressure on upserts (429) before queries degrade.

Recall silently drops

A bad parameter change or quantization can cut recall without errors. A background prober runs 1,000 sample queries per collection hourly against exact search and alerts if recall@10 falls below target.

11 What impresses

  • Starts with the 3 TB calculation and lets it drive index choice
  • Explains why post-filtering fails for selective filters and proposes a planner
  • Uses an LSM/Lucene-style segment model for fresh writes and cheap recovery
  • Measures recall continuously with a prober, not just at launch
  • Discusses tail latency from fan-out and uses hedged requests
  • Says when a team should just use pgvector instead

12 Mistakes

  • Assuming one HNSW index in RAM scales to a billion vectors cheaply
  • Quoting latency without recall
  • Post-filtering every query and losing results for small tenants
  • Updating a huge graph in place under heavy writes
  • Ignoring deletes and re-embedding
  • Rebuilding indexes on the query nodes during peak traffic

13 Follow-ups

Q1How would you support hybrid dense + sparse (keyword) scoring?
Q2How do you guarantee a user's write is visible in their next query?
Q3How would you handle GDPR or DPDP delete requests across segments and snapshots?
Q4How would you use GPUs for indexing or search?
Q5What changes if the main use is image deduplication needing 0.99 recall?
Q6How would you charge internal teams for usage?

Saved on this device. Create a free account to keep it on every device.

Practise this with Sam

A real mock interview, out loud, with an honest scorecard. Free to start.

Start free