Skip to content

feat: Add a learned correction to the ranking stage - #312

Closed
flyinprogrammer wants to merge 1 commit into
MinishLab:mainfrom
flyinprogrammer:feat/learned-ranker
Closed

flyinprogrammer wants to merge 1 commit into
MinishLab:mainfrom
flyinprogrammer:feat/learned-ranker

Conversation

@flyinprogrammer

@flyinprogrammer flyinprogrammer commented Oct 19, 2026 •

Copy link
Copy Markdown

Closes #310

Summary

A small tree ensemble rescales each candidate's boosted score using signals the ranking stage already computes. It's evaluated in numpy from a packaged JSON file, so there's no new runtime dependency.

 search(query, …)
-  retrieve, fuse, boost                    # inline
-  rerank_topk(boosted)
+  signals = score_candidates(query, …)     # same retrieval/fusion/boosts, returns RankingSignals
+  scores = signals.boosted
+  if load_ranker() is not None:            # ranking/ranker.json; SEMBLE_LEARNED_RANKER=0 disables
+    scores = boosted × exp(ranker.predict(ranking_features(signals)))
+  rerank_topk(scores)                      # path penalties + file saturation, unchanged
 src/semble/ranking/
 ├── boosting.py        # + query_boost_deltas (stem/definition parts, for features); apply_query_boost unchanged
+├── learned.py         # RankingSignals, 21 features, numpy TreeEnsemble, load_ranker
+└── ranker.json        # 150 trees × 7 leaves, 69 KB
 benchmarks/
+└── train_ranker.py    # LightGBM via `uv run --with lightgbm` (never a dependency); --cv for leave-one-repo-out
  • Features: list ranks and scores; fused rank and score; file, stem and definition boost amounts; path penalty; file shape (chunks per file, whether this is the file's best chunk, path depth).
  • Training: LambdaRank, starting from the current boosted score, so an empty model reproduces today's ranking. Only the best-ranked chunk per annotated target is labelled relevant.

Evidence

  • Off switch is exact: with SEMBLE_LEARNED_RANKER=0, all 1,251 benchmark queries score exactly as on main.

  • Exported model matches LightGBM: the numpy evaluator differs from LightGBM's predictions by at most 3e-15 (checked by train_ranker.py before writing).

  • Held-out quality:

    Evaluation NDCG@10 Δ 95% CI (by repo)
    Benchmark, leave-one-repo-out (train_ranker.py --cv) 0.8550 → 0.8627 +0.78 [+0.36, +1.19]
    Commit-message queries (Benchmark: add a commit-message query set so ranking changes aren't tuned to the annotations' style #301): 840 from 21 repos, trained on the other 42 repos' benchmark queries 0.7637 → 0.7721 +0.84 [+0.40, +1.30]
  • Latency (run_benchmark, M5 Max): median query 1.3 ms → 3.1 ms, 95th percentile 6.0 → 7.7 ms. Indexing is unchanged.

  • Tests: new tests/test_learned_ranker.py covers the evaluator on a hand-built tree, feature construction, score rescaling, and that the packaged model matches FEATURES and can be disabled. make test 395 passed; make lint and make typecheck clean.

Merge Danger

Door: two-way

SEMBLE_LEARNED_RANKER=0 or a revert restores today's ranking exactly, and no index or cache format changes.

Blast Radius: ranking

A small LambdaRank tree ensemble (150 trees, 7 leaves) rescales each candidate's
boosted score by exp(correction), using 21 signals the ranking stage already
computes: list ranks and scores, fusion, the file/stem/definition boosts, path
penalty and file shape. It is evaluated with numpy from the packaged
ranking/ranker.json (69 KB), so there is no new runtime dependency.

search() is split into score_candidates(), which returns every intermediate
score as RankingSignals, and the final rerank. apply_query_boost is unchanged;
query_boost_deltas exposes its stem and definition parts as features. Setting
SEMBLE_LEARNED_RANKER=0 restores the hand-tuned ranking exactly.

benchmarks/train_ranker.py retrains the model from the benchmark annotations
(LightGBM via `uv run --with lightgbm`, never a dependency) and checks the
numpy evaluator against LightGBM before writing. Only the best-ranked chunk per
target is labelled relevant.

Leave-one-repo-out on the benchmark: NDCG@10 0.8550 -> 0.8627 (+0.78).
On 840 commit-message queries from 21 repos the model never trained on:
+0.84 [+0.40, +1.30]. Median query latency +1.8 ms on an M5 Max.
@stephantul stephantul closed this Oct 19, 2026
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.

Proposal: a small learned correction to the ranking stage (no new runtime dependency)

2 participants