fix(search): sanitize user query before FTS5 MATCH — hyphens/operators crashed everyday searches - #2
Open
andrewschreiber wants to merge 1 commit into
Conversation
FTS5 parses the MATCH string as a query expression, so hyphens, slashes, colons, plus signs, parens, carets, stars and unbalanced quotes in everyday queries (power-on, on/off, C++ 17, wifi-6E) threw 'no such column' / 'fts5: syntax error'. Extract bare word/number tokens, quote each as a literal phrase (implicit AND preserved -> identical rows and bm25 scores for previously-working queries), return [] when no tokens remain. Vector arm still embeds the original query.
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.
Problem
bm25Top()insrc/search.jspasses the rawsearch_brainquery directly intoWHERE fts MATCH ?. FTS5 parses the MATCH string as a query expression, so-,+,/,:,",*,(,),^, and a bare token following a hyphenare operators or column filters. Ordinary natural-language queries therefore
throw SQLite errors that surface as MCP tool failures:
power-onno such column: onon/off togglefts5: syntax error near "/"C++ 17fts5: syntax error near "+"wifi-6Eno such column: 6E"unbalanced quoteunterminated stringSince MCP clients are LLMs emitting natural language, hyphenated/punctuated
queries are routine — this fires constantly in real use.
Reproduction
Or end-to-end: call the
search_braintool withquery: "power-on".Fix
Add
sanitizeFtsQuery(): extract bare word/number tokens (/[\p{L}\p{N}]+/gu),double-quote each as a literal FTS5 phrase, join with spaces (implicit AND).
bm25Topuses the sanitized string and returns[]when no tokens remain(all-punctuation query). The vector arm still embeds the original query, so
semantic recall is untouched; only the BM25 arm is sanitized.
This is a whitelist: the only characters that can reach MATCH are letters/numbers
inside quoted phrases — no operator, column filter,
NEAR, prefix*, orunbalanced quote can survive. Inside an FTS5 string the only meta-character is
", and extracted tokens cannot contain one.Behavior preservation
For every query that worked before, results are identical — same rows, same
bm25 scores: an unquoted bareword and a double-quoted single-token phrase
tokenize to the same phrase under unicode61, and the implicit-AND join is
unchanged. Verified empirically (rows + scores equal to 6 dp) for multi-word,
single-word, mixed-case, accented, and numeric queries.
Intentional changes: uppercase
AND/OR/NOTand other FTS5 query syntax arenow treated as literal words rather than operators (the tool never documented
FTS5 syntax, and its callers are LLMs sending natural language); underscore
compounds like
snake_casematch assnake AND case(broader recall) insteadof an adjacency phrase.
Alternatives considered: escaping quotes alone doesn't help (hyphens/colons are
structural outside strings); quoting the whole query as one phrase demands
token adjacency and destroys recall; OR-joining changes ranking and old AND
semantics. Per-token phrase quoting is the minimal behavior-preserving transform.
Tests
tests/fts-sanitize.test.js(node:test, no new deps):AND/OR/NOT/NEAR()) treated as literaltokens, never throwing
searchBrain(BM25 path) withpower-on,on/off hub,always-on multi-devicereturning the right chunk, and an all-punctuationquery returning
[]instead of throwingRisk
Low. Single call site; BM25 arm only; vector search and RRF fusion untouched;
identical output for previously-working queries (proven above); previously-
crashing queries now return results. No schema or API change;
sanitizeFtsQueryis exported for testability.