Skip to content

release: v1.9.0 — evidence-driven ranking, half the query latency - #25

Merged
denfry merged 3 commits into
mainfrom
release/1.9.0
Sep 2, 2026
Merged

release: v1.9.0 — evidence-driven ranking, half the query latency#25
denfry merged 3 commits into
mainfrom
release/1.9.0

Conversation

@denfry

@denfry denfry commented Sep 2, 2026

Copy link
Copy Markdown
Owner

Problem

The retrieval benchmark was 36 hand-written queries against this repository, in
Python. At that size a ±0.03 MRR move is one query, so a delta column could not
separate a real improvement from a reshuffle, and nothing guarded against tuning
the ranker to a single repository in a single language.

Three defects were found once the measurement was strong enough to see them:

  1. Cross-retriever agreement never reached the score. RRF fuses on
    (path, line-bucket). A symbol definition at line 40 and a lexical hit at
    line 120 in the same file land in different buckets, so a file that two
    retrievers agreed on fused as two weak candidates rather than one strong one.
    agreeing_sources was computed but only used for confidence.
  2. Design docs outranked implementation. Prose describing a feature matches a
    natural-language question more literally than the code implementing it.
  3. Two signals were charging latency for nothing. Fuzzy identifier matching
    was ~20% of query time; the quality credited to SimHash dedup was actually
    coming from the pool widening that enabling dedup implicitly caused.

Change

  • Evaluation first. tests/eval/gen_queries.py derives objective, leak-free
    ground truth from any git repository (commit subject → files that commit
    changed; the query text is git metadata, not corpus content).
    run_eval.py --corpus REPO:QUERIES pools repositories, and every non-baseline
    row carries a seeded paired bootstrap CI and permutation p-value.
  • Fusion credits each candidate, at file_agreement_weight (0.4), with the
    RRF mass of retrievers that found its file at another locator, excluding
    retrievers already counted at that locator.
  • Documentation prior −0.05 → −0.20; generated/vendor → −0.25 to stay the
    least-preferred role; MAX_ABS_PRIOR pins the tiebreaker invariant.
  • Fuzzy matching becomes a recall fallback (fuzzy_fallback_min).
  • candidate_pool_multiplier replaces the implicit over-fetch.
  • SimHash folds repeated tokens by multiplicity and caches digests —
    bit-for-bit identical, ~2× faster.

Fixes two latent bugs: synonym expansions were being reported as exact symbol
matches (+0.20 rerank, unconditional high confidence), and duplicate
suppression broke ties in the opposite direction to its own docstring.

Results

Against v1.8.0 over 305 queries (Python, Java, TypeScript), identical harness,
old source checked out in a worktree:

Metric v1.8.0 1.9.0 Δ p
MRR 0.564 0.591 +0.027 <0.001
MAP 0.433 0.461 +0.028 <0.001
nDCG@10 0.503 0.527 +0.024 <0.001
recall@5 0.529 0.560 +0.031 <0.001
P@5 0.182 0.192 +0.009 0.001
tokens/query 1183 1091 −8%
p50 / p95 / p99 ms 78.6 / 193.2 / 277.1 51.2 / 94.9 / 152.2 −35% / −51% / −45%

Rejected by measurement

Kept out of the default because the data did not support them: IDF-weighted
lexical coverage (significantly worse, p=.011), stem-prefix expansion,
IDF posting-scan candidate generation, a larger FTS pool, score-aware fusion,
intent weight retuning, and a file-length prior — the last looked compelling
descriptively (Java top-1 results averaged 124.5 chunks vs 8.6 for ground truth)
but the medians matched, the mean was outlier-driven, and it was significantly
harmful at every strength tried.

An oracle bound explains why every recall-side idea failed: perfectly reranking
the current candidate pool would give MRR 0.905 vs 0.593 actual, so the
remaining loss is ranking (0.312), not candidate generation (0.095).

Verification

  • 506 tests pass; coverage 84.77% (gate 80%)
  • ruff check src tests clean; mypy clean across 60 files
  • skill copies in sync at 1.9.0
  • python -m build, twine check, and scripts/release_smoke.py all pass

Compatibility

No CLI, MCP, Skill, or API surface changes. Every new signal sits behind an
independent RetrievalTuning flag, and RetrievalTuning.baseline() still
reproduces the pre-1.8.0 pipeline so the ablation baseline does not drift.

…n half

