Benchmarks

Search accuracy on real BEIR benchmarks, DuckDB mirror speedups at scale, write-path tuning, and how to run the benchmark suite yourself.

Measured 2026-08-20, commit 00a361c, Apple Silicon MacBook Pro, CPython 3.13. Search accuracy uses real IR benchmarks (BEIR); speed numbers are smoke-scale (50 docs / 50 graph nodes / 1,000 analytics rows) unless noted. Full-scale runs (--benchmark-full) exist but were not part of this study.

Search accuracy (BEIR)

Evaluated on two BEIR datasets with real queries and relevance judgments: NFCorpus (3,633 medical docs, 324 queries, graded 0–3) and SciFact (5,183 scientific abstracts, 301 queries, binary). Embeddings: ChromaDB’s bundled MiniLM.

Dataset Mode nDCG@10 Recall@10 P@10 MRR
NFCorpus keyword 0.3083 0.1489 0.2167 0.5128
NFCorpus semantic 0.3145 0.1542 0.2418 0.5063
NFCorpus hybrid 0.3429 0.1699 0.2467 0.5548
SciFact keyword 0.6683 0.7956 0.0883 0.6348
SciFact semantic 0.6484 0.7883 0.0890 0.6068
SciFact hybrid 0.7031 0.8443 0.0943 0.6649

Fusion-weight sensitivity (hybrid nDCG@10)

fts_weight 0.2 0.5 (default) 0.8
NFCorpus 0.3271 0.3429 0.3332
SciFact 0.6741 0.7031 0.6948

Findings

  1. Hybrid fusion is the right default. It beats both single modes on every metric in both domains (+11% nDCG over keyword on NFCorpus, +5% on SciFact). RRF captures the lexical and semantic regimes.
  2. fts_weight=0.5 is the sweet spot and the curve is flat (spread < 0.03) — no tuning needed.
  3. The hash embedding fallback is a 5.3× accuracy cliff (nDCG 0.059 vs 0.315, near-random). The fallback exists for offline smoke tests, not production.

Analytics: DuckDB mirror vs raw SQLite

Same queries on the SQLite table (raw_query) and the DuckDB mirror (olap.query), median of 5 runs.

Rows Query SQLite DuckDB Speedup
10k full-scan agg 1.46ms 0.24ms 6.2×
10k group-by 3.42ms 0.47ms 7.3×
100k group-by 34.13ms 1.37ms 24.9×
100k filtered agg 8.85ms 0.19ms 46.4×
1M full-scan agg 70.98ms 1.23ms 57.6×
1M group-by 378.31ms 2.83ms 133.6×
1M filtered agg 76.86ms 0.22ms 351.9×
1M join 640.17ms 8.45ms 75.8×

Findings

  1. The DuckDB mirror pays off immediately and compounds with scale. Even at 10k rows it is 4–7× faster; at 1M rows, 58–352×. There is no “SQLite wins at small scale” regime for analytical queries.
  2. Filtered aggregation is the columnar sweet spot (352× at 1M rows) — vectorized scans over WHERE clauses.
  3. Point lookups are the mirror’s weak spot (2.4× at 1M) — the mirror has no index. Use get()/query() for point lookups.
  4. The mirror is cheap to maintain: full sync copies 1M rows in ~0.6s; incremental journal sync adds no measurable write overhead.

Write path

2026-08-20 tuning

Profiling insert_batch revealed the bottleneck was not journal writes but a fresh SQLite connection per row — schema lookups opened ~200k connections per 100k rows. Fixes: the batch cursor is threaded through the hot paths, per-row schema reads are hoisted, and _process_journal uses one connection end-to-end.

The journal remains the write bottleneck (~2,000 rows/s) — dominated by per-row Python + journal overhead. Optimization candidates: batched journal writes, dropping the per-row re-select.

Versioned-table rollback (0.7.0)

  • Removal-heavy rollbacks ~20× faster: 1,000 removed rows 1,980ms → 92ms (batched path; ~122ms with Chroma + DuckDB live). Root cause: rollback() looped per removed row through public delete(), paying one connection + transaction and ~10 statements per row. Removals are now set-based — one transaction, chunked DELETE … WHERE pk IN (…), hash-chain tombstones via executemany.
  • Batched restores: update-heavy rollbacks re-apply state as new versions — 1,000 restored rows were 3,931ms (41× the removal path); now 107ms at 100k-row tables via split batched INSERT/UPDATE. Chroma re-embedding of restored LONGTEXT remains by design.

Running the benchmarks yourself

Benchmarks are separate from the default test run:

uv run python -m pytest -q
# 262 passed, 48 skipped (benchmarks under tests/benchmarks/)
uv run python -m pytest tests/benchmarks -q --run-benchmarks --benchmark-disable
# 48 passed — smoke scale: 50 docs, 50 graph nodes, 1,000 analytics rows
uv run python -m pytest tests/benchmarks -q --run-benchmarks
# timed loops; full-scale via --benchmark-full
Pre-release gate

Split smoke runs are the recommended pre-release benchmark checks: search, then analytics+graph, then storage+concurrent.

Source studies live in the repo: docs/PERFORMANCE.md and docs/BENCHMARKS.md.

Source of truth for this page: HybridDB · open-assistants-lab/HybridDB