fix(mcp): rank tool_search on whole words weighted by rarity - #323
Merged
Conversation
The word-level loop kept every query word longer than two characters and matched it with includes(), so three-letter English words matched inside real tokens. "the" hit snow_sp_theme_manage four ways at once — id, description, and the keywords "theme" and "theming" — for +31, and it was the top result for 24 of the eval's 101 queries, including "close the incident and fill in the resolution notes". "for" did the same to form/platform/performance, "out" to outage/layout, "not" to notification. Four changes, each measured on its own against the eval: - Whole tokens. Ids split on _, text on non-alphanumeric runs. A query term that is a strict prefix of an indexed token still scores, at 40%, which is what reaches "SyntaxErrors" from "syntax". - IDF, smoothed so it is never zero. The field weights are the original 10/8/5; rarity does the separating. The smoothing is not cosmetic: a term carried by every tool scores log(1)=0, and on a one-tool index that made every score 0 and returned nothing — which the list_changed tests build. - Coverage. The score is multiplied by 1 + (terms matched / terms asked), so breadth beats repeating one term. - A query-side synonym map at 70%, for words people use that the catalog does not: ticket, column, shift, mail. 101 queries, 445 tools: recall@1 0.267 -> 0.426, recall@5 0.505 -> 0.663, recall@20 0.663 -> 0.802, MRR 0.384 -> 0.536. On the 67 queries never inspected while tuning, recall@1 0.403 -> 0.582 and MRR 0.572 -> 0.719, so the gain is not fitted to the failures I read. Three queries regress out of the top 20. All three were found by accidental substring: "out" inside "outbound", "list" inside the list-view tools, "syntax" inside "SyntaxErrors". Against 17 of 34 previously unreachable queries now landing in the top 20. Measured and rejected: indexing use_cases (buys recall@20 on the hard subset, worse on every metric on the unseen one) and lifting the 10-word keyword cap (worse across the board). Both are written down in the source so the next attempt does not repeat them. Floors raised to sit under the re-measured tie band. The fallback fixture in tool-search-enablement.test.ts needed a tool sharing no word with its query — snow_query_incidents is a real hit for "snow_query_table" now, and the branch it guards stopped running. Fixes #298
This was referenced Aug 20, 2026
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.
Fixes #298
Stacked on #322 — the corpus it scores is the 445 tools that PR makes reachable, not 436. Review after it merges, or read only the second commit.
What was wrong
The word-level loop kept every query word longer than two characters and matched it with
includes(). Three-letter English words therefore matched inside real tokens.snow_sp_theme_managecollected +10 (id contains "the"), +5 (description), and +8 twice (keywords "theme", "theming") — +31 for the word the — and was the top hit for 24 of the eval's 101 queries.What replaces it
_, text on non-alphanumeric runs. A query term that is a strict prefix of an indexed token still scores, at 40% — that is what reaches "SyntaxErrors" from "syntax".log(1 + (N+1)/(df+1)). Field weights are the original 10/8/5; rarity does the separating.1 + (terms matched / terms asked). Breadth beats repeating one term.The smoothing on IDF is not cosmetic.
log((N+1)/(df+1))is 0 for a term every tool carries — correct as a ranking weight, fatal as a filter, becausescore > 0decides whether a tool is returned at all. On a one-tool index every term is carried by every tool, which is exactly what thelist_changedtests build: searching "query table" againstsnow_query_tablereturned nothing.Numbers
101 queries over 445 tools, from
tool-search-eval.test.ts:snow_sp_theme_manageis rank 1 for 1 query now instead of 24, and does not appear at all for "close the incident and fill in the resolution notes".The synonym map was written after reading the queries the old ranker missed, so the headline number is partly fitted. Split by whether I inspected a query while tuning:
The gain is larger on the queries I never looked at than on the ones I did.
Regressions, all three of them
out⊂outboundsyntax⊂SyntaxErrors, now a prefix match rather than a substring one, so it survives but lowerEach was found by an accidental substring. Against them: 17 of the 34 previously unreachable queries now land in the top 20.
Measured and rejected
use_cases(all 445 tools have them). Buys recall@20 0.802→0.832 on the full set, costs recall@1 and MRR — and on the unseen subset it is worse on every metric. That is the signature of a change fitted to the hard cases.Both are recorded in the source so the next attempt does not repeat them.
Other
tool-search-enablement.test.tsneeded a fixture tool sharing no word with its query. It heldsnow_query_incidentsagainst the query "snow_query_table" — no substring match, but "query" is a real token hit now, so the registry-fallback branch it guards had stopped running.no-module-stateallowlist entries for the derived token/IDF cache, which is rebuilt fromtoolIndexand cleared with it.mcp-tool-discoveryclaimed "table names mostly do not work". They work now, because a table name is words — the guide says so, and says which case still fails (sys_user, where the distinguishing word is one the catalog uses constantly).424 tests pass, typecheck clean.