Benchmark Results

May 23, 2026 · View on GitHub

capy's benchmark suite validates two core claims: that FTS5/BM25 search retrieves the right information, and that context reduction preserves the information the LLM needs. All metrics are deterministic — no LLM-in-the-loop evaluation.

Setup

  • Corpus: 156 test cases across 5 content types (markdown, JSON, plaintext, transcripts, curated knowledge)
  • Fixtures: Synthetic but realistic — documentation, API responses, logs, conversation transcripts, ADRs
  • Search engine: SQLite FTS5 with two-layer RRF (Porter stemming + trigram), fuzzy Levenshtein correction, post-processing (diversification, proximity reranking, entity boosting)
  • No embeddings: Pure lexical search. No vector DB, no embedding model, no API key
  • Go version: go1.25.2
  • Dataset hash: sha256:7d45338724b0... (fixture manifest hash — qualstat aborts if comparing reports with different hashes)

Retrieval Quality

Standard IR metrics. A result is "relevant" if it contains any needle substring from the test case.

Overall

MetricScoreCases
R@10.897156
R@30.974
R@50.987
R@100.994
NDCG@100.950
MRR0.938
Rank Ceiling Pass1.000

By Content Type

Content TypeR@1R@5R@10NDCG@10MRRCases
Transcript0.9671.0001.0000.9880.98330
Curated0.9331.0001.0000.9750.96730
Plaintext0.9001.0001.0000.9570.94430
Markdown0.8891.0001.0000.9480.93836
JSON0.8000.9330.9670.8810.85730

JSON is the weakest. Recursive key-path chunking sometimes splits deeply nested structures across chunk boundaries, making exact-match queries harder. This is a known trade-off of the JSON chunker — preserving parent context helps, but deep nesting still causes boundary splits.

What the Metrics Mean

  • R@K: Is at least one relevant result in the top K? Binary per case, averaged across all cases.
  • NDCG@10: Normalized Discounted Cumulative Gain — rewards relevant results ranked higher.
  • MRR: Mean Reciprocal Rank — 1/position of the first relevant result.
  • Rank Ceiling Pass: Did the first relevant result appear at or above the expected rank?

Context Reduction (Needle-in-a-Haystack)

"Bytes saved" is a vanity metric if the reduced context drops information the LLM needed. Each test case defines needles — specific facts that must survive reduction. Compression is measured against intentSearch-style summaries (title + first-line preview per result), which is the actual surface that enters the LLM context.

Overall

MetricScoreCases
Compression Ratio49.8%156
Context Recall0.983
Perfect Recall Rate97.1%
Effective Compression49.7%

By Content Type

Content TypeCompressionContext RecallPerfect RecallEff. CompressionCases
Plaintext60.4%1.000100.0%60.4%30
Transcript59.0%1.000100.0%59.0%30
Curated48.4%1.000100.0%48.4%30
JSON44.3%0.92587.5%43.9%30
Markdown39.1%0.99097.9%39.1%36

Transcript and plaintext compress best because they have the largest haystacks relative to information density. Markdown compresses least because heading-aware chunking preserves structural context (headings, code fences), producing larger summary sections. JSON has the lowest context recall due to deeply nested structures splitting across chunk boundaries.

What the Metrics Mean

  • Compression Ratio: 1 - (summary_bytes / haystack_bytes). Higher = more bytes saved.
  • Context Recall: Fraction of needles found across all search results. 3 needles, 2 found = 0.67.
  • Perfect Recall Rate: Percentage of cases where every needle survived (Context Recall = 1.0).
  • Effective Compression: Compression × Recall. Partial credit — high compression with low recall scores poorly.

5000-Byte Threshold

capy auto-indexes output above 5000 bytes when an intent is provided. Below the threshold, raw output passes through. This measures the latency cost of the indexing path.

Output SizeLatencyIndexedOutput to ContextCompression
4,999 bytes1.7 msNo4,999 bytes
5,001 bytes322 msYes316 bytes93.7%
10,000 bytes7.0 msYes318 bytes96.8%
50,000 bytes24.0 msYes319 bytes99.4%

The 5,001-byte case shows the cold-start cost: first indexing into a fresh FTS5 database. Subsequent indexing (10K, 50K) is much faster because the database schema and indexes already exist.

Known Limitations

  1. Synthetic fixtures only. The corpus is hand-crafted, not sampled from real production data. Results may not generalize to all real-world content patterns.
  2. No embedding comparison. capy uses pure lexical search (FTS5/BM25). We don't benchmark against vector/embedding approaches because capy deliberately avoids them (no API key, no model dependency). A fair comparison would need a shared dataset — see COMPARISON.md.
  3. 156 cases is small. Enough for regression detection; not enough for statistical significance claims. We'd need 500+ cases to make strong generalization claims.
  4. Context reduction measures summaries, not LLM comprehension. We measure whether needles appear in the summary text. We don't measure whether an LLM can actually answer questions from the summary. That would require LLM-in-the-loop evaluation, which we deliberately avoid for determinism.

Reproducing

git clone https://github.com/serpro69/capy.git
cd capy
export CAPY_DB_KEY=test-key-for-development
make bench-quality    # → bench-results/{branch}.json

# View results
go run -tags fts5 ./cmd/qualstat bench-results/*.json

All fixtures, test code, and the qualstat CLI are committed. Results land in bench-results/ (gitignored). The dataset manifest hash in the JSON report ensures you're comparing results from identical fixtures.

Updating These Tables

The tables in this document are generated from the JSON report:

go run -tags fts5 ./cmd/qualstat --markdown bench-results/{branch}.json

Copy the output, replacing the tables above. The prose sections (methodology, caveats, known limitations) need manual review when results change significantly.