~/satyajit

zvec: an in-process vector database, and the ANN search inside it

mdjsonmcp

2026-07-08 · 8 min · explainer · vector-search · systems · quantization · information-retrieval

zvec is Alibaba's open-source (Apache 2.0, Tongyi Lab) in-process vector database — it links into your app as a library instead of running as a server. The pitch is "SQLite for vectors": no cluster, no network hop, one embedded engine that does approximate nearest-neighbour (ANN) search over embeddings. The core is C++ (built on Proxima, Alibaba's older vector-search engine), with SDKs for Python, Node.js, Go, Rust, and Dart/Flutter and builds for Linux (x86_64/ARM64), macOS (ARM64), Windows, and Android.

The headline number is a throughput claim: on VectorDBBench's Cohere 10M set (10M vectors, 768-d) a 16-vCPU / 64-GiB instance serves 8,475 QPS, roughly 2× the next-fastest entry on that board. That is the top bar in the cover figure. Two mechanisms do the work, and neither is unique to zvec — they are the two levers every fast ANN index pulls. This post explains both from first principles, then puts the self-reported numbers up in full.

Why nearest-neighbour search is hard

The naive query is brute force: score the query vector against all NN stored vectors, keep the top-kk. That is O(N)O(N) distance computations per query. At N=107N = 10^7 and 768 dimensions, each query touches ten million dot products — correct, and far too slow to serve at thousands of QPS.

The standard fix is a graph index. zvec's default is HNSW (Hierarchical Navigable Small World): wire every vector to its nearest neighbours, then answer a query by walking the graph — start somewhere, repeatedly hop to whichever neighbour is closer to the query, stop at a local minimum. You compute distances only to the nodes you actually visit, and that count grows roughly logarithmically with NN, not linearly. Step through one descent:

greedy descent · N = 80 vectorsillustrative
queryentry
hops
0 / 8
distances computed
1 / 80
brute force
80 / 80
hop toward the query

Brute force scores the query against all 80 vectors. The graph search starts at a far entry node and keeps hopping to the neighbour closest to the query, stopping at the nearest point. It reaches the answer after 8 hops, computing distances to 29 of 80 vectors — the rest are never touched. The hop count grows roughly logarithmically, so at 10M vectors the same descent scores a few thousand, not ten million.

That is the first lever: visit fewer vectors. The graph decides which candidates to score. It trades a small, bounded loss in recall (you can land on an approximate neighbour, not the exact one) for skipping the overwhelming majority of the dataset. ef-search is the knob — a larger search frontier means more nodes visited, higher recall, lower QPS. The benchmark run uses --ef-search 118 with --m 50 (m = neighbours per node in the graph).

Quantize to make each score cheap

The graph decides how many distances you compute. Quantization decides how expensive each one is. A 768-d fp32 vector is 3072 bytes; scoring it is a 768-wide float dot product. Compress it and both the memory footprint and the per-distance cost drop:

The catch: aggressive codes distort distances, so the ranking off the compressed vectors is wrong. zvec's answer is the refiner (the --is-using-refiner flag). Retrieve a broad shortlist using the cheap quantized distances, then re-score just that shortlist with the original full-precision vectors and return the exact-scored top-kk. Coarse pass to go fast, fine pass to stay accurate. Toggle refinement off and watch a true neighbour fall out of the result:

quantize → shortlist → refine · top-3illustrative
query
96 B/vec (768-d)distance: popcount32× smaller, coarse
coarse pass · top-5 by quantized distance
1. #92. #13. #34. #45. #7
result · top-3 after refine
#3 #7 #9
recall@3 vs exact: 3 / 3(amber rings = true neighbours; blue fill = returned)
1-bit codes shrink each vector 32× and turn a distance into a popcount, but they collapse points to quadrant centroids, so the coarse ranking is wrong: with refinement off a true neighbour gets pushed out. Turn refinement on and the shortlist is re-scored with exact vectors — as long as the true neighbour survived into the top-5, it comes back. That is the whole trick: quantize to go fast, refine to stay accurate.

The refiner is why the benchmark can run int8 codes and still report high recall: the int8 pass is only a filter, and the returned order is decided by full-precision math on a handful of survivors. RaBitQ is the more aggressive bet — it aims to skip that refine step entirely, which is a stronger claim and the one I'd want independent numbers on before trusting.

What zvec actually ships

The two levers above sit inside a fuller engine. The index and quantization menu:

