Status: Accepted Date: 2026-06-18 Deciders: Achref Soua
Quiver's headline claim is memory-frugal, security-first ANN. That claim is meaningless without a credible, reproducible, head-to-head comparison against the open-source field. The existing bench/ harness measures Quiver alone (recall@10, QPS, latency); it has no competitor support and no automated report.
Phase B of v0.17.0 delivers a paper-grade, multi-DB benchmark suite whose output is a single committed Markdown report. The guiding rule inherited from the rest of the project: every number is real, measured, and labelled with the hardware it ran on; losses are named; reference-hardware-pending markers are used when the dev box is not the right place to produce a headline figure.
The dev box (WSL2 on shared hardware, co-hosting pipetwin) is not a source of:
- official QPS / latency headline numbers (virtualized I/O, shared CPU),
- memory-at-scale numbers at 10M+ vectors (OOM risk).
It can produce, honestly:
- smoke-run validation at SIFTSMALL (10k vectors) to verify every competitor adapter end-to-end,
- recall correctness at any scale (recall is a property of the index, not the host),
- relative RSS at SIFT1M scale when all competitors run sequentially on idle-ish hardware (clearly labelled as "dev-box, resource-shared").
Everything at 10M+ or needing production-grade QPS numbers carries a [reference-hardware-pending] marker. The reference-hardware runbook (docs/benchmarks/reference-hardware-runbook.md) is updated with the multi-DB procedure so the owner can produce the headline numbers on dedicated hardware.
The existing Python project in bench/ is already isolated from the Cargo workspace and the Rust deny/audit gate. We add Python competitor adapters as modules inside bench/quiver_bench/competitors/. Competitor Python clients (qdrant-client, lancedb, chromadb, pymilvus, etc.) live in a new competitors optional dependency group in bench/pyproject.toml. They never touch Cargo.toml, deny.toml, or cargo audit.
| Competitor | Mode | Reason for inclusion |
|---|---|---|
| Qdrant v1.13.4 | Docker | The most direct competitor; HNSW + HNSW-on-disk |
| LanceDB 0.33.0 | Python library (no Docker) | Fast Lance disk-resident IVF_PQ; strong on memory |
| Chroma 1.5.9 | Python embedded | Fast chromadb embedded mode; popular for RAG use-cases |
| Milvus Lite 3.0.0 | Python library (no Docker) | Milvus engine without the cluster machinery |
| pgvector 0.7 on PG16 | Docker | SQL-native ANN; baseline for the SQL world |
| FAISS 1.14.3 | Python library (no Docker) | The canonical library baseline; flat/IVF/HNSW implementations |
| Weaviate 1.27.0 | Docker | Alternative HNSW store with rich schema |
Competitors run on datasets where they support the required metric (L2 or cosine). Competitors that do not support a dataset's metric are skipped and noted in the report.
| Dataset | dim | N | Metric | Where used |
|---|---|---|---|---|
| siftsmall | 128 | 10k | L2 | Smoke-run adapter validation (dev box) |
| SIFT1M | 128 | 1M | L2 | Full in-memory comparison (dev box, labelled) |
| GloVe-100 | 100 | ~1.2M | cosine | Cosine path [reference-hardware-pending] |
| Deep10M | 96 | 10M | L2 | Disk-path memory headline [reference-hardware-pending] |
SIFTSMALL is already in bench/datasets/ (generated by the prior harness). SIFT1M is the existing bench/datasets/sift directory. GloVe and Deep10M require download; bench/quiver_bench/datasets.py gains download helpers with SHA-256 verification.
For every (competitor, dataset, param-sweep-point):
- Recall@10 against the dataset's exact ground truth.
- QPS (single-thread) — sequential queries, no concurrency.
- Latency p50 / p95 / p99 (ms).
- RSS at steady state (MB) — sampled after index load + warmup, before the query run; see §5.
- Build time (s) — time to build / load the index from base vectors.
- On-disk index size (MB) — total data written by the competitor (not the base vectors).
The headline comparison is recall@10 vs RSS at a common operating point (recall@10 ≥ 0.95). QPS and latency are secondary figures.
RSS is sampled identically for all competitors at the same lifecycle point (after index load, after 10-query warmup, before the timed query run):
- Native process / Python library (LanceDB, Chroma, FAISS, Milvus Lite):
/proc/<pid>/status→VmRSSon Linux;psutil.Process().memory_info().rssas a cross-platform fallback. - Docker container (Qdrant, pgvector, Weaviate):
docker stats --no-stream --format json <container>→MemUsagefield.
Both paths are collected by bench/quiver_bench/rss.py.
- Fixed RNG seed (42) for any random data; datasets use their own fixed ground truth.
- Warmup: 10 queries discarded before the timed run.
- Repetitions: ≥ 3 complete query-set runs; report mean ± stdev for recall, QPS, latency.
- Sequential competitor runs: only one competitor runs at a time (no concurrent containers).
- Drop OS page cache between competitors (Linux:
echo 3 > /proc/sys/vm/drop_cachesif privileged; note if skipped). - Each competitor gets the same upserted vector set, with IDs coerced to the competitor's required format.
- Competitor configs follow each system's own recommended defaults for the dataset scale; the exact config is embedded in the result JSON.
docs/benchmarks/results/
comparison-v0.17.0/
manifest.json # hardware + OS + versions + methodology hash
siftsmall-smoke.json # per-competitor raw results (smoke validation)
sift1m/
quiver.csv
qdrant.csv
lancedb.csv
chroma.csv
milvus_lite.csv
pgvector.csv
faiss.csv
weaviate.csv
comparison-v0.17.0.md # the auto-generated report (committed)
Two new recipes:
bench-compare *ARGS:
uv run --project bench python -m quiver_bench.comparison {{ ARGS }}
bench-report:
uv run --project bench python -m quiver_bench.report \
docs/benchmarks/results/comparison-v0.17.0comparison accepts --dataset siftsmall|sift1m|glove|deep10m, --competitors all|qdrant,lancedb,..., --smoke (alias for siftsmall), and --out DIR.
report reads the result directory and generates comparison-v0.17.0.md.
The auto-generated comparison-v0.17.0.md contains:
- Hardware manifest block (CPU, RAM, OS, kernel, versions).
- One sentence on the methodology link.
- For each dataset run:
- A table at the recall@10 ≥ 0.95 operating point: DB | recall@10 | QPS(1T) | RSS(MB) | Build(s) | Index(MB) | notes.
- A wins / ties / losses matrix (Quiver vs each competitor, per metric).
- Honest conclusions: wins highlighted, losses or ties stated plainly, every number tagged with the hardware it ran on.
[reference-hardware-pending]for anything not produced at full scale on documented reference hardware.
Static plots are omitted from the committed report (they require matplotlib / graphing deps) — the raw CSVs are the artifact; a --plot flag can generate them locally.
Good:
- A committed, reproducible comparison report answers the credibility question before v1.0.0.
- Extending
bench/(not forking) keeps the harness DRY and the commit surface small. - Docker isolation means competitor versions are pinned and reproducible.
- Honest reporting — including losses — is more credible than cherry-picked wins.
Bad / accepted costs:
- Docker startup adds latency to the comparison run (Qdrant, pgvector, Weaviate).
- On WSL2,
drop_cachesrequires root and may not be available — noted in the report. - Some competitors (Milvus full cluster, Weaviate at scale) are heavier than they appear at 10k vectors; the harness caps dataset size per competitor to avoid OOM on the dev box.
- The report at SIFT1M is labelled "dev box, resource-shared" and not cited as an official headline until reproduced on documented reference hardware.
Fork a new Python project — rejected; would duplicate dataset loading, metrics, and the quiver-client dependency with no benefit.
Use ann-benchmarks Docker directly — too inflexible for our Quiver REST-over-HTTP adapter and our security (key-per-collection) surface. The ann-benchmarks harness also bundles a fixed version of every competitor; we prefer to control the versions.
Measure all competitors in one long CI run — rejected; this box is resource-shared and CI is manual-only (ADR-0015). The harness is invoked locally or on reference hardware via just bench-compare.
Include Redis/Vespa — deferred; both require significant infrastructure setup that competes with the OOM budget on this box. They are noted as stretch goals in the runbook.
Shipped on develop via squash-merge of feat/benchmark-comparison-suite.
Files added:
| File | Purpose |
|---|---|
bench/quiver_bench/competitors/base.py |
CompetitorAdapter ABC + BenchResult dataclass; query_sweep() with 3-rep mean±stdev |
bench/quiver_bench/competitors/{faiss,lancedb,chroma,milvus_lite,qdrant,pgvector,weaviate,quiver}_adapter.py |
8 concrete adapters |
bench/quiver_bench/comparison.py |
CLI runner: sequential multi-competitor, emits CSV per competitor |
bench/quiver_bench/report.py |
Report generator: reads CSVs → comparison-v0.17.0.md |
bench/quiver_bench/rss.py |
RSS via /proc/VmRSS (native) / docker stats (containers) |
bench/quiver_bench/manifest.py |
Hardware provenance capture |
bench/tests/test_comparison.py |
18 unit tests |
Deviations from the design:
- The output layout uses per-competitor
*.csvfiles undersmoke/andsift1m/subdirectories rather than asiftsmall-smoke.json. CSV is friendlier fordiffandgrep. - Quiver SIFT1M was not included in the comparison CSVs: REST API upload takes ~12 minutes on the dev box, making a fair sequential comparison with in-process competitors impossible. Real Quiver SIFT1M numbers (single-DB harness, same methodology) are in
docs/benchmarks/results/sift1m.mdand referenced from the report. - LanceDB SIFT1M recall (0.52–0.56) is the honest measurement under default 256-partition IVF_PQ; the report notes this is configurable but doesn't tune it — that would be cherry-picking.
- Qdrant, pgvector, and Weaviate SIFT1M runs deferred to reference hardware (Docker API overhead at 1M-vector scale on WSL2 is not representative).
| Check | Result |
|---|---|
bench/tests/test_comparison.py (18 tests) |
✅ all pass |
| Smoke run — all 8 competitors on siftsmall (10k, 128-d, L2) | ✅ complete; CSVs in smoke/ |
| SIFT1M — FAISS (HNSW M=16, efConstruction=200) | ✅ ef_search 16→256; recall 0.81→1.00; peak 7685 QPS [dev-box] |
| SIFT1M — LanceDB (IVF_PQ, 256 partitions) | ✅ nprobes 4→64; recall 0.52–0.56 (config-limited, noted) [dev-box] |
comparison-v0.17.0.md generated |
✅ at docs/benchmarks/results/comparison-v0.17.0/ |
| No fabricated numbers | ✅ confirmed; every figure is a real measurement |
[reference-hardware-pending] on all non-smoke results |
✅ |
just verify (Rust gate) |
✅ green (no Cargo changes in this PR) |