Disclosure: I maintain omind, a separate
Apache-2.0 project solving an adjacent problem (durable agent memory in a plain-Markdown
Obsidian vault). I read claude-obsidian's source before filing and I'm linking omind's
implementation as a worked reference — not proposing you adopt its code.
Problem
claude-obsidian is unusually disciplined about not overclaiming retrieval quality.
scripts/contextual-prefix.py says it plainly:
Anthropic reported a 35-49% retrieval-failure reduction in its upstream experiments;
this local implementation requires evaluation on each vault and makes no fixed gain claim.
The README repeats it ("Model-based retrieval falls back to deterministic BM25 when the
embedding or reranking stage cannot be trusted"), and CONTRIBUTING makes it a rule: "Do
not add capability, performance, or competitor claims without current, traceable
evidence."
The problem is that the product ships no way to produce that evidence. The retrieval
stack has at least four independently tunable stages —
- prefix tier (
anthropic-api / claude-cli / synthetic / skipped),
- chunking (2,000-char target, 4,000 hard split, 200 overlap),
- BM25 (
k1=1.5, b=0.75),
- rerank (cosine on a selected Ollama model, or no-op),
— and no harness that answers "did that change help?" for any of them.
tests/test_retrieve.py and test_bm25_index.py verify correctness (right shape, right
ordering given fixed scores), which is a different question from quality. The one piece
of tooling that sounds relevant, scripts/benchmark-runner.py, isn't in the public tree at
all — tests/test_benchmark_tools.py guards on TOOLS_PRESENT and skips. And nothing in
the repo mentions recall, MRR, nDCG, or any ranking metric.
The practical consequence: a maintainer cannot tell whether the synthetic prefix tier is
worth its complexity, whether the rerank stage earns the Ollama dependency, or whether a
chunk-size change regressed anything. --explain shows per-stage diagnostics for one
query, which is a debugging tool, not a measurement.
This is the one gap where I think the project's own stated values most clearly want the
feature.
Proposed solution
A labelled query set plus a metrics runner.
1. A version-controlled labelled set — a JSON file mapping queries to the page(s) that
should be retrieved:
{
"schema_version": 1,
"queries": [
{"query": "how does the transaction lock recover", "expect": ["wiki/concepts/Transactions.md"]},
{"query": "why is egress gated by default", "expect": ["wiki/concepts/Egress.md"]}
]
}
Ship a small fixture set against tests/fixtures/, and let users keep their own against a
real vault — vault-specific evaluation is exactly what contextual-prefix.py says is
required.
2. claude-obsidian.py bench --quality --vault PATH [--queries FILE] reporting
recall@1, recall@5, and MRR, plus a count of skipped targets (labelled pages that no longer
exist — otherwise vault drift silently deflates the score) and the worst individual misses,
which is where the diagnostic value actually is.
3. --strategy to hold everything constant but one stage, so --no-rerank,
--no-llm, and a chunk-size override become measurable rather than matters of taste.
omind's equivalent is run_quality in
src/omind/bench.py
(omind bench --quality), which reports exactly that set — recall@1, recall@5, MRR,
skipped targets, worst misses — against a version-controlled labelled set on the live
vault. It's how we justified keeping some retrieval stages and dropping others;
docs/retrieval.md
records the measured before/after.
Alternatives considered
- Keep it private.
benchmark-runner.py presumably exists in the dev tree. But then
contributors can't produce the evidence CONTRIBUTING demands of them, and the burden of
proving a retrieval change lands entirely on the maintainer.
- Golden top-k assertions in the test suite. Brittle in the wrong way — they fail on
any scoring tweak without saying whether quality moved up or down. Metrics degrade
gracefully; golden orderings don't.
- nDCG with graded relevance. Better metric, much worse labelling ergonomics. Binary
relevance with recall@k and MRR is the right first cut.
Scope
Compatibility
- Behavior change for existing v1.x vaults? No — read-only measurement, no vault writes.
- New opt-in (
bin/setup-*.sh)? No.
- New dependency? No — recall@k and MRR are a few lines of arithmetic over the JSON
retrieve.py already returns. Deliberately no numpy, no scikit.
Testing
Fully hermetic; this is one of the easier things to test offline:
- a fixture vault + fixture labelled set with a known answer — hand-computed recall@1,
recall@5, and MRR, asserted exactly;
- a labelled entry pointing at a deleted page counts as skipped, not as a miss (the
distinction that keeps the metric honest as a vault drifts);
- an empty/malformed labelled file fails with a usage error, not a divide-by-zero;
--no-rerank runs without Ollama and still reports metrics, using the existing no-op
rerank path;
- exit 10 when no index exists, matching
retrieve.py's established contract.
Because BM25 scoring is deterministic, the metric assertions can be exact rather than
tolerance-based — no flakiness.
Additional context
Problem
claude-obsidian is unusually disciplined about not overclaiming retrieval quality.
scripts/contextual-prefix.pysays it plainly:The README repeats it ("Model-based retrieval falls back to deterministic BM25 when the
embedding or reranking stage cannot be trusted"), and CONTRIBUTING makes it a rule: "Do
not add capability, performance, or competitor claims without current, traceable
evidence."
The problem is that the product ships no way to produce that evidence. The retrieval
stack has at least four independently tunable stages —
anthropic-api/claude-cli/synthetic/skipped),k1=1.5,b=0.75),— and no harness that answers "did that change help?" for any of them.
tests/test_retrieve.pyandtest_bm25_index.pyverify correctness (right shape, rightordering given fixed scores), which is a different question from quality. The one piece
of tooling that sounds relevant,
scripts/benchmark-runner.py, isn't in the public tree atall —
tests/test_benchmark_tools.pyguards onTOOLS_PRESENTand skips. And nothing inthe repo mentions recall, MRR, nDCG, or any ranking metric.
The practical consequence: a maintainer cannot tell whether the synthetic prefix tier is
worth its complexity, whether the rerank stage earns the Ollama dependency, or whether a
chunk-size change regressed anything.
--explainshows per-stage diagnostics for onequery, which is a debugging tool, not a measurement.
This is the one gap where I think the project's own stated values most clearly want the
feature.
Proposed solution
A labelled query set plus a metrics runner.
1. A version-controlled labelled set — a JSON file mapping queries to the page(s) that
should be retrieved:
{ "schema_version": 1, "queries": [ {"query": "how does the transaction lock recover", "expect": ["wiki/concepts/Transactions.md"]}, {"query": "why is egress gated by default", "expect": ["wiki/concepts/Egress.md"]} ] }Ship a small fixture set against
tests/fixtures/, and let users keep their own against areal vault — vault-specific evaluation is exactly what
contextual-prefix.pysays isrequired.
2.
claude-obsidian.py bench --quality --vault PATH [--queries FILE]reportingrecall@1, recall@5, and MRR, plus a count of skipped targets (labelled pages that no longer
exist — otherwise vault drift silently deflates the score) and the worst individual misses,
which is where the diagnostic value actually is.
3.
--strategyto hold everything constant but one stage, so--no-rerank,--no-llm, and a chunk-size override become measurable rather than matters of taste.omind's equivalent is
run_qualityinsrc/omind/bench.py(
omind bench --quality), which reports exactly that set — recall@1, recall@5, MRR,skipped targets, worst misses — against a version-controlled labelled set on the live
vault. It's how we justified keeping some retrieval stages and dropping others;
docs/retrieval.mdrecords the measured before/after.
Alternatives considered
benchmark-runner.pypresumably exists in the dev tree. But thencontributors can't produce the evidence CONTRIBUTING demands of them, and the burden of
proving a retrieval change lands entirely on the maintainer.
any scoring tweak without saying whether quality moved up or down. Metrics degrade
gracefully; golden orderings don't.
relevance with recall@k and MRR is the right first cut.
Scope
scripts/<name>) — or abenchverb on the existing CLICompatibility
bin/setup-*.sh)? No.retrieve.pyalready returns. Deliberately no numpy, no scikit.Testing
Fully hermetic; this is one of the easier things to test offline:
recall@5, and MRR, asserted exactly;
distinction that keeps the metric honest as a vault drifts);
--no-rerankruns without Ollama and still reports metrics, using the existing no-oprerank path;
retrieve.py's established contract.Because BM25 scoring is deterministic, the metric assertions can be exact rather than
tolerance-based — no flakiness.
Additional context
run_qualityinsrc/omind/bench.pydocs/retrieval.md