search_text whole_word=true treats boolean/prefix FTS expressions as literal text and silently drops matches #224

Closed
opened 2026-09-08 10:33:18 +02:00 by buildagent · 2 comments
Member

With whole_word=true, supported FTS query expressions silently become literal strings in the post-filter. A boolean query returns zero despite matching source, and the response describes that total as exact.

Reproduction

Against this repository at a32a7519be, call search_text:

{"query":"(single OR writer)","whole_word":true,"project":"primary","path_glob":"crates/daemon/tests/rpc_e2e.rs"}

Actual: total: 0, one candidate examined, saturated: false, and semantics saying the total is exact.

Independent control:

rg -n -i -w 'single|writer' crates/daemon/tests/rpc_e2e.rs

This finds 20 matching lines in that file.

A minimal indexed fixture with // alpha in a.rs and // beta in b.rs also returns zero for (alpha OR beta) and for alph* when whole_word=true. Both queries have matching words.

Cause

whole_word_needle unquotes a single phrase but otherwise returns the raw query. Both search routes pass that string to contains_as_word, so parentheses, OR, and * are searched as source characters. FTS candidate selection and the post-filter answer different questions.

Suggested fix

Prefer evaluating whole-word matching over the parsed query semantics. Reuse the daemon's actual query interpretation so the two layers cannot disagree about whether input is syntax or an auto-quoted literal; avoid a second approximate FTS grammar.

If full expression support is deferred, explicitly reject unsupported combinations with a structured error and actionable hint. Returning an exact-looking zero is not an acceptable fallback. Continue supporting plain literals and single quoted phrases, including escaped quotes.

Acceptance tests

  • Exercise boolean and prefix queries through both default fan-out and pinned-project MCP routes.
  • For the two-file fixture, boolean OR returns both matching files, or the unsupported combination is explicitly rejected; prefix behavior is equally explicit.
  • Keep positive and negative literal/quoted-phrase controls, including TODO versus ToDouble.
  • Verify the result census and total describe the same predicate.
  • Restoring the raw-query post-filter must fail the new tests.

Validation

Confirmed on a freshly built 0.27.0 (a32a7519be), using MCP over stdio in disposable projects. Also dogfooded the connected 0.26.1 server against this repository. The reviewed Rust implementation is unchanged between those commits. Existing checks pass: 241 MCP binary unit tests and 8 daemon RPC integration tests; these cases are not covered by those passing tests.

Priority assessment: P2 / medium correctness. Found during code-index-vs-ripgrep self-review on 2026-09-08. Full review and reproduction output will be attached.

With `whole_word=true`, supported FTS query expressions silently become literal strings in the post-filter. A boolean query returns zero despite matching source, and the response describes that total as exact. ## Reproduction Against this repository at a32a7519beb4, call `search_text`: ```json {"query":"(single OR writer)","whole_word":true,"project":"primary","path_glob":"crates/daemon/tests/rpc_e2e.rs"} ``` Actual: `total: 0`, one candidate examined, `saturated: false`, and semantics saying the total is exact. Independent control: ```sh rg -n -i -w 'single|writer' crates/daemon/tests/rpc_e2e.rs ``` This finds **20 matching lines in that file**. A minimal indexed fixture with `// alpha` in `a.rs` and `// beta` in `b.rs` also returns zero for `(alpha OR beta)` and for `alph*` when `whole_word=true`. Both queries have matching words. ## Cause [`whole_word_needle`](https://git.h-dv.de/h-dv/code-index/src/commit/a32a7519beb49b22c0bbf628435cad582a576349/crates/mcp-server/src/server.rs#L16991) unquotes a single phrase but otherwise returns the raw query. Both search routes pass that string to `contains_as_word`, so parentheses, `OR`, and `*` are searched as source characters. FTS candidate selection and the post-filter answer different questions. ## Suggested fix Prefer evaluating whole-word matching over the parsed query semantics. Reuse the daemon's actual query interpretation so the two layers cannot disagree about whether input is syntax or an auto-quoted literal; avoid a second approximate FTS grammar. If full expression support is deferred, explicitly reject unsupported combinations with a structured error and actionable hint. Returning an exact-looking zero is not an acceptable fallback. Continue supporting plain literals and single quoted phrases, including escaped quotes. ## Acceptance tests - Exercise boolean and prefix queries through both default fan-out and pinned-project MCP routes. - For the two-file fixture, boolean OR returns both matching files, or the unsupported combination is explicitly rejected; prefix behavior is equally explicit. - Keep positive and negative literal/quoted-phrase controls, including `TODO` versus `ToDouble`. - Verify the result census and total describe the same predicate. - Restoring the raw-query post-filter must fail the new tests. ## Validation Confirmed on a freshly built **0.27.0 (a32a7519beb4)**, using MCP over stdio in disposable projects. Also dogfooded the connected 0.26.1 server against this repository. The reviewed Rust implementation is unchanged between those commits. Existing checks pass: **241 MCP binary unit tests and 8 daemon RPC integration tests**; these cases are not covered by those passing tests. Priority assessment: P2 / medium correctness. Found during code-index-vs-ripgrep self-review on 2026-09-08. Full review and reproduction output will be attached.
Author
Member

Shared evidence for #224, #225 and #226:

Extract the ZIP, build the reviewed checkout with cargo build --offline -p code-index-mcp -p code-index-cli, then run python3 /path/to/repro.py from that checkout. The script uses disposable fixture projects and an isolated plugin store; the generation change is fault injection only in a disposable database.

Shared evidence for #224, #225 and #226: - [Full self-review and comparison with traditional tools](https://git.h-dv.de/attachments/4241ecf2-c7fa-4098-997a-838b9f635f4f) - [Captured 0.27.0 reproduction responses and ripgrep controls](https://git.h-dv.de/attachments/50b202ee-1313-44f3-b6e7-dd121ed857e2) - [Runnable reproduction script with instructions](https://git.h-dv.de/attachments/6e671f52-f609-42c6-b00f-03d4da77a6eb) Extract the ZIP, build the reviewed checkout with `cargo build --offline -p code-index-mcp -p code-index-cli`, then run `python3 /path/to/repro.py` from that checkout. The script uses disposable fixture projects and an isolated plugin store; the generation change is fault injection only in a disposable database.
Author
Member

Fixed in 492c0d3 — "search_text(whole_word=true) grades the query the daemon actually ran" — on master, shipping in v0.27.0.

Boolean and prefix FTS expressions were matched as literal source characters, and the resulting total: 0 was then described as exact. One compiled WordMatcher now serves admission, census and line numbers, and an expression the post-filter cannot honour is refused with a reason instead of answered with a confident zero.

Closing on merge.

Fixed in `492c0d3` — *"`search_text(whole_word=true)` grades the query the daemon actually ran"* — on `master`, shipping in v0.27.0. Boolean and prefix FTS expressions were matched as literal source characters, and the resulting `total: 0` was then described as exact. One compiled `WordMatcher` now serves admission, census and line numbers, and an expression the post-filter cannot honour is **refused with a reason** instead of answered with a confident zero. Closing on merge.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
h-dv/code-index#224
No description provided.