OSS · Python CLI
rag-retriever-bench
Retrieval problems disguise themselves as LLM problems.
Measure your retriever before blaming your LLM. Seven vector databases benchmarked on the same corpus, the same queries and the same metrics: recall, nDCG and latency, with no LLM judge.
View on GitHub 100k-run report →
- Pick the right vector DB 7 databases, 9 configurations, one leaderboard
- Trust your recall recall@k, MRR, nDCG against complete ground truth
- Catch silent degradation self_check verifies the index is really used
- Ship with known latency p50 / p95 / p99 measured on 100k passages
- Open Source · MIT
- No LLM judge
- 100k-passage public report
- Created by Ken Imoto
Three steps
-
rag-retriever-bench prepareDownload the dataset, sample the corpus, embed it once (cached). -
rag-retriever-bench runBenchmark all 9 configurations against the same queries. -
results/*.mdCompare the leaderboard: quality, latency, ingestion and self_check evidence per backend.
Who it’s for
- AI engineers picking a vector database with measured numbers instead of vendor benchmarks
- RAG & search developers verifying the index actually works before touching a single prompt
- AI platform teams standardizing one benchmark across the backends they operate
- LLM researchers who need reproducible retrieval baselines with complete ground truth
When to reach for it
- Before touching prompts: rule the retriever out first, so you stop debugging the wrong layer
- Before switching vector databases: decide with your own measured numbers instead of vendor benchmarks
- Before production deployment: verify with self_check that the index is not being silently bypassed
- When search feels off but nothing errors: the three silent-degradation cases below are exactly that
Install
git clone https://github.com/kenimo49/rag-retriever-bench
cd rag-retriever-bench
pip install -e ".[all]"
docker compose up -d
cp .env.example .env # OPENAI_API_KEY (embeddings only) Server backends (pgvector, ClickHouse, Qdrant, Weaviate, Milvus) run via docker compose; Chroma and LanceDB are embedded and need no services.
What it measures
- Retrieval quality: recall@k, hit@k, MRR@k, nDCG@k against human-annotated relevance judgments, no LLM judge
- Query latency: client-side p50 / p95 / p99 / mean per query, serialization included
- Ingestion: bulk-load seconds and index-build seconds per backend
- 7 backends, 9 configurations: pgvector, ClickHouse (HNSW ×2 + brute force), Qdrant, Weaviate, Milvus, Chroma, LanceDB
- A self_check per backend: an EXPLAIN or server-statistics probe that verifies the ANN index was actually used
- HNSW parameters aligned across backends (m=16, ef_construction=64, ef_search=100), so differences reflect the engine, not the tuning
- MIRACL-ja dataset: the sampled corpus always contains every positive passage, so recall is measured against complete ground truth
Measured results (100k passages)
Server backends (network hop included)
| backend | recall@10 | nDCG@10 | p50 (ms) | p95 (ms) |
|---|---|---|---|---|
| pgvector (HNSW) | 0.936 | 0.878 | 4.8 | 6.1 |
| ClickHouse (HNSW) | 0.923 | 0.866 | 43.1 | 51.5 |
| ClickHouse (HNSW, g=128) | 0.921 | 0.866 | 11.5 | 13.1 |
| ClickHouse (brute force) | 0.952 | 0.891 | 65.9 | 86.7 |
| Qdrant (HNSW) | 0.947 | 0.888 | 3.3 | 4.1 |
| Weaviate (HNSW) | 0.929 | 0.873 | 1.8 | 2.1 |
| Milvus (HNSW) | 0.940 | 0.882 | 2.0 | 2.3 |
Embedded backends (in-process; latency not comparable to server backends)
| backend | recall@10 | nDCG@10 | p50 (ms) | p95 (ms) |
|---|---|---|---|---|
| Chroma (HNSW, embedded) | 0.929 | 0.871 | 1.7 | 1.9 |
| LanceDB (IVF_HNSW_SQ, embedded) | 0.811 | 0.777 | 1.8 | 1.9 |
Brute force sets the quality ceiling at recall 0.952; the closest ANN configuration is Qdrant at 0.947. At this scale the HNSW engines differ mainly in latency: Weaviate and Milvus answer under 2.5 ms at p95, while ClickHouse pays 13 to 52 ms depending on index granularity.
How much one choice moves the numbers
Same corpus, same embeddings, same HNSW budget. Only the decision below changed.
Index choice
recall@10 0.811 → 0.947
LanceDB IVF_HNSW_SQ vs Qdrant HNSW: a recall gap of 0.136 with zero errors raised on either side.
One index parameter
p95 51.5 ms → 13.1 ms
ClickHouse HNSW with index granularity 128: latency drops to a quarter while recall stays put (0.923 → 0.921).
Usage
# 10k-passage smoke run (MIRACL-ja downloads on first use)
rag-retriever-bench run -c configs/miracl-ja.yaml --corpus-size 10000
# full 100k run
rag-retriever-bench run -c configs/miracl-ja.yaml
# prepare only: download dataset, sample corpus, embed
rag-retriever-bench prepare -c configs/miracl-ja.yaml
What it caught
Three of the nine configurations shipped a way to silently degrade, with zero errors raised. The self_check probe caught all three during development:
- ClickHouse HNSW: skip the OPTIMIZE FINAL step and queries quietly fall back to brute force. The EXPLAIN probe caught it before it could surface as a "ClickHouse is slow" result.
- Qdrant: with indexing_threshold_kb above the corpus size the index is never built; the collection reports indexed_vectors_count = 0 and searches run on the raw segment.
- Milvus: after a flush it could load a stale snapshot missing sealed segments, returning partial results with no error.
Every one of those degradation paths produced plausible-looking numbers. Reports land in results/ as Markdown + JSONL, one row per backend with quality, latency, ingestion, and the index-verification evidence.
Numbers are MIRACL-ja + text-embedding-3-small on a single node. Embedded and server backends are reported in separate tables because their latency is not directly comparable. Measure your own data before deciding.
Why this tool exists
Most RAG teams optimize prompts before verifying retrieval quality. When an answer comes back wrong, the model takes the blame and the prompt gets rewritten, while the retriever that returned the wrong passages is never questioned. This harness exists because retrieval errors are routinely mistaken for LLM failures. Three of the nine configurations above could degrade with zero errors raised; nothing in the answers would have pointed at the index. Measure retrieval first.
Alongside RAGAS and DeepEval
Complementary layers: RAGAS and DeepEval score the contexts and answers your pipeline returns, while this harness benchmarks the retrieval backend underneath before you commit to one.
| rag-retriever-bench | RAGAS | DeepEval | |
|---|---|---|---|
| Evaluates | The retrieval backend itself | Contexts and answers your pipeline returns | Contexts and answers your pipeline returns |
| Provisions real vector DBs | ✓ 7 backends via docker compose | — | — |
| Ground truth | Human-annotated qrels, no LLM judge | Mainly LLM-as-judge; non-LLM and ID-based variants exist | LLM-as-judge based |
| Latency and ingestion | ✓ p50 / p95 / p99 + ingestion | — | — |
| CLI | ✓ | ✓ | ✓ |
| License | MIT | Apache-2.0 | Apache-2.0 |
A dash means the official docs describe no such feature as of July 2026: both frameworks are designed to score what your pipeline returns, and that is the intended division of labor. Pick the backend here, then score your pipeline with them.
FAQ
Do I need an OpenAI API key?
Only for embeddings (text-embedding-3-small by default). Embeddings are cached as .npy files, so re-runs cost nothing, and the benchmark itself never calls an LLM.
Can I use local embedding models such as sentence-transformers or Ollama?
Not in v0.1: embeddings come from the OpenAI API only. Switching among OpenAI embedding models works through the config.
Does it support BM25 or hybrid search?
Not yet. v0.1 is vector-only; hybrid (vector plus full-text) is on the v0.2 roadmap.
Can I compare embedding models with it?
That is out of scope by design: every backend receives identical embeddings, so score differences reflect the engine. For embedding model comparison, MTEB and JMTEB are the right tools.
Can I benchmark my own corpus?
Not yet. v0.1 pins MIRACL-ja, where the sampled corpus always contains every positive passage, so recall is measured against complete ground truth. Bring-your-own-corpus evaluation is on the roadmap.
Why retriever-only? Why not evaluate the whole RAG pipeline?
Because retrieval and generation fail differently and need different fixes. Separating the two is increasingly the accepted practice in RAG evaluation: if the right passages never leave the database, no prompt or model swap will fix the answer. This harness isolates that layer so you know which half to debug.
Why not just use RAGAS?
Use RAGAS, but for a different question. RAGAS scores the contexts and answers your pipeline returns; it does not provision vector databases or measure their latency. This harness works one layer earlier: which database and which index, verified against human-annotated qrels with no LLM judge.
Why not DeepEval?
Same division of labor. DeepEval unit-tests your LLM application with mostly LLM-as-judge metrics, which is useful once a pipeline exists. Benchmarking which vector database should sit underneath it, on equal HNSW settings with measured p95 latency, is the part this harness covers.
About the author
Built by Ken Imoto: 300+ technical articles across Zenn, Qiita, Dev.to and this site, 40+ books in 4 languages, 400K+ pageviews on Zenn and Qiita, 4 research papers on Zenodo, and creator of the LLMO Framework.
Related
The measurements from this harness feed rag-db-advisor, an MCP/CLI advisor that answers RAG-stack questions from the measured evidence.
Related developer tools
- opencut-mcp MCP server that drives OpenCut classic from any MCP client. Playwright holds the editor session; twelve tools expose the timeline, media assets, and export pipeline. Runs against a fork of OpenCut classic.
- mcp-scorecard Pre-flight checks for MCP servers: passive token footprint, use-case scoping, security, and name safety. LLM-facing quality scorecard.
- 404 Games Ten single-file canvas mini games for your 404 page, inspired by famous error pages. Vanilla JS, zero dependencies, theme-aware. Live on this site's 404.
- historymap Turn a YAML file into a corporate-style product-history timeline. Ten layouts, switchable right on the page. Static, self-contained, iframe-embeddable.