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
Terminal demo: rag-retriever-bench help output, then a rendered report from a real 100k-passage MIRACL-ja run comparing 9 backend configurations on recall, nDCG, and latency

Three steps

  1. rag-retriever-bench prepare Download the dataset, sample the corpus, embed it once (cached).
  2. rag-retriever-bench run Benchmark all 9 configurations against the same queries.
  3. results/*.md Compare 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)

MIRACL-ja, 100,000 passages · 860 queries · text-embedding-3-small · top_k=10 · measured 2026-07-11 on a single node.

Server backends (network hop included)

backendrecall@10nDCG@10p50 (ms)p95 (ms)
pgvector (HNSW)0.9360.8784.86.1
ClickHouse (HNSW)0.9230.86643.151.5
ClickHouse (HNSW, g=128)0.9210.86611.513.1
ClickHouse (brute force)0.9520.89165.986.7
Qdrant (HNSW)0.9470.8883.34.1
Weaviate (HNSW)0.9290.8731.82.1
Milvus (HNSW)0.9400.8822.02.3

Embedded backends (in-process; latency not comparable to server backends)

backendrecall@10nDCG@10p50 (ms)p95 (ms)
Chroma (HNSW, embedded)0.9290.8711.71.9
LanceDB (IVF_HNSW_SQ, embedded)0.8110.7771.81.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
Terminal screenshot of a finished 100k-passage run: two rendered tables listing every backend with recall@10, nDCG@10, MRR@10, hit@10, p50/p95/p99 latency, load and index-build seconds
What a finished run looks like: one leaderboard row per backend, from the real 100k report.

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-benchRAGASDeepEval
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

All products →

← All products