feat(platform): let chat find entities by words in a question - #3123
Merged
Conversation
Every chat entity leg receives the user's whole message as its search term and compares it as one substring, so anything phrased as a question matches nothing. "do we have red running shoes" found no product; the board question that opened #2992 found no task. Adds a word clause alongside each leg's existing phrase clause: the meaningful words of the term, each matched at the START of a word. The phrase clause is untouched, so a typed name fragment keeps working. Closes #2992.
Israeltheminer
force-pushed
the
feat/chat-legs-word-search
branch
from
September 2, 2026 14:47
45e445f to
9e99db0
Compare
This was referenced Sep 2, 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.
Chat can now find a task, project, contact, product or knowledge entry from a question. Before this, every entity leg compared the user's whole message as one substring, so anything phrased as a question matched nothing.
Closes #2992. Supersedes #3106, which fixed three of these legs against the Convex tree that #3107 replaced.
Why
Each leg receives the user's message as its search term and passes it straight to
ILIKE '%<term>%'. Observed against a real Postgres, with one product and one task seeded:origin/maindo we have red running shoesRed Runningrecruitment ads Facebook ad account project taskswho is anna leeThe second row is the one that made this invisible: a typed name fragment works, so the legs look healthy until someone asks a question. The third is the query that opened #2992 — a board question that answered with nothing, which is why chat recommended Asana and Jira instead.
#2983 fixed this on the Convex entity search. The 0.5 legs are new SQL and were written with the phrase compare, so the fix did not carry across.
What changed
wordStartPatternsinbackend/lib/word-match.tsturns a term into Postgres word-start patterns. It importsqueryTokensfrom the 0.4 matcher rather than re-listing the en/de/fr stopword set, so the two cannot drift.Each leg gains
OR column ~* ANY(patterns)next to its existingILIKE. Fields follow the 0.4 strategy files: tasks on title and description, projects on name and description, contacts on name and email.Two rules make a question usable. Function words and one-character fragments are dropped, so a term with no signal yields no patterns and the leg adds no word clause. A token must match at the start of a word, so an OR over tokens does not turn
adinto a match foroverhead.The query is split on anything that is not a letter or digit, matching how the 0.4 matcher defines a word on the text side. Without that, a trailing question mark rides on the last token and
have?reads as neither a stopword nor a prefix of anything.Knowledge entries take an opt-in flag, because
listKnowledgeEntriesalso backs the entries page, which passes what a reader typed rather than a question.Risk
Word matching is added, never substituted. Anyone replacing a phrase clause with the word clause would drop the mid-word matches the pages rely on.
Tests
word-match.test.tscovers the token rules. Three mutations, each turning it red: dropping the punctuation split (2 failed), usingallmode instead ofany(5 failed), and dropping the\manchor (5 failed).integration-check.tsgains a leg check over real SQL, in a fixture org of its own so its rows do not move the org-wide task counts three later checks assert. It pins the table above plus two negatives: an unrelated question and an all-stopword question both return nothing, anddo we have any ebook tasksdoes not reachFacebookwhiledo we have any facebook tasksdoes.Scope
Ranking is unchanged. This decides whether a row matches, not how matches are ordered — the legs still order by
updated_at_ms.avezis not in the shared French stopword list, so it survives tokenization. A surplus token is harmless under an OR of word starts, and widening the list would change the 0.4 legs that share it.Gate:
typecheck,oxlint --type-aware,oxfmt --check,knipandlint:sastgreen; the new unit test 7/7.