fix(recall): stabilize relevance threshold and CJK domain inference - #278
Open
m0Nst3r873 wants to merge 1 commit into
Open
fix(recall): stabilize relevance threshold and CJK domain inference#278m0Nst3r873 wants to merge 1 commit into
m0Nst3r873 wants to merge 1 commit into
Conversation
Two defects in recall's relevance gating, both measured rather than inferred, plus a regression baseline for a third that is deferred. 1. --check compared a hardcoded absolute score (4.0) against results drawn from two incompatible scales. Learnings scores are unbounded TF-IDF sums whose IDF numerator is the total entry count, so they drift with corpus size: adding 12 unrelated documents moved one entry from 14.0 to 30.6 (+119%). Codebase scores are log-compressed into [0,10] and do not drift. Verdicts are now taken per source -- codebase keeps the absolute threshold, learnings normalizes against the IDF baseline of a single-occurrence token. The relative cutoff alone regressed cold starts: at N<=5 a lone tag match scored 1.7-3.6 and would newly pass where 4.0 rejected it, so LEARNINGS_ABSOLUTE_FLOOR keeps the stricter behavior until the corpus is large enough (N>=7) for the ratio to exceed it. 2. inferQueryDomain matched only ASCII tag entries, so every Chinese query scored zero and fell back to 'neutral' -- the technical/ops/ support rows of DOMAIN_WEIGHT were unreachable for CJK users. The three vocabularies now carry Chinese entries. Only 2-char words are added: the tokenizer emits bigrams, making single chars ambiguous and 3+ char words unreachable by construction. Also adds a regression baseline for cross-domain IDF pollution. The invariant "adding entries of one domain must not change scores in another" does not hold today; fixing it requires partitioning IDF per domain, which bumps SEARCH_INDEX_VERSION and forces a full rebuild. That work is deferred, so the case is marked it.fails() -- CI stays green, and if the assertion ever starts passing, it.fails() reports it and the case can be promoted to a plain it(). Measurements show the pollution shifts absolute scores (shared tokens -33%, domain-exclusive tokens +139%) but preserves same-domain ordering, since IDF scales all candidates for a given query alike. That is why the threshold fix above is the higher-value half of the pair. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
7 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Two measured defects in recall's relevance gating, plus a regression baseline for a third that is deliberately deferred.
Both fixes came out of a real incident investigation where recall was of no help — the root causes turned out to be mechanical, not knowledge gaps.
1.
--checkcompared a hardcoded absolute score against two incompatible scalesRECALL_RELEVANCE_THRESHOLD = 4.0was applied to whichever result sorted first, butallResultsmixes:search())recall.ts~L313)min(10, log2(s+1)*2), bounded[0,10]Measured drift: adding 12 unrelated-domain documents moved one entry from 14.0 → 30.6 (+119%) without its own content changing.
Verdicts are now taken per source — codebase keeps the absolute threshold, learnings normalizes against
computeIdfBaseline()(the IDF of a single-occurrence token).Cold-start regression this surfaced
The relative cutoff alone made small corpora looser, which is the opposite of what the initial framing assumed:
That lands squarely on new users and new projects.
LEARNINGS_ABSOLUTE_FLOORkeeps the stricter pre-existing behavior until the corpus is large enough (N ≥ 7) for the ratio to exceed it on its own.2.
inferQueryDomainwas unreachable for Chinese queriesThe three tag vocabularies contained only ASCII entries, so any pure-Chinese query scored zero across all three and fell back to
'neutral'— making thetechnical/ops/supportrows ofDOMAIN_WEIGHTdead code for CJK users.Chinese entries added. Only 2-char words, deliberately:
超,时,重)数据库only ever yields数据/据库配置,失败,服务,文档) —文档in particular would hijack接口文档更新and故障复盘文档intosupportVerified after the change:
接口文档更新→technical,部署流程文档→ops,数据库连接池超时→technical.3. Deferred: cross-domain IDF pollution (regression baseline only)
The invariant "adding entries of one domain must not change scores in another" does not hold today.
dfandNare global (search-index.tsL549-554, L641), so any new domain reprices shared tokens.Fixing it means partitioning IDF per domain →
SEARCH_INDEX_VERSION6→7 → full index rebuild for every user. Deferred, and the case is markedit.fails()so CI stays green while the contract is retained — if the assertion ever starts passing,it.fails()reports it and the case can be promoted to a plainit().Measured effect, which is why it was deprioritized:
Absolute scores shift substantially, but same-domain ordering is preserved — IDF is a per-token multiplier that scales all candidates for a given query alike. The real damage was therefore concentrated in the absolute-threshold comparison, which is what §1 fixes.
Test plan
tsc --noEmitcleanrecall-relevance-threshold.test.ts— floor vs. relative governance either side of the N≈7 cross-over, legacy/no-df fallback,idfBaseline=0defense,computeIdfBaselineexcluding legacy indexes when picking max Nsearch-idf-domain-isolation.test.ts— global-df characterization, IDF drift measurement, CJK domain resolution (both directions: ratio 2.0 = 1.0/0.5 technical-query, 1.4286 = 1.0/0.7 ops-query), plus the deferredit.fails()invariant with fixture sanity split into its ownit()so it cannot pass vacuously--checkstdout format byte-identical (RELEVANT score=X.X\n) — asserted by existingrecall-check.test.tsSEARCH_INDEX_VERSIONandDOMAIN_WEIGHTvalues untouched; no index rebuild triggeredKnown limitations recorded, not fixed
allResults.sortstill compares the two scales directly — as the corpus grows, learnings hits increasingly crowd out codebase hits regardless of true relevance. Marked with aTODO(cross-scale)at the sort site; arguably worth more than the per-domain IDF work.computeIdfBaselinetakes the max entry count across scopes — a conservative approximation. Precise handling means carrying each index's baseline alongside its results; noted in the docstring.内存/资源/权限/证书/磁盘are classifiedops, so technically-framed memory questions get the ×0.7 cross-domain penalty. Known bias, left as-is.🤖 Generated with Claude Code