feat(agent): optional keyword stemming, measured and left off by default
The keyword stage had no stemming, so "training" and "trains" were unrelated terms. bm25::TokenFilter::Stemmed strips common English inflections (plurals, -ing/-ed, with consonant un-doubling) from documents and queries alike; BM25Index::build_with and HDF5Memory::set_token_filter select it, and the index records which filter built it so a stale one is rebuilt rather than mixed. Measured over the full LongMemEval haystack (500 questions, real MiniLM embeddings) rather than adopted on principle — and it is a trade, not a win: BM25 only Hit@1 53.8% Hit@5 75.0% Hit@10 81.6% MRR 0.6320 BM25 stemmed Hit@1 52.0% Hit@5 77.8% Hit@10 84.0% MRR 0.6320 Hybrid 0.4/0.6 Hit@1 51.6% Hit@5 81.4% Hit@10 87.8% MRR 0.6430 Hybrid stemmed Hit@1 50.2% Hit@5 81.4% Hit@10 88.2% MRR 0.6394 Conflation buys depth and costs the top rank: on BM25 alone MRR is unchanged to four decimal places, the deeper gains exactly offsetting the rank-1 loss. On the shipping hybrid configuration the vector stage already supplies most of that recall, so the trade is narrower and slightly negative. Default stays Plain; Stemmed is there for callers who want Hit@5/@10 over rank-1 precision. The stemmer is deliberately conservative — it only strips inflections, and only when the stem stays long enough to be meaningful, since an aggressive one also conflates unrelated words. Tests pin both the pairs that must meet and the pairs that must not. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
@@ -625,6 +625,34 @@ labelled data to tune against. Here there is, so the weighted sum is kept as
|
||||
the default. `Fusion::Rrf` remains available for callers whose stages are more
|
||||
evenly matched.
|
||||
|
||||
### Keyword tokenizer — stemming, full haystack, n=500
|
||||
|
||||
The keyword stage lowercases and splits on non-alphanumerics, with no stemming,
|
||||
so "training" and "trains" are unrelated terms. `TokenFilter::Stemmed` strips
|
||||
common English inflections (plurals, `-ing`/`-ed`, with consonant un-doubling)
|
||||
from documents and queries alike. Turn-level:
|
||||
|
||||
| Mode | Hit@1 | Hit@5 | Hit@10 | MRR | session Hit@1 |
|
||||
|---|---|---|---|---|---|
|
||||
| BM25 only | **53.8%** | 75.0% | 81.6% | 0.6320 | 86.2% |
|
||||
| BM25 only, stemmed | 52.0% | 77.8% | 84.0% | 0.6320 | 88.0% |
|
||||
| Hybrid 0.4/0.6 | 51.6% | **81.4%** | 87.8% | **0.6430** | 91.0% |
|
||||
| Hybrid 0.4/0.6, stemmed | 50.2% | **81.4%** | **88.2%** | 0.6394 | **91.4%** |
|
||||
|
||||
**Stemming is a trade, not a win, and the default stays off.** It reliably buys
|
||||
depth and costs the top rank: on BM25 alone, +2.8pp Hit@5 and +2.4pp Hit@10 for
|
||||
−1.8pp Hit@1, with MRR unchanged to four decimal places — the gains deeper down
|
||||
exactly offset the loss at rank 1. That is what conflation does: merging
|
||||
"train"/"training"/"trains" surfaces documents an exact-match query would never
|
||||
reach, and also lets a near-miss outrank the exact hit.
|
||||
|
||||
On the configuration that actually ships (hybrid 0.4/0.6) the trade is
|
||||
narrower still — Hit@5 identical, Hit@10 +0.4pp, Hit@1 −1.4pp, MRR −0.004 —
|
||||
because the vector stage already supplies much of the recall stemming would
|
||||
add. There is no case here for changing the default; `TokenFilter::Stemmed`
|
||||
is available via `HDF5Memory::set_token_filter` for callers who want Hit@5/@10
|
||||
over rank-1 precision.
|
||||
|
||||
### Weight sweep — full haystack, n=500
|
||||
|
||||
`0.7/0.3` was a documented default, never a searched one. Sweeping
|
||||
|
||||
Reference in New Issue
Block a user