LayerOptions
IndexHNSW (dense + sparse), IVF, Flat (brute-force), HNSW-RaBitQ, Vamana / DiskANN (on-disk)
Quantizationfp16, int8, int4, RaBitQ (1-bit)
Distancefull-precision refiner pass over any quantized index
Retrievaldense + sparse vectors, multi-vector queries, full-text search with hybrid fusion

Systems details that matter for the throughput story:

The exact config behind the headline run, as published:

# VectorDBBench · Cohere 10M (10M × 768-d) · Alibaba Cloud g9i.4xlarge (16 vCPU / 64 GiB)
zvec-bench \
  --index hnsw \
  --quantize-type int8 \
  --m 50 \
  --ef-search 118 \
  --is-using-refiner \
  --threads 12-20
# → 8,475 QPS, index build ≈ 1 hour

The Python surface is the usual embedded-DB shape — a CollectionSchema to declare fields and the vector index, Doc objects to insert, and a VectorQuery to search — so wiring it into a RAG loop is a library import, not a service to stand up.

The numbers, in full

The cover chart is the VectorDBBench QPS ranking, with zvec highlighted at the top:

A horizontal bar chart titled 'Qps (more is better)'. The top bar, Zvec-16c64g-v0.1, reaches 8,475 and is boxed in red. Below it: ZillizCloud-8cu-perf 3,957; OpenSearch-16c128g-force_merge 1,611; ElasticCloud-8c60g-force_merge 1,520; Pinecone-p2.x8-1node 1,131; then a long tail of managed services from ~505 down to 8.7.
VectorDBBench QPS on Cohere 10M; zvec is the boxed top bar at 8,475. Bars mix embedded and managed-cloud entries on differing hardware — the labels encode each configuration (zvec benchmarks, VectorDBBench).

Only the top of the field, redrawn so the gap is legible. The label suffixes (16c64g, 8cu-perf, p2.x8-1node) are each entry's own hardware and version — this is a leaderboard of different setups, not one controlled sweep:

VectorDBBench · Cohere 10M · QPS (self-reported; hardware differs per row)
zvec 16c64g
8475
ZillizCloud 8cu
3957
OpenSearch 16c128g*
1611
ElasticCloud 8c60g*
1520
Pinecone p2.x8
1131
Qdrant 16c64g
446.9
Milvus 16c64g sq8
437.2
0500010000

QPS is the throughput axis; VectorDBBench measures it at a matched recall level per entry, and that recall panel isn't shown on this chart, so treat the ranking as "throughput at comparable accuracy" rather than raw speed at any accuracy. The * rows (OpenSearch, ElasticCloud) use force_merge, a build-time optimisation that trades index time for query speed. zvec's own build is the ~1-hour figure above.

The take

The genuinely interesting thing about zvec is not the top bar — it's the form factor. An embedded, Apache-2.0, single-file-ish vector engine with real SDKs across five languages, that runs on-device down to Android, is a useful thing to have for local RAG where standing up Milvus or a managed cloud is overkill. "SQLite for vectors" is the right mental model, and the single-writer/many-reader concurrency model matches it exactly.

The 8,475 QPS is real but oversold by the framing. It is an in-process number — no network — sitting on a chart next to managed services that pay round-trip latency, on hardware that varies row to row. The mechanisms getting it there are the standard two: a graph index that visits a logarithmic slice of the data, and int8 quantization with a full-precision refiner so the cheap codes only filter while exact math decides the final order. Both are well-understood; zvec's contribution is a clean, SIMD-dispatched, embeddable implementation of them, plus a newer RaBitQ 1-bit path whose "high recall without re-ranking" claim is the one I'd hold out for independent verification on. For a team that wants vector search inside the application binary, it's worth a real evaluation — just run VectorDBBench yourself, on your hardware, against your recall target, before believing any single bar.


Built on the zvec release (Alibaba Tongyi Lab, Apache 2.0) — GitHub README, zvec.org docs, and the v0.3.0 notes. All QPS numbers are self-reported via VectorDBBench on Cohere 10M; the two interactive diagrams are illustrations of greedy graph descent and quantize-then-refine, not measured traces. The benchmark figure is reproduced from the project's published chart for commentary.

Cite this article

For attribution, please use the following reference or BibTeX:

Satyajit Ghana, "zvec: an in-process vector database, and the ANN search inside it", ai.thesatyajit.com, July 2026.

bibtex
@misc{ghana2026zvec,
  author = {Satyajit Ghana},
  title  = {zvec: an in-process vector database, and the ANN search inside it},
  url    = {https://ai.thesatyajit.com/articles/zvec},
  year   = {2026}
}
share