RRF fused on (path, line-bucket), so a symbol definition at line 40 and a
lexical hit at line 120 in the same file produced two weakly-scored candidates
instead of one strong one — two retrievers agreeing on a file never reached the
score at all. Each candidate now also receives, at file_agreement_weight (0.4),
the RRF mass of every retriever that found its file at another locator,
excluding retrievers already counted at that locator so nothing double-counts.

Deepen the documentation prior from -0.05 to -0.20: prose describing a feature
matches a natural-language question more literally than the code implementing
it, so design notes were displacing the modules they describe. Because this only
reorders prose relative to code, documentation-seeking queries improved too
(category MRR 0.579 -> 0.612); -0.35 overshoots and collapses them. Generated
and vendored paths move to -0.25 to stay the least-preferred role, and
MAX_ABS_PRIOR pins the invariant that priors remain tiebreakers.

Fuzzy identifier matching becomes a recall fallback, running only when the
precise lookup named no symbol and returned fewer than fuzzy_fallback_min rows.
It moved no ranking metric across 305 queries while accounting for ~20% of query
latency; typo and acronym recall is unchanged because those are exactly the
queries where the precise lookup comes up empty.

Make candidate over-fetch explicit as candidate_pool_multiplier instead of an
implicit side effect of enabling dedup or MMR. That alone showed the quality
previously credited to SimHash dedup was really the wider pool; dedup is kept
for what it measurably does, which is cutting the duplicate rate of returned
snippets from ~1.6% to ~0%. SimHash itself now folds repeated tokens by
multiplicity and caches token digests, bit-for-bit identical but ~2x faster.

Also fixes two latent bugs: exactness was read from SQL relative to whichever
needle retrieved the row, so a synonym expansion ("config" for "configuration")
marked an unrelated symbol as an exact match, worth +0.20 at rerank and an
unconditional high confidence; and duplicate suppression broke ties with ">",
handing the slot to whichever equal-scoring copy arrived last and contradicting
its own documented "ties favor input order".

Measured against 1.8.0 over 305 queries on Python, Java and TypeScript corpora:
MRR +0.027, MAP +0.028, nDCG@10 +0.024, recall@5 +0.031 (all p < 0.001), with
p50 latency 78.6ms -> 51.2ms and p95 193.2ms -> 94.9ms.
The benchmark was 36 hand-written queries against this repository in Python.
At that size a +-0.03 MRR move is a single query, so a delta column could not
tell an improvement from a reshuffle, and nothing guarded against tuning the
ranker to one repository in one language.

gen_queries.py mints objective ground truth from any git repository by pairing a
human-written commit subject with the files that commit actually changed. Unlike
docstring-derived benchmarks the query text lives in git metadata rather than in
the indexed corpus, so the answer is not copied into the document being
retrieved. Only localised, described changes survive: no merges, reverts,
releases, version bumps or formatting commits; at most --max-files files, all of
which must still exist at HEAD; duplicate subjects collapse. Changelog-style
files are never accepted as answers because they paraphrase subjects, and any
commit touching tests/eval is dropped so the set cannot grade itself.

run_eval.py gains --corpus REPO:QUERIES to pool several repositories into one
benchmark, weighting every query equally rather than averaging per-corpus
averages. Every non-baseline row now reports a seeded paired bootstrap 95%
confidence interval and a paired permutation p-value, so signals ship on the
strength of that test rather than the sign of a delta.

The report also tracks agent-facing noise: mean emitted snippet tokens (results
past the budget carry no snippet and are not billed), duplicate rate of returned
results, and p99 latency.
Ranking quality and latency both improve against 1.8.0, measured over 305
queries on Python, Java and TypeScript corpora: MRR 0.564 -> 0.591, MAP
0.433 -> 0.461, nDCG@10 0.503 -> 0.527, recall@5 0.529 -> 0.560 (all
p < 0.001), emitted tokens 1183 -> 1091, p50 78.6ms -> 51.2ms, p99
277.1ms -> 152.2ms.

Documentation now describes the shipped implementation rather than the plan.
RETRIEVAL.md gains the cross-locator agreement formula and a source-prior table,
and drops a "recency" rerank feature that was listed but never implemented.
BENCHMARKS.md adds the retrieval eval as a fourth surface and is explicit that
it measures version-over-version deltas, not absolute superiority over any
other tool.
@denfry
denfry merged commit b48ed14 into main Sep 2, 2026
10 checks passed
@denfry
denfry deleted the release/1.9.0 branch September 2, 2026 07:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant