System designDeep dive9 min

Vector search & ANN indexes

Keyword search finds the words you typed; vector search finds things that mean the same. A model turns each item into a list of numbers, and search becomes "find the nearest points". Doing that across millions of items in milliseconds takes approximate indexes, which give up a little accuracy for a lot of speed.

1 · The idea

Vector search finds items that mean the same, not items with the same words

A second-hand clothing marketplace has 20 million listings. A shopper types "flowy summer dress". The best match is described as "light linen sundress, loose fit", which shares none of those words, so a keyword index (Search & inverted indexes) never finds it.

An embedding fixes that. An embedding model turns a piece of text, or an image, into a vector: a list of floating-point numbers, such as the 1,536 that OpenAI's text-embedding-3-small returns. The model is trained so that the distance between two vectors measures how related their inputs are. The query is embedded by the same model, and search becomes "find the listings whose vectors are nearest to this one":

"flowy summer dress"Embedding modeltext → 1,536 numbersVector index20 million listingsembednearest 20"flowy summer dress"Embedding modeltext → 1,536 numbersVector index20 million listingsembednearest 20

Every listing was embedded by the same model when it was posted. Vectors from different models live in different spaces, so changing the model means re-embedding all 20 million listings.

2 · Distance

Exact search compares the query with every vector

"Nearest" needs a distance function, and the choice must match how the model was trained:

Measure

What it compares

Notes

Cosine similarity

The angle between two vectors, ignoring length

The usual choice for OpenAI's embeddings

Dot product

Angle and length together

Equals cosine when vectors are normalized to length 1, and is faster

Euclidean (L2)

Straight-line distance between the points

Ranks unit-length vectors the same way as cosine

The exact answer is simple: compute the distance from the query to every listing and keep the closest 20. For 20 million vectors of 1,536 numbers, that's about 30.7 billion multiply-adds per query, reading 123 GB of 4-byte floats. A search box can't do that for every keystroke of every shopper. pgvector, the vector extension for PostgreSQL, does exact search by default, which gives perfect recall, and leaves adding an index to you.

3 · Approximate

Approximate indexes trade a little recall for a lot of speed

An approximate nearest neighbor (ANN) index answers by looking at only a small part of the data, chosen so the true neighbors are very likely inside it. Its quality is measured by recall: of the true 20 nearest listings, how many did the index return? 19 of 20 is a recall of 95%.

Aspect

Exact search

ANN index

Looks at

Every vector

A small part chosen to hold the neighbors

Recall

Always 100%

Below 100%, such as 19 of the true 20

Speed on millions of vectors

Slow

Fast

Tuning

None

A dial trading speed for recall

Approximate indexes behave differently from the B-tree indexes developers are used to (Database indexing):

  • Adding one changes the results. A B-tree makes a query faster and returns the same rows; an ANN index may return slightly different neighbors.
  • Every index has dials. Each has a setting that trades speed for recall at query time, so the same index can be fast and rough or slower and closer.
  • Recall must be measured. Compare the index's answers with exact search on a sample of real queries, and watch it as the data changes.

pgvector and FAISS, Meta's vector search library, both offer two families: graphs, which walk from neighbor to neighbor, and clustering, which only searches the clusters closest to the query. Both can be combined with compression.

4 · HNSW

HNSW walks a layered graph toward the query

HNSW (hierarchical navigable small world) connects each vector to its nearest neighbors in a graph, and stacks several graphs in layers. Every vector is in the bottom layer; each is also placed in higher layers with an exponentially decreasing probability, so the top layers are small and their links span long distances. A search enters at the top, greedily moves to whichever neighbor is closer to the query, and drops a layer when it can't get closer:

Top layerfew vectors, long hopsMiddle layermore, shorter hopsBottom layerall 20 milliondrop downdrop downTop layerfew vectors, long hopsMiddle layermore, shorter hopsBottom layerall 20 milliondrop downdrop down

Like flying to the right city, then driving to the right street: the upper layers get near the query in a few hops, and the bottom layer searches its neighborhood for the nearest 20. The cost grows roughly with the logarithm of the number of vectors.

Three settings control it, shown here with pgvector's defaults:

  • m (16): links per vector in each layer. More links, better recall, more memory.
  • ef_construction (64): candidates considered while inserting. Higher builds a better graph, more slowly.
  • ef_search (40): candidates kept while searching. The query-time dial: raise it for recall, lower it for speed.

HNSW gives better speed for a given recall than IVF, below, and needs no training step, so it can be built on an empty table. The price is memory and build time: it keeps every full vector plus its links, about 125 GB for the marketplace, and builds much faster when the graph fits in memory.

5 · Clusters & compression

