Benchmarks¶
raggio vs Weaviate on 552,515 real email embeddings, measured with the same harness, corpus, queries, and host — an NVIDIA DGX Spark. The point of the comparison is what a single 1 GiB Python container gives up (and doesn't) against a dedicated vector database running with 8x the memory.
Setup¶
| raggio | Weaviate | |
|---|---|---|
| Container memory cap | 1 GiB | 8 GiB |
| Vector index | 4-bit quantized flat (brute-force scan), TQ+ calibrated | HNSW, uncompressed float32, defaults |
| Text index | SQLite FTS5, BM25 | built-in BM25 over body |
| Hybrid fusion | unweighted RRF (k=60) | rankedFusion, alpha 0.5 |
- Corpus: 552,515 email embeddings, 1536 dims (
text-embedding-3-small), each chunk carrying a distinct body (shared boilerplate + the email's path tokens) so BM25 has something real to rank. - Queries: 500 held-out corpus vectors, k=10, seed 42. Hybrid queries pair each held-out vector with the path tokens of a sampled ingested document.
- Ground truth: exact float32 cosine top-10 over the full corpus, computed offline once and cached.
- Host: NVIDIA DGX Spark — GB10 Grace, 20 aarch64 cores (10x Cortex-X925 + 10x Cortex-A725), 122 GB unified LPDDR5x, native Linux, rootless podman. Both engines queried over REST with client-supplied vectors (no embedding calls in the measured path), identical stored payloads.
Results¶
| Metric | raggio (1 GiB) | Weaviate (8 GiB) |
|---|---|---|
| Ingest wall time (s) | 500 | 204 |
| Ingest throughput (vec/s) | 1105 | 2713 |
| Memory after ingest (MB) | 490 | 7851 |
| Memory under query load (MB) | 588 | 7738 |
| Disk footprint (MB) | 980 | 4979 |
| Vector search p50 (ms) | 9.0 | 21.8 |
| Vector search p95 (ms) | 11.1 | 35.8 |
| Vector search p99 (ms) | 14.7 | 42.9 |
| QPS serial | 98 | 44 |
| QPS concurrent (8) | 264 | 260 |
| p95 under concurrency (ms) | 39.8 | 57.9 |
| Filtered search p50 (ms) | 6.8 | 30.0 |
| Filtered search p95 (ms) | 9.8 | 50.9 |
| Recall@10 vs exact | 0.976 | 0.989 |
| Hybrid p50 (ms) | 14.5 | 60.8 |
| Hybrid p95 (ms) | 26.5 | 147.7 |
| Hybrid p99 (ms) | 67.0 | 247.1 |
| Hybrid QPS serial | 60.1 | 13.9 |
| Hybrid QPS concurrent (8) | 88.3 | 93.4 |
| Hybrid text-hit@10 | 1.000 | 1.000 |
| Cold start to first query (s) | 2.2 | 9.2 |
Reading the numbers¶
Where raggio wins. Every search latency, including the one a flat index is "supposed" to lose: raw vector search is 2.4x faster than HNSW at p50 (9.0 vs 21.8 ms) and 3.2x at p95, serial QPS is 2.2x higher, and hybrid and filtered search win at every percentile. The reason is that the two engines are bound by different resources: the 4-bit brute-force scan streams a ~500 MB quantized index sequentially and is memory-bandwidth-bound, so Grace's unified LPDDR5x feeds it at full rate across 20 cores — while HNSW traversal is a chain of dependent random reads that no amount of bandwidth or cores accelerates. Footprint stays the headline elsewhere: ~13x less memory under load, 5x less disk (the raw float32 vectors alone are 3.4 GB — the whole raggio deployment is 980 MB), and a 4x faster cold start. Both engines surface the lexically-targeted document in the fused top-10 for 100% of hybrid queries (text-hit@10 = 1.000).
Where Weaviate wins. Ingest: 204 s vs 500 s — HNSW construction parallelizes across all 20 cores, while raggio's durable single-writer journal pipeline becomes the bottleneck (the POSTs themselves complete in ~256 s; the rest is queue drain). Recall: 0.989 (uncompressed HNSW) vs 0.976 (4-bit quantization with TQ+ calibration) — the price of the 8x index compression. And hybrid concurrent QPS by a hair (93.4 vs 88.3).
Ingestion. raggio journals each batch to SQLite before returning 202, replays interrupted jobs after a crash, marks a job done only after the vector index is synced to disk, and returns the journal's pages to the OS as jobs complete — the 1,105 vec/s and the 8.3-minute wall time include all of that. Peak transient disk during a deep ingest backlog is roughly the size of the un-drained journaled payloads on top of the steady-state footprint.
Reproducing¶
# containers (podman or docker), one volume each so re-runs skip ingest
podman build -t raggio .
podman run -d --name bench-tv --memory 1g -p 18000:8000 -v bench-tv:/data \
-e ROOT_API_KEY=bench raggio
podman run -d --name bench-wv --memory 8g -p 18080:8080 -v bench-wv:/var/lib/weaviate \
-e AUTHENTICATION_ANONYMOUS_ACCESS_ENABLED=true \
-e PERSISTENCE_DATA_PATH=/var/lib/weaviate \
semitechnologies/weaviate
uv run python bench/bench.py # full run, both engines
uv run python bench/bench.py --engine raggio --reingest # one engine, wipe first
uv run python bench/bench.py --limit 20000 --queries 100 # quick smoke
The harness (bench/bench.py) ingests, then measures serial / concurrent /
filtered / hybrid search, recall against cached exact ground truth, memory,
disk, and cold start, and writes bench/results.md. Ingested data stays in
the volumes and is fingerprinted, so repeat runs start straight at the search
phases; ingest wall time is measured on --reingest runs. Supporting
decomposition tools live next to it: bench/microbench.py (kernel / FTS /
hydration cost split), bench/ivf_probe.py (IVF feasibility sweep), and
bench/cal_probe.py (TQ+ calibration flows).