Vector Database Internals โ Algorithms for Approximate Nearest Neighbor Search
How the approximate nearest neighbor (ANN) search at the core of vector databases works, from distance metrics through IVF, Product Quantization, HNSW, and DiskANN, following the papers and implementations such as FAISS.
With the spread of large language models (LLMs), vector databases, which store and search embedding vectors, have become a common foundation for semantic search, recommendation, and Retrieval-Augmented Generation (RAG). The problem they solve internally has been studied for decades: Approximate Nearest Neighbor Search (ANN) in high-dimensional spaces.
ANN gives up exact answers in exchange for speed: it visits fewer vectors, makes each distance computation cheaper, or both. This article explains the core algorithms in order, starting from brute force and moving through IVF (Inverted File), Product Quantization (PQ), HNSW (Hierarchical Navigable Small World), and DiskANN (Vamana). Design decisions are discussed with reference to the papers and to the code of open-source implementations (FAISS, Milvus, pgvector, Qdrant).
The big picture, as a libraryImagine looking for books similar to one you have, in a huge library. Brute force means picking up every book and checking how similar it is; with a billion books, you never finish.
- IVF: each shelf has a label card. You pick the 10 shelves whose labels are closest to your book and search only those.
- Product Quantization: each book's cover is summarized as 64 color codes chosen from a 256-color palette. Comparing codes instead of covers is much faster, at the cost of some accuracy.
- HNSW: similar books are linked as "friends," and you follow the links toward your target, zooming from a sparse world map down to a dense street map.
- DiskANN: the same network of links, stored on SSD. RAM holds only a compressed copy of every book (PQ-compressed vectors), and the originals are read from SSD only for promising candidates.
Show transcript
How does a vector database find similar documents in an instant? From brute force to IVF, PQ, HNSW, and DiskANN, let's trace the machinery behind approximate nearest neighbor search.
An embedding maps text or images into fixed-dimensional real-valued vectors. Items with similar meaning become nearby points in that space. Search becomes a geometric problem: find the points nearest to the query point.
But the dimensionality is commonly above one thousand. OpenAI text-embedding-3-small has 1536 dimensions; text-embedding-3-large has 3072. In high dimensions, distances concentrate, and structures like kd-trees stop pruning effectively. So production systems relax to ANN: return most of the true top-k, often targeting 95% recall or higher.
The baseline is Flat search: measure the distance to every stored vector. Its cost is O(Nd). With N equal to one million and d equal to 1024, one query streams 4 GB. ANN speeds this up in two ways: visit fewer vectors, or make each distance computation cheaper.
IVF reduces candidates by partitioning the space into centroid-owned cells. At build time, k-means creates nlist centroids, and each point goes to its nearest cell. At query time, IVF opens only the nprobe cells closest to the query and scans inside them. If the true neighbor sits in a cell you did not open, recall drops.
PQ compresses long vectors into short integer codes. For 1024 dimensions, split into 64 parts, then replace each part with the ID of its nearest entry in a 256-entry codebook. A 4096-byte float32 vector becomes a 64-byte code: 64ร compression. During search, precompute query-to-codebook distance tables, then score each vector by summing table entries.
HNSW is a layered graph where similar vectors are connected by neighbor links. The upper layers are sparse world maps; the bottom layer is the dense street map containing every point. Search greedily descends through upper layers, then expands an efSearch-wide beam on Layer 0. Increasing M, the maximum links per node, improves recall but also increases link memory.
HNSW assumes the whole index fits in RAM. One billion 1024-dimensional float32 vectors take 4 TB before indexing overhead. DiskANN keeps its Vamana graph and full vectors on SSD, with only PQ-compressed vectors in RAM. Because one hop is a 4 KB random read, long-range edges are used to reduce the number of hops.
Real systems also need filters: only published documents, or only one tenant. Post-filtering is simple, but when few items pass the filter, the final top-k can come back empty. Pre-filtering works for tiny subsets, but sometimes throws away the speed advantage of ANN. In the messy middle, where a moderate share passes, systems like Qdrant preserve connectivity with a filterable HNSW graph.
In production, the choice depends on N, memory budget, update rate, and recall target. For small N, use Flat; with enough RAM, HNSW; under memory pressure, IVFPQ; at billion scale, DiskANN. Operationally, watch Recall@k, QPS, p99 latency, build time, and index size. Hybrid pipelines often fuse BM25 and vector results with RRF, then re-rank them with a cross-encoder.
A vector DB is not magic; it is a set of indexing ideas extended into high-dimensional space. The article goes deeper, with interactive demos for PQ and HNSW and the implementation pitfalls to watch for. Read the full vector-database-internals article for the complete walkthrough.
1. Why vector search is hard
1.1 Embedding vectors
An embedding maps text, images, or audio to a fixed-dimensional real vector so that items with similar meaning end up close together. Typical dimensionalities:
| Model | Dimension | Preferred distance |
|---|---|---|
OpenAI text-embedding-3-small | 1536 | cosine |
OpenAI text-embedding-3-large | 3072 | cosine |
Cohere embed-v3 | 1024 | cosine / dot |
| BGE-M3 | 1024 | cosine |
| CLIP ViT-L/14 | 768 | cosine |
Dimension is often above 1000, and at that scale brute-force search over many vectors is no longer practical.
1.2 Distances and similarities
For vectors the three common metrics are:
Euclidean (L2): Inner product (IP; sum of element-wise products of the two vectors): Cosine:When every vector is normalized to unit length,
so L2, cosine, and inner product are related by monotonic transformations (transformations that preserve or exactly reverse order), and all three give the same ranking. Most ANN libraries therefore use L2 or IP internally and switch between the three depending on whether vectors are normalized first.
1.3 The curse of dimensionality
In high dimensions, intuition from low dimensions stops working:
- Distance concentration: the ratio of pairwise distances approaches 1, so the gap between the nearest and a far point becomes relatively small.
- Volume concentration: almost all of the volume of a unit ball lies near its surface.
- Space partitioning breaks down: axis-aligned structures like kd-trees (a classical index that recursively splits space in two along coordinate axes) work well in low dimensions, but once they visit about as many nodes as brute force.
So practical vector search gives up on exact answers and solves the problem approximately, aiming for 95% recall or better (the fraction of the true top-k that the search actually returns).
2. Baseline: brute-force (Flat)
If brute force is fast enough for your data, use it. In FAISS this is IndexFlatL2 / IndexFlatIP.
# Naive brute-force search (pseudocode)
def search(query: np.ndarray, xb: np.ndarray, k: int):
dists = np.linalg.norm(xb - query, axis=1) # O(Nd)
idx = np.argpartition(dists, k)[:k] # O(N)
return idx[np.argsort(dists[idx])]Complexity is . With , , and float32, every query scans 4 GB of memory. Even so, with SIMD (AVX-512 / NEON; CPU instructions that compute on many numbers at once) and BLAS-style GEMM (highly tuned matrix-multiply routines), brute force can be faster than HNSW up to about . FAISS's IndexFlat is therefore useful at that scale, and it is also the reference that produces exact results for checking ANN output.
ANN reduces that in two ways: (a) visiting fewer vectors or (b) making each distance computation cheaper. IVF does (a) and PQ does (b). HNSW achieves (a) by a different method from IVF, and DiskANN combines (a) and (b).
3. IVF: partitioning the space
Instead of checking everything, check only the nearby shelves. When building the index, the data is grouped into clusters of similar items; at query time only the few clusters closest to the query are opened and searched exhaustively. The weakness is that a true answer in a cluster you didn't open is missed.
3.1 Coarse quantization via k-means
The name IVF (inverted file index) comes from the inverted index used in text search.
- Train: run k-means on the dataset, producing centroids (representative vectors for the center of each cluster) .
- Build: assign every vector to its nearest centroid and store its ID in that centroid's posting list (like the list of page numbers under one entry in a book's index).
- Search: pick the top- centroids closest to the query; scan only the corresponding posting lists.
3.2 Parameter design
nlist: larger values mean shorter posting lists and faster search, but you also need a largernprobeto keep the same recall. For below 1 million, the FAISS guideline is (FAISS wiki).nprobe: the number of posting lists visited. Each posting list corresponds to the region assigned to one centroid, called a cell.nprobe=1is very fast but has low recall, because the true nearest neighbor might sit in a cell you didn't open. On SIFT1M (1 million 128-dim vectors) withnlist=16384, FAISS measured recall@1 of 0.83 atnprobe=16, 0.95 at 64, and 0.99 at 256 (FAISS wiki).- Training data: FAISS's clustering defaults are
min_points_per_centroid = 39andmax_points_per_centroid = 256. About 39-256 รnlistsamples is the target; below that FAISS warns, and above it FAISS subsamples automatically.
3.3 Limits of IVF alone
IVF still uses brute force inside each cell, so search stays slow when a cell holds many points. That is why IVF is usually combined with Product Quantization, covered next (FAISS's IndexIVFPQ).
4. Product Quantization (PQ)
Instead of comparing the query with full vectors, compare it with short summary codes. Each vector is cut into short chunks, and each chunk is labeled with the nearest entry from a 256-entry palette, compressing a vector to a few dozen bytes of integers. Memory use drops and distance computation becomes orders of magnitude faster, at the cost of some accuracy lost to the coarser summary.
4.1 Splitting a vector into subvector codes
PQ[1] (Jรฉgou et al., 2011) compresses high-dimensional vectors into short integer codes to make distance computation faster.
- Split into subvectors: , each .
- In each subspace, run k-means independently to get centroids . These form the codebook (a 256-color palette per subspace).
- Replace each with the 8-bit ID of its nearest centroid.
A -dim float32 vector ( bytes) shrinks to bytes. For and , that is 4096 bytes down to 64 bytes, 64ร compression.
4.2 Asymmetric Distance Computation (ADC)
PQ is fast because of Asymmetric Distance Computation: the query stays uncompressed, and a lookup table (LUT) is computed per subspace at query time.
- Split the query: .
- For each subspace , precompute distances to all 256 centroids:
- For a stored vector with code , the approximate distance is just table lookups summed:
Once the lookup tables are built, a distance needs zero float multiplications. Summing table values replaces the -dimensional subtract-and-square, which is why PQ is orders of magnitude faster. With , each stored code needs 64 byte-indexed table lookups and additions of the table values.
Product Quantization: approximating distance from subspace codes
PQ stores only the ID (code) of the nearest centroid in each subspace. Each subspace panel shows the centroids (gray dots), the DB vector's subvector (blue dot), and the centroid chosen as the code (large dot). This scaled-down demo splits 8 dimensions into 4 subspaces with 4 centroids each; the article's example (d=1024) uses m=64, k*=256.
Move the sliders to change the query and see how closely the PQ approximation tracks the true value. The DB vector's codes stay fixed, and the query is never compressed (ADC).
A tiny worked example. Take , (subvector dimension 2), (2-bit IDs). Query splits into and . If subspace 1's codebook is (IDs 0-3), then
(each entry is ). If subspace 2 uses the same codebook, . A stored vector with PQ code (so ) has approximate squared distance : two table lookups and one add, with no multiplication. A real index has 64 tables of 256 entries each. FAISS's fast-scan implementation (pq4_fast_scan) goes further: it shrinks the IDs to 4 bits so each table has only 16 entries, keeps the tables in SIMD registers, and runs the lookups and additions for 32 codes at once (FAISS wiki).
4.3 Error and tuning
PQ is lossy, so it introduces error. Rules of thumb for the parameters:
m: the number of sub-quantizers. It must divide ; between 4 and 16 balances accuracy and speed. The FAISS guidelines saymis typically 64 or less and, when OPQ is used, recommend 4 dimensions per sub-vector (FAISS wiki).nbits: usually 8 (256 centroids). 16 bits (65,536 centroids) is possible, but codebook training becomes expensive.- Optimized Product Quantization (OPQ)[2]: rotate vectors with an orthogonal matrix before splitting, to even out the variance across subspaces. The gain depends on the data and setup; in the paper's SIFT1B (one billion vectors) comparison, replacing PQ with OPQ raised recall by 1 to 5 percentage points.
4.4 IVFPQ: two stages
One of the most widely used configurations in practical vector databases is IVF + PQ, available in FAISS as IndexIVFPQ:
- IVF narrows the search to a few cells.
- Vectors inside each cell are stored as PQ codes.
- PQ encodes not the vector itself but the residual from the cell centroid (residual PQ).
The reason for encoding the residual is geometric. A raw vector can sit anywhere in the full space, but after subtracting its cell centroid , the residual only describes the offset inside that cell, which is a small vector. Because the distribution is tighter, the same 256 centroids can capture finer differences and quantization error goes down. The more cells (), the stronger this effect.
IndexIVFPQ scales to (a billion vectors) on a single machine (the FAISS wiki page Indexing 1G vectors has speed and recall measurements on billion-vector datasets). Latency depends heavily on hardware, nprobe, and the recall target. Recall tends to be lower than HNSW, but because it uses little memory and runs on GPUs, it is the first choice for GPU search (FAISS GPU), for environments without much memory, and for setups that also use disk.
Production pitfalls. If the training data doesn't match the serving distribution, you can get codebook collapse: almost all vectors are assigned to a handful of centroids and the rest stay empty, and recall drops sharply. To prevent it, use at least as many training samples as FAISS's min_points_per_centroid warning requires, and train on samples drawn from the same distribution you serve (for example, with multilingual BGE, match the language mix in production). IVF has a related problem: if the data is skewed toward a few topics, cell sizes become uneven, and queries that open a long posting list push up p99 latency. Recording the maximum and minimum cell sizes as a metric helps you catch this early.
5. HNSW: hierarchical navigable small world
Build a graph in advance in which similar vectors are linked as "friends." At query time, start from one point and keep moving to a friend closer to the query, descending from a world map to a country map to a street map. It is the standard choice today for in-memory vector search, but it assumes all data fits in RAM.
5.1 Inspiration: skip lists and small-world graphs
HNSW[3] (Malkov & Yashunin, 2016) is the most widely used graph-based ANN method today. It combines two ideas:
- Navigable Small World (NSW): in a graph with long-range edges as well as edges to nearby points, greedy routing (at every step, move to whichever neighbor of the current node is closest to the query, a purely local rule) can reach any node in a small number of hops (Kleinberg's 2000 small-world model). Kleinberg's key result is that in a grid model, if and only if long-range edges are drawn with probability in the node distance , a greedy algorithm using only local information reaches its target in hops. HNSW builds on this idea with probabilistic layers and diversity-based neighbor selection; it does not explicitly sample long-range edges from Kleinberg's distribution.
- Skip lists: building layers at random lets the search start on a coarse layer and move down to finer ones.
5.2 Structure
HNSW is a layered graph:
- Every point is in Layer 0.
- The highest layer each point belongs to is drawn at random from an exponential distribution, (default ).
- Each node has at most edges per layer ( at Layer 0).
5.3 Search algorithm
HNSW: greedy descent through 3 layers
Red q is the query, the large dot is the current best node, and yellow dots are nodes evaluated on that layer. Upper layers have fewer nodes, and each layer's best node becomes the entry point one layer down.
In pseudocode:
search(q, k):
ep = entry_point
for level L in [top_level ... 1]:
ep = greedy_search_layer(q, ep, ef=1, level=L)
W = greedy_search_layer(q, ep, ef=efSearch, level=0)
return top-k of W
greedy_search_layer(q, ep, ef, level):
visited = {ep}
candidates = min-heap([(dist(q, ep), ep)]) # priority: closest first
W = max-heap([(dist(q, ep), ep)]) # keep top ef
while candidates not empty:
c = candidates.pop_min()
f = W.peek_max()
if dist(q, c) > dist(q, f): break # pruning
for each neighbor n of c at `level`:
if n in visited: continue
visited.add(n)
if dist(q, n) < dist(q, f) or |W| < ef:
candidates.push((dist(q, n), n))
W.push((dist(q, n), n))
if |W| > ef: W.pop_max()
return WThe search has two stages: in the upper layers it heads roughly toward the query, and in the bottom layer it follows several candidates in parallel.
efSearch: the size of the candidate set. Larger values give higher recall but slower search. The default is 16 in FAISS and 40 in pgvector. On SIFT1M withM=32, FAISS measured recall@1 of 0.87 atefSearch=16, 0.98 at 64, and 0.99 at 256 (FAISS wiki).- Upper layers use
ef=1: a plain greedy move toward whatever is closest to the query. - Layer 0 uses
ef=efSearch: beam search (keeping several candidates at once and expanding them in parallel) widens the candidates.
5.4 Insertion and neighbor selection
insert(x):
โ = floor(-ln(rand()) * m_L)
ep = entry_point
for L in [top_level ... โ+1]:
ep = greedy_search_layer(x, ep, ef=1, L)
for L in [min(top_level, โ) ... 0]:
W = search_layer(x, ep, ef=efConstruction, L)
neighbors = select_neighbors_heuristic(x, W, M)
add bidirectional edges between x and neighbors
for each n in neighbors:
if degree(n) > M_max:
W' = neighbors(n)
neighbors(n) = select_neighbors_heuristic(n, W', M_max)
ep = W
if โ > top_level: top_level = โ; entry_point = xHNSW's quality depends on how neighbors are selected. Instead of simply taking the nearest, HNSW uses a heuristic that keeps neighbors diverse (select_neighbors_heuristic):
select_neighbors_heuristic(x, W, M):
R = []
sort W by dist(x, ยท) ascending
for e in W:
if |R| == M: break
if โ r โ R: dist(x, e) < dist(r, e):
R.append(e)
return RThis rule keeps a candidate only if is closer to than any already-selected neighbor is. Candidates lying in the same direction as an existing neighbor are dropped, so the neighbors spread out in different directions. As a result, the graph does not stay confined to local clusters, and search is less likely to get stuck in a local optimum. In FAISS, shrink_neighbor_list in faiss/impl/HNSW.cpp implements this.
The paper's full Algorithm 4 has two more options. extendCandidates grows the candidate set with each candidate's neighbors before selecting; the paper sets it to false by default and describes it as useful only for extremely clustered data. keepPrunedConnections refills from the pruned candidates in distance order if at the end, so each node keeps a fixed number of connections. Many implementations, including FAISS, use neither by default.
5.5 Parameters and cost
M: the hnswlib documentation says 12-48 is fine for most use cases (ALGO_PARAMS.md). Larger values improve recall but use more memory ( bytes of links per point at Layer 0 alone; FAISS wiki).efConstruction: the size of the candidate list used only during the build. The default is 40 in FAISS, 64 in pgvector, and 100 in Qdrant. Larger values improve index quality but make the build slower: on a 200-million-vector subset of SIFT in the paper, raising it from 40 to 500 withM=16increased build time from 42 minutes to 5.6 hours[3].- Complexity: search is in expectation, build is .
- Memory: vectors plus roughly bytes of links (most nodes exist only in Layer 0, so upper-layer links are negligible). With (), , and , that is 4 GB of vectors and about 128 MB of links, around 3% of the vectors themselves.
HNSW's weaknesses are high memory use and difficult deletion. Actually removing a node from the graph and rewiring its edges can break connectivity, so most implementations mark deleted nodes with a tombstone (a "deleted" flag: the node stays in the graph but is excluded from results) and remove them during a periodic rebuild (compaction: physically removing all tombstoned nodes and rebuilding the index). As deletions accumulate, the graph becomes sparse and unreachable regions appear; in the worst case recall does not recover even with a larger efSearch. Some implementations therefore rebuild once the tombstone fraction passes a threshold. Qdrant, for example, splits its data into segments and, by default, makes a segment eligible for rebuilding once 20% or more of its vectors are marked deleted (Qdrant documentation).
6. DiskANN / Vamana: a graph index for SSDs
Like HNSW, DiskANN follows links between neighbors, but the vectors themselves stay on SSD and only a compressed copy is kept in RAM. It can handle billion-scale datasets that don't fit in RAM, but as concurrent queries increase, SSD IOPS tends to become the bottleneck.
6.1 Motivation
HNSW assumes every vector fits in RAM. A billion 1024-dim float32 vectors take 4 TB as raw data. An index held in memory needs that much RAM, which is expensive, and the RAM size caps how much data it can handle. DiskANN[4], published by Microsoft in 2019, is a single-layer graph index designed for SSD. The paper indexes and searches a billion points on one workstation with 64 GB of RAM and an inexpensive SSD.
6.2 The Vamana graph
Like HNSW, search is greedy, but there is no hierarchy: a single-layer graph mixes long and short edges. The search entry point is the medoid of the dataset, computed once at build time. A centroid is a computed point and is not necessarily a real data point. The medoid, by contrast, is the existing data point closest to the mean of the dataset. Because search starts from a real node near the center of the data, many queries can be expected to reach their target in fewer hops. Construction uses a rule called -pruning:
build_vamana(X, R, ฮฑ):
G = random graph (R edges per node)
for two passes (ฮฑ=1.0, then ฮฑ=1.2):
for p in random permutation of X:
V = greedy_search(G, p, ep, L) # candidate set
neighbors(p) = robust_prune(p, V, ฮฑ, R)
for n in neighbors(p):
add edge nโp
if degree(n) > R:
neighbors(n) = robust_prune(n, N(n)โช{p}, ฮฑ, R)robust_prune(p, V, ฮฑ, R):
V = V sorted by dist(p, ยท)
P = []
while V not empty and |P| < R:
p* = V.pop_closest()
P.append(p*)
V = {v โ V : ฮฑ ยท dist(p*, v) > dist(p, v)} # drop v dominated by p*, keep diverse (long-range) edges
return PThe paper runs the first pass with and the second with a user-chosen , and its experiments use . The default in the microsoft/DiskANN implementation is 1.2 (defaults.h). With , Vamana keeps more long-range edges than HNSW's diversity heuristic. A larger makes the removal condition stricter, so a point in the same direction as an already-selected neighbor survives if it is far enough away, and a single hop can travel further across the graph. On SSD, one hop is one random read, so fewer hops translate directly into lower latency.
6.3 SSD layout
- Each vector and its adjacency list are stored together in the same page on disk (Vamana page layout).
- If a node's vector and adjacency list fit in 4 KB, visiting a node on the search path costs one 4 KB random read that returns both (in the paper's SIFT1B setup, a 128-byte vector and a 512-byte adjacency list for degree 128 fit in one sector).
- A PQ-compressed copy stays in memory, and these approximate distances are used for distance computation and pruning during traversal. The full-precision vectors read from disk are used to re-rank candidates, which limits the accuracy lost to compression.
6.4 IOPS and latency
On SIFT1B (a billion points), the paper reports 95%+ recall@1 with mean latency under 3 ms and more than 5,000 queries per second, on a 16-core workstation with 64 GB of RAM and two SSDs in RAID-0. The paper sets the design goal of keeping random reads per query to a few dozen and round trips to the SSD under ten, preferably five. DiskANN handles scales (over a billion vectors) that HNSW cannot hold in RAM on the same budget. FreshDiskANN (2021) proposes doing inserts, deletes, and rebuilds in the background.
Production pitfalls. As concurrent queries increase, SSD IOPS (random I/O operations per second) saturates and tail latency worsens sharply. According to the paper, an inexpensive SSD takes a few hundred microseconds per random read and can serve about 300K random reads per second. As QPS (queries per second) ร (reads per query) approaches that ceiling, queues form and each read takes a millisecond or more. When that happens, p99 (the slowest 1% of queries) can jump sharply while the median barely moves. The paper finds that a small beam width (2-8) balances latency and throughput; in that setting the SSD load factor was 30-40%. Common mitigations are striping across multiple SSDs (RAID-0), tuning beam_width to visit fewer nodes per query, and giving the in-memory PQ codes more bytes (less compression) so their approximate distances are more accurate and more pruning happens in memory (DiskANN documentation).
7. ScaNN: Google's anisotropic quantization
ScaNN[5] refines ยง4's PQ for inner-product search. Google published it in 2020, proposing a quantization (compressing each vector into a short sequence of bits) designed for maximum inner product search.
Regular PQ minimizes reconstruction error . But what matters for search is the error in the inner product . The queries for which a data point should rank near the top are those with a large inner product, and such queries point in nearly the same direction as . So the error component parallel to has the largest effect on inner-product error. ScaNN's anisotropic quantization loss is:
Here is the component parallel to and the orthogonal component. With , the learned quantizer keeps error parallel to especially small. In the paper's experiments on Glove1.2M (1.2 million 100-dimensional word embeddings), this gave higher recall than quantization that minimizes reconstruction error at the same bit budget.
8. Filtered search (pre / post / in-filter)
Real services need metadata-filtered ANN, such as "only published documents" or "only data owned by user X."
- Post-filter: retrieve results () with ANN, then apply the filter. When selectivity (the fraction of rows that pass the filter) is low, most of the results are filtered out, fewer than remain, and you have to search again with a larger .
- Pre-filter: build the set of IDs matching the filter and brute-force within that set. Fast when the subset is small, but the ANN graph goes unused.
- In-filter (integrated): keep only filter-matching nodes as candidates during graph traversal. Each database has its own technique to keep the graph reachable. For example, Qdrant uses a filterable HNSW index that adds extra links to the HNSW graph based on payload categories so the graph stays connected after filtering (and switches from HNSW to scanning the payload index when cardinality is low).
Milvus uses standard filtering by default, which narrows the candidates with the scalar conditions before the ANN search (a pre-filter in that it filters first). For complex filter expressions it also offers iterative filtering, which pulls vector-search results from an iterator, filters each one with the scalar conditions, and continues until it has the top- results; you select it with a parameter in the search request (Milvus documentation).
Rough guide to which strategy suits which selectivity:
| Strategy | Works well when | Fails when | Typical home |
|---|---|---|---|
| Post-filter | selectivity โฅ 50% (most rows pass) | selectivity โค 1%; top- ends up empty | pgvector, naive Elasticsearch |
| Pre-filter | selectivity โค 1% (subset is tiny) | mid-range selectivity โ ANN is wasted and the linear scan dominates | Milvus standard filtering |
| In-filter | the middle range, selectivity 1โ50% | harder to implement; graph connectivity must be engineered in | Qdrant filterable HNSW, Filtered-DiskANN |
In recent research, Filtered-DiskANN (2023) takes filter labels into account when building the graph, while ACORN (2024) builds the graph without assuming any particular filter and, at query time, traverses only the nodes that satisfy the filter.
9. Implementations compared
The table below shows how popular OSS and SaaS products combine the algorithms from ยง2โยง8. FAISS is a library; Milvus, Qdrant, and Weaviate are database products that include distribution and persistence; pgvector is a PostgreSQL extension; and Pinecone is a fully managed SaaS.
| Implementation | Core algorithms | Notes |
|---|---|---|
| FAISS | Flat, IVF, IVFPQ, HNSW, OPQ | Meta. GPU + SIMD. The research reference. |
| Milvus | Wraps FAISS / HNSW / DiskANN | Distributed, persistent, filtering, multi-tenant. Kubernetes-first. |
| Qdrant | HNSW (custom Rust) | Payload filtering built into HNSW search. gRPC/REST. |
| Weaviate | HNSW | Module-based vectorizers, GraphQL API. |
| pgvector | IVFFlat, HNSW | PostgreSQL extension. Joins with existing SQL. |
| Pinecone | Proprietary hybrid | Fully managed SaaS. |
| LanceDB | IVFPQ, HNSW over Arrow/Parquet | Columnar persistence โ think "DuckDB for embeddings." |
| Elasticsearch / OpenSearch | Lucene HNSW | BM25 + vector hybrid in one engine. |
9.1 A selection flowchart
10. Hybrid search: BM25 + vector
In practice, combining lexical matching (BM25; a classical relevance score from the TF-IDF family that weights how many of the query's words appear in each document) and semantic similarity (vector) gives better quality than ANN alone. Two common ways to combine them:
10.1 Reciprocal Rank Fusion (RRF)
is the set of retrievers (BM25, vector, and so on), and is the usual empirical value. Because it uses only ranks, scores don't need to be on the same scale, and Elasticsearch and Weaviate both offer RRF as a fusion method for hybrid search.
10.2 Cross-encoder re-rank
Retrieve 100 results with ANN, then re-score them with a BERT-style cross-encoder. This is a common setup in production RAG.
Bi-encoder vs cross-encoder. Vector search uses a bi-encoder: the query and document are embedded separately and compared by cosine similarity (fast, because document embeddings can be computed offline). A cross-encoder instead concatenates the query and document, passes them through one Transformer, and lets attention relate the two directly to produce a relevance score. It is more accurate, but slow, because it must run again for every document on every query. So a two-stage setup, using a cheap bi-encoder to narrow down to 100 candidates and an expensive cross-encoder to re-score only those 100, improves accuracy while keeping cost down.
11. Production metrics
| Metric | Meaning |
|---|---|
| Recall@k | overlap between returned top-k and true top-k |
| QPS | queries per second |
| p50 / p95 / p99 latency | median / top-5% / top-1% latency; the tail (especially p99) is where GC pauses and disk I/O stalls show up |
| Build time | batch reindex time |
| Index size | shows compression efficiency |
| Insert/delete throughput | whether streaming updates are viable |
ann-benchmarks is the standard benchmark for comparing HNSW, IVFPQ, ScaNN, and others under the same conditions. Comparisons usually use a Pareto frontier plot with recall on the x-axis and QPS on the y-axis. The Pareto frontier is the curve through the points where you can no longer raise QPS without lowering recall, or the reverse. If one algorithm's whole curve lies outside another's, it is better at every operating point. When curves cross, the comparison becomes a trade-off such as A being faster at 95% recall while B is faster at 99%, which a single number like "A is faster than B" cannot capture.
12. State of the art (as of 2026)
Research and implementations drawing attention as of 2026 address themes such as larger scale, tighter compression, and easier updates.
- SPANN / SPFresh (Microsoft, 2021-2023): an IVF-style approach that keeps centroids in memory and posting lists on SSD. SPFresh adds in-place updates to the posting lists. Like DiskANN, it targets billion-scale search on SSD.
- Learned indexes: a research area that uses neural networks to learn centroid selection or hierarchical structure and adapt it to the data distribution (not yet in FAISS itself).
- RaBitQ: a randomized binary quantization that guarantees a theoretical bound on the error of its distance estimates (Gao & Long, SIGMOD 2024[6]); PQ and its variants have no such guarantee. FAISS has implemented it as
faiss::IndexRaBitQ/IndexIVFRaBitQsince v1.11.0. As of November 2026, pgvector does not include it; adding it to the existing IVFFlat and HNSW indexes has been proposed in issue #1003. - GPU graph index (CAGRA, 2023): part of NVIDIA RAPIDS (cuVS). Not a port of HNSW, but a CUDA implementation of a non-hierarchical graph index designed for parallel processing on GPUs.
- Hybrid DRAM / CXL layouts: since Intel Optane was discontinued, NVMe plus large RAM has been the norm, and new configurations using CXL memory pools are being researched.
13. Putting it all together
The design choices in vector search can be organized along three axes:
| Axis | Technique |
|---|---|
| Reduce candidate set | IVF / Graph (HNSW, Vamana) |
| Make distance computation cheaper | PQ / OPQ / ScaNN / RaBitQ |
| Exploit the memory hierarchy | DiskANN / SPANN (SSD), CAGRA (GPU) |
In practice, data size, memory budget, update frequency, and recall target narrow the choice. By data size, memory budget, existing systems, and the need for distribution, a rough guide is:
- โ Flat
- with plenty of RAM โ HNSW
- with limited RAM โ IVFPQ
- โ DiskANN / SPANN
- Already on PostgreSQL โ pgvector (HNSW)
- Need distribution โ Milvus / Qdrant / Weaviate
A vector database can be seen as the classical inverted-index idea extended to high-dimensional space. IVF uses per-cluster posting lists, PQ makes distance computation cheaper by compressing vectors, and HNSW and DiskANN are graphs for walking through candidates. Knowing these mechanisms tells you which parameter (efSearch, nprobe, and so on) to suspect when recall drops, whether to spend on memory or SSD, and what to look at when p99 gets worse.
References
- H. Jรฉgou, M. Douze, C. Schmid, "Product quantization for nearest neighbor search", IEEE TPAMI, 2011.
- T. Ge et al., "Optimized Product Quantization", IEEE TPAMI, 2014.
- Y. Malkov, D. Yashunin, "Efficient and robust approximate nearest neighbor search using Hierarchical Navigable Small World graphs", 2016. arXiv:1603.09320
- S. Jayaram Subramanya et al., "DiskANN: Fast Accurate Billion-point Nearest Neighbor Search on a Single Node", NeurIPS, 2019.
- R. Guo et al., "Accelerating Large-Scale Inference with Anisotropic Vector Quantization", ICML, 2020.
- J. Gao, C. Long, "RaBitQ: Quantizing High-Dimensional Vectors with a Theoretical Error Bound for Approximate Nearest Neighbor Search", SIGMOD, 2024. arXiv:2405.12497
- FAISS wiki โ documentation for FAISS, the de facto reference implementation of ANN algorithms.
- ann-benchmarks โ recall vs throughput comparisons.