Repository navigation
fix(search): path: and name: filters find their matches at any limit - #3018
Merged
Merged
Conversation
colbymchenry
force-pushed
the
claude/fix-search-filters-before-limit
branch
from
October 29, 2026 18:42
5714a44 to
3aded9b
Compare
QueryBuilder.searchNodes applied `path:` and `name:` only to its final list, after every candidate source had cut its rows to the limit: full-text search to max(limit*5, 100) rows, the substring fallback to `limit`, the typo fallback to 50 names, a filter-only search to the first limit*5 names in name order, and the exact-name supplement to 20 rows. Whenever enough symbols outside the filters ranked first, nothing the filters asked for was left, so `codegraph query "authenticate path:product/"` found nothing at limits 1, 2 and 10, and found the match at 200. That broke the query parser's contract: filters narrow the set and the search ranks within it. Every source now applies the filters before its own limit and offset. Its statement is streamed and JavaScript decides each row, so matching keeps toLowerCase's Unicode folding and literal substrings (SQLite's lower() and LIKE fold ASCII only, and LIKE reads % and _ as wildcards). A page and its offset count only the rows that pass, and the read stops once the page is full. To spare JavaScript the rows that cannot pass, each statement also carries a SQL pre-filter that keeps every row the filters could pass: instr(lower(...)) for a printable ASCII value, which agrees exactly with JavaScript on printable ASCII text, while text with any other character passes through to JavaScript. The typo fallback looks names up until it has its results instead of giving up after 50 names, and skips names that `name:` rules out. A filter that no symbol's name, or no symbol's file, passes returns nothing at once, checked against the memoized name list and a memoized list of file paths. The old final filter stays as a safety net, now before the cut. A search without these filters runs the same SQL as before, except that a filter-only search now honours its offset (the same rows at offset 0, which every caller in the codebase passes). Validation: - Kit repro through `codegraph query --json`: on main all 18 filtered queries (6 queries at limits 1, 2 and 10) returned nothing; with the fix each returns its match. - 9 new tests in search-field-filters.test.ts; 7 fail on main's source. Mutations caught: each of the five candidate sources left unfiltered, the offset ignored, the non-ASCII pass-through removed or kept only for non-ASCII values, and the JavaScript check removed. - Latency A/B, main vs fix in one process on one index, 5 interleaved rounds of the median of 5 calls: vscode src/vs/base + src/vs/editor (56,406 nodes) and a 10x replica of it (564,060 nodes). The 45 unfiltered queries (explore's kinds at limit 16, plain limit 10) return identical ids and scores: 89.7 vs 88.4 ms and 435.7 vs 441.1 ms in total. On the replica the filtered queries now find their matches in 0.2-26 ms; the slowest are a substring fallback that finds nothing (156 ms, what an unfiltered miss already costs) and `kind:method path:...` (116 ms). A filter nothing passes answers in 0.2 ms instead of reading every row. Reimplements PR #2254 by @rudycelekli. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
colbymchenry
force-pushed
the
claude/fix-search-filters-before-limit
branch
from
October 29, 2026 19:08
3aded9b to
900f53d
Compare
inth3shadows
added a commit
to inth3shadows/codegraph
that referenced
this pull request
Oct 29, 2026
Brings 4955eac..787eafb (colbymchenry#3014, colbymchenry#3017, colbymchenry#3020, colbymchenry#3018, colbymchenry#3023). Clean auto-merge. README: drop colbymchenry#2871 (closed upstream; colbymchenry#2934 merged in its place, and name-matcher already matches upstream).
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
A search with a
path:orname:filter, incodegraph query,codegraph_searchor the library'ssearchNodes(), now finds the symbols it asks for at any limit, and an offset pages through those symbols alone.Reimplements #2254 by @rudycelekli, who found the bug.
Root cause
QueryBuilder.searchNodesappliedpath:andname:only to its final list, after every candidate source had already cut its rows to the limit:max(limit*5, 100)rows;limitrows;limit*5symbols in name order;Whenever enough symbols outside the filters ranked first, nothing the filters asked for was left.
codegraph query "authenticate path:product/"found nothing at limits 1, 2 and 10, and found the match at 200. That broke the query parser's contract (query-parser.ts): filters narrow the set, and the search ranks within it.Fix
toLowerCase's Unicode folding and literal substrings. SQLite'slower()and LIKE fold ASCII only, and LIKE would read%and_as wildcards. A page and its offset count only the rows that pass, and the read stops once the page is full.instr(lower(col), ?), which agrees exactly with JavaScript on printable ASCII text. Text holding any other character passes through to JavaScript, and that is the only text a value with another character can match.name:rules out.kind:function) now honours its offset. It returns the same rows at offset 0, which every caller in the codebase passes.Validation
Kit repro through
codegraph query --jsonon the PR's fixture: on main all 18 filtered queries (6 queries at limits 1, 2 and 10) returned nothing; with the fix each returns its match. The unfilteredauthenticatequery returns the same three symbols on both.9 new tests in
__tests__/search-field-filters.test.ts: limits 1, 2 and 10 through each source, offset paging, non-ASCII folding (including the Kelvin sign, whose lowercase is an ASCIIk), literal%and_, repeated filters, filters nothing passes, an unfiltered search, andcodegraph_searchat limit 1. 7 fail on main's source. Mutations caught: each of the five sources left unfiltered, the offset ignored, the non-ASCII pass-through removed or kept only for non-ASCII values, and the JavaScript check removed.Latency A/B, main vs fix in one process on one index, 5 interleaved rounds, each the median of 5 calls. The 45 unfiltered queries are every search term at explore's kinds and limit 16, and at limit 10, plus three multi-word and two kind-only searches. A whole vscode index needs about 10 GB of RAM, which this shared machine can't spare, so the large index is a 10× replica of the real one: every symbol copied under
copyN/paths, with full-text search indexing the copies.src/vs/base+src/vs/editor(56,406 nodes)Filtered queries on the replica (results in parentheses; main's are what was left after its final filter):
model path:src/vs/editor/contrib/getValue path:src/vs/base/common/model name:textgetValeu path:src/vs/editor/contrib/(typo fallback)odelDecor path:src/vs/editor/contrib/(substring fallback)kind:class path:src/vs/editor/contrib/kind:method path:src/vs/base/common/dispose name:widget(nothing matches both)editor path:nonexistent/path:nonexistent/The real CLI on the real vscode index:
codegraph query "dispose path:src/vs/editor/contrib/" --limit 3,"getValue path:src/vs/base/common/"and"kind:class name:widget path:src/vs/editor/browser/"return nothing on main. With the fix they returndisposemethods ineditor/contrib/,getValueinbase/common/, andCodeEditorWidget,DiffEditorWidgetandEmbeddedCodeEditorWidget.codegraph exploreprints byte-identical answers on main and with the fix for 5 queries, at the same speed (1.2–1.4 s per call, mostly CLI start-up).codegraph_explorenever sends these filters. It searches with words split from the query at non-alphanumeric characters, and looks symbols up by identifier-shaped tokens, which can't hold a single:or a/. Either way apath:orname:token never arrives intact.Rebased onto main da1f263, which brings in fix(rust): use bindings follow Rust's scopes, and module paths walk the crate's modules #3014 (Rust resolution) and fix(index): a repeat full index keeps the name words of files it didn't change #3017 (the name-word vocabulary fix, which also edits
src/db/queries.ts, in other functions). The rebase was clean and the diff is unchanged (same patch-id). The measurements above were taken on the first base, e7c9ffd. This PR's tests and fix(index): a repeat full index keeps the name words of files it didn't change #3017's pass together on the rebased code.Full suite on this exact commit (900f53d, on main da1f263), native kernel built: 785 files, 9,295 tests passed, 0 failed, 85 skipped (
--maxWorkers=2, no reruns).Left out
path:search that matches some files but few symbols walks the name index to fill its page: about 0.2–0.3 s on the 564k-node replica for one small directory. A substring test can't use an index. Reading matches in path order instead would make the common broad filter, which today stops after a few rows, sort every match.🤖 Generated with Claude Code