You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
[Due for Payment 2026-04-09][$250] Search - Multi-word merchants are not searchable using keywords #86235
Version Number: v9.3.43-0 Reproducible in staging?: Yes Reproducible in production?: Yes If this was caught during regression testing, add the test name, ID and link from BrowserStack:https://test-management.browserstack.com/projects/2219752/test-runs/TR-12779/folder/13176922/41237512/1276031929?sort=%2Bidentifier Email or phone of affected tester (no customers): n/a Issue reported by: Applause Internal Team Bug source: Regression TC Execution Device used: Mac 15.4.1 Safari/Chrome, iPhone 13 iOS 26.3.1 App Component: Search
Please re-state the problem that we are trying to solve in this issue.
When a user searches for expenses using a partial merchant name (e.g., merchant:coffee), expenses with multi-word merchant names containing that keyword (e.g., "Coffee shop") are not returned in the results.
What is the root cause of that problem?
The search query parser always emits the eq (exact equality) operator for the : syntax, even for free-text fields like merchant and description. When a user types merchant:coffee, the PEG parser at src/libs/SearchParser/baseRules.peggy:160 maps : to "eq", producing the AST {operator: 'eq', left: 'merchant', right: 'coffee'}. This is sent to the backend, which performs an exact match — so "coffee" does not match "Coffee shop".
Interestingly, a contains operator already exists in the codebase at src/CONST/index.ts:7633 and has a serialization mapping *: in src/libs/SearchQueryUtils.ts:58, but the PEG parser grammar never produces it — making it effectively dead code.
What changes do you think we should make in order to solve the problem?
For free-text fields like merchant and description, the standardFilter rule in src/libs/SearchParser/searchParser.peggy should emit contains instead of eq when using the : operator. This would make merchant:coffee produce {operator: 'contains', left: 'merchant', right: 'coffee'}, enabling partial/substring matching on the backend (which already supports the contains operator).
The change would be in the standardFilter rule: after determining the operator, check if the field is a text field (merchant or description) and the operator is eq, then change it to contains. The existing unit tests in SearchParserTest.ts would need to be updated to expect contains instead of eq for these fields.
Note: This assumes the backend already supports the contains operator for text fields — the operator constant and serialization mapping exist, suggesting backend support is already in place. If not, a backend change would also be needed.
What alternative solutions did you explore? (Optional)
Add *: syntax to the parser grammar — this would let users explicitly opt into partial matching (e.g., merchant*:coffee). However, this is not user-friendly and doesn't match user expectations that merchant:coffee should find partial matches.
Change the backend to treat eq as partial match for text fields — this would fix the issue without any frontend changes, but would change the semantics of eq for text fields across all consumers.
Use freeTextFilter (keyword search) for unquoted merchant values — this would be a larger refactor and would break the current filter semantics.
Next Steps for Contributor+ team: Reply with @MelvinBot implement this to create a draft PR, @MelvinBot <your feedback> to refine this analysis, or explain why you are rejecting Melvin's proposal.
changed the title [-]Search - Multi-word merchants are not searchable using keywords[/-][+][$250] Search - Multi-word merchants are not searchable using keywords[/+]on Mar 24, 2026
Please re-state the problem that we are trying to solve in this issue.
When searching merchant:coffee, expenses with multi-word merchants like "Coffee shop" are not found. The search only returns exact matches, so partial keyword searches on text fields don't work.
What is the root cause of that problem?
The search() function in Search.ts sends the parser's AST directly to the backend without adjusting the operator for free-text fields. The parser maps : to eq (exact match) for all fields uniformly, but text fields like merchant and description need substring matching via the contains operator — which already exists in the codebase but is never used:
// Backend expects 'maximumResults' instead of 'limit'
...(limit!==undefined&&{maximumResults: limit}),
};
constjsonQuery=JSON.stringify(query);
So merchant:coffee sends {operator: "eq", left: "merchant", right: "coffee"} to the backend, which performs an exact match and misses "Coffee shop".
What changes do you think we should make in order to solve the problem?
Add an applyContainsOperatorToTextFields() utility in SearchQueryUtils.ts that recursively traverses the AST and replaces eq with contains for free-text filter fields (merchant, description). Call this in search() before building the JSON query sent to the backend.
This approach transforms the operator at the query construction layer rather than in the PEG parser grammar, which means:
The parser stays clean — no grammar changes or parser regeneration needed
The user-facing search syntax preserves : (not *:)
Existing parser tests (73 cases) remain unchanged
Adding future text fields is a simple Set addition in TypeScript
When a user types merchant:coffee in the search bar, expenses with merchants like "Coffee shop" don't show up — only an expense with the exact merchant name "coffee" would match. So partial keyword search on text fields is basically broken right now.
Root cause:
The PEG parser maps : to eq (exact match) for every filter field without exception. The thing is, a contains operator already exists in the codebase — it's defined in CONST/index.ts and even has a serialization mapping (*:) in SearchQueryUtils.ts — but no rule in the parser grammar ever produces it. It's dead code. So the fix doesn't need to touch the parser at all.
What I'd change:
I'll add a small utility function applyContainsForTextFields() in SearchQueryUtils.ts that walks the query AST and swaps eq → contains for free-text fields (merchant, description). Then I'll call it inside search() in Search.ts before the query gets serialized and sent to the backend.
Something along these lines:
const TEXT_FILTER_FIELDS = new Set(['merchant', 'description']);
function applyContainsForTextFields(filters: ASTNode): ASTNode {
if (!filters) return filters;
if (filters.operator === 'eq' && TEXT_FILTER_FIELDS.has(filters.left)) {
if (!filters.isQuoted) {
return { ...filters, operator: CONST.SEARCH.SYNTAX_OPERATORS.CONTAINS };
}
}
return {
...filters,
left: applyContainsForTextFields(filters.left),
right: applyContainsForTextFields(filters.right),
};
}
Why not change the PEG grammar?
Touching the grammar means regenerating the parser, and it would break all 73 existing parser tests. The utility approach keeps the parser clean — it stays responsible for syntax only, while SearchQueryUtils handles the field-level semantics. Adding more text fields in the future is just one line in the Set.
Edge cases I'll make sure to handle:
Quoted values should stay exact match — if someone types merchant:"Coffee shop", that's intentional and should not get the contains treatment.
Search bar display — contains serializes to *: internally, but the user typed :. I'll make sure the display layer maps contains back to : for these fields so the search bar doesn't suddenly show merchant*:coffee after a search.
Both merchant and description — both are free-text fields and both have the same problem, so I'll fix them together.
Files I'll touch:
src/libs/SearchQueryUtils.ts — add the utility function + fix the display mapping
src/libs/actions/Search.ts — call the utility before building the JSON query
src/libs/SearchParser/__tests__/SearchParserTest.ts — add test cases covering partial match on merchant and description
@wildan-m@MelvinBot Please provide a draft test branch with your working solution so I can test whether it actually fixes the issue. I'm asking because I tried applying the solutions locally but they did not fix the issue on my side 😓
This branch (claude-searchMultiWordMerchantContains) implements the contains operator transformation for merchant and description text fields. The change is in two files:
src/libs/SearchQueryUtils.ts — adds applyContainsOperatorToTextFields() and applyContainsOperatorToRawFilters() utilities that recursively walk the AST/rawFilterList and swap eq → contains for text fields
src/libs/actions/Search.ts — applies both transformations to the filters and rawFilterList in the query payload before sending to the backend
The key difference from a simple AST-only transformation: this also transforms rawFilterList (which is sent alongside filters in the jsonQuery), in case the backend uses rawFilterList for query execution rather than (or in addition to) the filters AST.
Important caveat: If the backend doesn't actually support the contains operator for these fields, this frontend-only change won't fix the issue and a backend change would also be needed. Please test against staging to confirm.
Please mark it as "Ready for review" when it is ready for review.
changed the title [-][$250] Search - Multi-word merchants are not searchable using keywords[/-][+][Due for Payment 2026-04-09][$250] Search - Multi-word merchants are not searchable using keywords[/+]on Apr 7, 2026
[Contributor] The offending PR has been commented on, pointing out the bug it caused and why, so the author and reviewers can learn from the mistake.
Link to comment: N/A - Result of the original design
[Contributor] If the regression was CRITICAL (e.g. interrupts a core flow) A discussion in #expensify-open-source has been started about whether any other steps should be taken (e.g. updating the PR review checklist) in order to catch this type of bug sooner.
Link to discussion: N/A
[Contributor] If it was decided to create a regression test for the bug, please propose the regression test steps using the template below to ensure the same bug will not reach production again.
[BugZero Assignee] Create a GH issue for creating/updating the regression test once above steps have been agreed upon.
Activity
MelvinBot commentedon Mar 24, 2026
Proposal
Please re-state the problem that we are trying to solve in this issue.
When a user searches for expenses using a partial merchant name (e.g.,
merchant:coffee), expenses with multi-word merchant names containing that keyword (e.g., "Coffee shop") are not returned in the results.What is the root cause of that problem?
The search query parser always emits the
eq(exact equality) operator for the:syntax, even for free-text fields likemerchantanddescription. When a user typesmerchant:coffee, the PEG parser atsrc/libs/SearchParser/baseRules.peggy:160maps:to"eq", producing the AST{operator: 'eq', left: 'merchant', right: 'coffee'}. This is sent to the backend, which performs an exact match — so"coffee"does not match"Coffee shop".Interestingly, a
containsoperator already exists in the codebase atsrc/CONST/index.ts:7633and has a serialization mapping*:insrc/libs/SearchQueryUtils.ts:58, but the PEG parser grammar never produces it — making it effectively dead code.What changes do you think we should make in order to solve the problem?
For free-text fields like
merchantanddescription, thestandardFilterrule insrc/libs/SearchParser/searchParser.peggyshould emitcontainsinstead ofeqwhen using the:operator. This would makemerchant:coffeeproduce{operator: 'contains', left: 'merchant', right: 'coffee'}, enabling partial/substring matching on the backend (which already supports thecontainsoperator).The change would be in the
standardFilterrule: after determining the operator, check if the field is a text field (merchantordescription) and the operator iseq, then change it tocontains. The existing unit tests inSearchParserTest.tswould need to be updated to expectcontainsinstead ofeqfor these fields.Note: This assumes the backend already supports the
containsoperator for text fields — the operator constant and serialization mapping exist, suggesting backend support is already in place. If not, a backend change would also be needed.What alternative solutions did you explore? (Optional)
Add
*:syntax to the parser grammar — this would let users explicitly opt into partial matching (e.g.,merchant*:coffee). However, this is not user-friendly and doesn't match user expectations thatmerchant:coffeeshould find partial matches.Change the backend to treat
eqas partial match for text fields — this would fix the issue without any frontend changes, but would change the semantics ofeqfor text fields across all consumers.Use
freeTextFilter(keyword search) for unquoted merchant values — this would be a larger refactor and would break the current filter semantics.Next Steps for Contributor+ team: Reply with
@MelvinBot implement thisto create a draft PR,@MelvinBot <your feedback>to refine this analysis, or explain why you are rejecting Melvin's proposal.melvin-bot commentedon Mar 24, 2026
Triggered auto assignment to Contributor-plus team member for initial proposal review - @ikevin127 (
External)[-]Search - Multi-word merchants are not searchable using keywords[/-][+][$250] Search - Multi-word merchants are not searchable using keywords[/+]melvin-bot commentedon Mar 24, 2026
Job added to Upwork: https://www.upwork.com/jobs/~022036469900322154796
wildan-m commentedon Mar 24, 2026
Proposal
Please re-state the problem that we are trying to solve in this issue.
When searching
merchant:coffee, expenses with multi-word merchants like "Coffee shop" are not found. The search only returns exact matches, so partial keyword searches on text fields don't work.What is the root cause of that problem?
The
search()function inSearch.tssends the parser's AST directly to the backend without adjusting the operator for free-text fields. The parser maps:toeq(exact match) for all fields uniformly, but text fields likemerchantanddescriptionneed substring matching via thecontainsoperator — which already exists in the codebase but is never used:App/src/CONST/index.ts
Line 7635 in 8328b44
App/src/libs/SearchQueryUtils.ts
Line 58 in 8328b44
When
search()builds the JSON query, it passes the AST filters through unchanged:App/src/libs/actions/Search.ts
Lines 510 to 520 in 8328b44
So
merchant:coffeesends{operator: "eq", left: "merchant", right: "coffee"}to the backend, which performs an exact match and misses "Coffee shop".What changes do you think we should make in order to solve the problem?
Add an
applyContainsOperatorToTextFields()utility inSearchQueryUtils.tsthat recursively traverses the AST and replaceseqwithcontainsfor free-text filter fields (merchant,description). Call this insearch()before building the JSON query sent to the backend.This approach transforms the operator at the query construction layer rather than in the PEG parser grammar, which means:
:(not*:)https://github.com/Expensify/App/compare/main...wildan-m:App:wildan/86235-search-text-field-contains?expand=1
jahir07 commentedon Mar 24, 2026
Proposal
Restating the problem:
When a user types
merchant:coffeein the search bar, expenses with merchants like "Coffee shop" don't show up — only an expense with the exact merchant name "coffee" would match. So partial keyword search on text fields is basically broken right now.Root cause:
The PEG parser maps
:toeq(exact match) for every filter field without exception. The thing is, acontainsoperator already exists in the codebase — it's defined inCONST/index.tsand even has a serialization mapping (*:) inSearchQueryUtils.ts— but no rule in the parser grammar ever produces it. It's dead code. So the fix doesn't need to touch the parser at all.What I'd change:
I'll add a small utility function
applyContainsForTextFields()inSearchQueryUtils.tsthat walks the query AST and swapseq→containsfor free-text fields (merchant,description). Then I'll call it insidesearch()inSearch.tsbefore the query gets serialized and sent to the backend.Something along these lines:
Why not change the PEG grammar?
Touching the grammar means regenerating the parser, and it would break all 73 existing parser tests. The utility approach keeps the parser clean — it stays responsible for syntax only, while
SearchQueryUtilshandles the field-level semantics. Adding more text fields in the future is just one line in theSet.Edge cases I'll make sure to handle:
merchant:"Coffee shop", that's intentional and should not get thecontainstreatment.containsserializes to*:internally, but the user typed:. I'll make sure the display layer mapscontainsback to:for these fields so the search bar doesn't suddenly showmerchant*:coffeeafter a search.merchantanddescription— both are free-text fields and both have the same problem, so I'll fix them together.Files I'll touch:
src/libs/SearchQueryUtils.ts— add the utility function + fix the display mappingsrc/libs/actions/Search.ts— call the utility before building the JSON querysrc/libs/SearchParser/__tests__/SearchParserTest.ts— add test cases covering partial match on merchant and descriptionikevin127 commentedon Mar 25, 2026
🎉 Thanks everybody for proposals!
@wildan-m @MelvinBot Please provide a draft test branch with your working solution so I can test whether it actually fixes the issue. I'm asking because I tried applying the solutions locally but they did not fix the issue on my side 😓
MelvinBot commentedon Mar 25, 2026
🤖 Draft PR created: #86299
This branch (
claude-searchMultiWordMerchantContains) implements thecontainsoperator transformation formerchantanddescriptiontext fields. The change is in two files:src/libs/SearchQueryUtils.ts— addsapplyContainsOperatorToTextFields()andapplyContainsOperatorToRawFilters()utilities that recursively walk the AST/rawFilterList and swapeq→containsfor text fieldssrc/libs/actions/Search.ts— applies both transformations to thefiltersandrawFilterListin the query payload before sending to the backendThe key difference from a simple AST-only transformation: this also transforms
rawFilterList(which is sent alongsidefiltersin thejsonQuery), in case the backend usesrawFilterListfor query execution rather than (or in addition to) thefiltersAST.Important caveat: If the backend doesn't actually support the
containsoperator for these fields, this frontend-only change won't fix the issue and a backend change would also be needed. Please test against staging to confirm.Please mark it as "Ready for review" when it is ready for review.
28 remaining items
melvin-bot commentedon Apr 7, 2026
Triggered auto assignment to @mallenexpensify (
Awaiting Payment)[-][$250] Search - Multi-word merchants are not searchable using keywords[/-][+][Due for Payment 2026-04-09][$250] Search - Multi-word merchants are not searchable using keywords[/+]mallenexpensify commentedon Apr 10, 2026
Payment Summary
Contributor: @wildan-m paid $250 via Upwork
Contributor+: @ikevin127 due $250 via NewDot
@ikevin127 plz complete the BZ checklist and tag me in a post once you have. Thx
ikevin127 commentedon Apr 10, 2026
BugZero Checklist:
[Contributor] The offending PR has been commented on, pointing out the bug it caused and why, so the author and reviewers can learn from the mistake.
Link to comment: N/A - Result of the original design
[Contributor] If the regression was CRITICAL (e.g. interrupts a core flow) A discussion in #expensify-open-source has been started about whether any other steps should be taken (e.g. updating the PR review checklist) in order to catch this type of bug sooner.
Link to discussion: N/A
[Contributor] If it was decided to create a regression test for the bug, please propose the regression test steps using the template below to ensure the same bug will not reach production again.
[BugZero Assignee] Create a GH issue for creating/updating the regression test once above steps have been agreed upon.
Link to issue: https://github.com/Expensify/Expensify/issues/623705
Regression Test Proposal
Coffee Shop Downtownand descriptionLunch with client.merchant:coffeein the search bar and press enter.description:lunchin the search bar and press enter.amount,date, andcurrencystill perform exact matching (unchanged).Do we agree 👍 or 👎.
@mallenexpensify
Zapier Logs
Run ID: 00040eee-4db1-ae72-cace-2ff583c41f54trjExpensify commentedon Apr 13, 2026
$250 approved for @ikevin127.