Clustering searches a few lists, and compression shrinks each vector to a few bytes

An IVF (inverted file) index groups the vectors into clusters, each with a center point. A search finds the few centers closest to the query and compares the query only with vectors in those clusters. pgvector's starting points are √rows lists for more than a million rows and √lists probes:

Exact search
20,000,000 vectors
IVF: 67 of 4,472 lists
299,000 vectors

√20,000,000 ≈ 4,472 lists and √4,472 ≈ 67 probes: about 1.5% of the vectors per query. Clusters are uneven, so the real share varies; more probes raise recall, and probing every list is exact search again.

IVF builds faster and uses less memory than HNSW but gives lower recall at the same speed. It also needs training: the cluster centers are learned from data, so the index should be built after the table has representative rows.

Memory is the next limit, and quantization addresses it by storing an approximation of each vector. Product quantization (PQ) splits the 1,536 numbers into 64 slices and replaces each slice with the ID of its closest pattern from a learned codebook, one byte per slice. FAISS stores such an index at the code size plus 8 bytes for the ID:

Full vectors (flat)
122.9 GB
HNSW, m = 16
125.4 GB
IVF + PQ, 64-byte codes
1.44 GB

6,144 bytes of floats per listing become 72 bytes. Distances on codes are approximate, so a common pattern is to fetch extra candidates from the compressed index and re-rank them with the full vectors.

6 · Filters & keywords

Filters and keywords make nearest-neighbor search harder

Shoppers rarely want just "similar": they want similar, in size M, under $40, still for sale. With an ANN index the filter is usually applied after the index finds its candidates. The arithmetic: with HNSW's default 40 candidates in pgvector and a filter that matches 10% of rows, about 4 results survive, not 20.

Three fixes, depending on the filter:

  • Search further until enough match. pgvector's iterative index scans keep scanning, up to a limit, until they find enough rows.
  • Pre-filter with a regular index when the filter is very selective, then compare exactly: few rows match, so exact search is cheap.
  • Separate indexes per common filter value, or per tenant. A shared index lets one tenant's vectors affect another's recall.

Keywords still matter: a shopper searching a brand name or "SKU 48213" wants that exact string, which is the keyword index's job. Hybrid search runs both searches and merges the lists. Reciprocal rank fusion needs no tuning: each item scores 1 ÷ (60 + its rank) in each list, summed:

Listing

Vector rank

Keyword rank

Fused score

Floral summer dress

3

2

1/63 + 1/62 = 0.0320

Light linen sundress

1

5

1/61 + 1/65 = 0.0318

Flowy maxi skirt

2

–

1/62 = 0.0161

An item found by both searches beats one found by only one, even if that one ranked higher there. The constant 60 is Elasticsearch's default; a larger constant gives lower-ranked items more say.

7 · In practice

Start inside your database, and measure recall before you tune

Choose by scale and by what you already run:

Option

Fits when

Vector column in your database (pgvector)

Vectors live next to the rows they describe, and filters and joins are plain SQL

A search engine with vector fields

You already need keyword search, and want hybrid ranking in one place

A dedicated vector database or library (FAISS)

Collections too big for one database, heavy compression, or GPU search

  1. Fix the model and the metric

    One embedding model per index, with the distance it was trained for; record the model version with every vector.

  2. Measure recall first

    Exact search on a sample of real queries is the ground truth; tune ef_search or probes until recall is good enough.

  3. Budget memory

    Full vectors × count, plus graph links for HNSW; compress and re-rank if it doesn't fit.

  4. Plan filters

    Iterative scans, pre-filtering or separate indexes, before shoppers see four results instead of twenty.

  5. Plan re-embedding

    A new model means embedding everything again and building a new index beside the old one, then switching.

Check yourself

4 questions

1. A search for "flowy summer dress" should find "light linen sundress". Why can vector search do this when an inverted index can't?
2. An HNSW index returns 18 of the true 20 nearest neighbors. Which setting raises recall at query time?
3. With HNSW's 40 candidates and a filter matching 10% of listings, a query returns about 4 results. What fixes it?
4. Why does product quantization often come with re-ranking?

Takeaways

Remember this

  • An embedding model turns items and queries into vectors, and search becomes finding the nearest vectors under the model's distance.
  • Exact search compares every vector; approximate indexes look at a small part and are judged by recall against exact results.
  • HNSW searches a layered neighbor graph: the best speed for its recall, at the cost of memory and build time.
  • IVF searches only the nearest clusters, and product quantization shrinks vectors to a few bytes, with re-ranking to recover accuracy.
  • Filters applied after the index search starve results, and hybrid search with rank fusion keeps exact keyword matches.