Repository navigation
Use contains operator for text field search filters - #86598
chuckdries merged 10 commits into
Conversation
The search parser maps : to the eq (exact match) operator for all fields, which means merchant:coffee only finds expenses with the exact merchant "coffee" and not "Coffee shop". Text fields like merchant and description should use partial/substring matching. Add applyContainsOperatorToTextFields() which transforms the AST before sending to the backend, converting eq to contains for free-text fields. This enables partial matching while preserving the user-facing : syntax. Fixes Expensify#86235
…-text-field-contains
8625d87 to
4d232c1
Compare
…patibility" This reverts commit fb2ed75.
|
@ikevin127 Please copy/paste the Reviewer Checklist from here into a new comment on this PR and complete it. If you have the K2 extension, you can simply click: [this button] |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 58328d5bf1
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
Codecov Report✅ Changes either increased or maintained existing code coverage, great job!
|
Reviewer Checklist
Screenshots/VideosAndroid: HybridAppscreen-20260328-165331.mp4Android: mWeb Chromescreen-20260328-165455.mp4iOS: HybridAppSimulator.Screen.Recording.-.iPhone.17.Pro.Max.-.2026-03-28.at.16.35.43.moviOS: mWeb SafariSimulator.Screen.Recording.-.iPhone.17.Pro.Max.-.2026-03-28.at.16.42.36.movMacOS: Chrome / SafariScreen.Recording.2026-03-28.at.16.51.41.mov |
ikevin127
left a comment
There was a problem hiding this comment.
The implementation is solid, well-tested, and follows best practices - safe to merge.
Status: ✅ Approved
🔥 Edge Cases / Destructive Tests
- Unicode/Emoji in text fields - Test international characters in merchant/description ✅ (tested w/ Sanskrit)
- Very long text values - Test with 1000+ character merchant names
- Special characters - Merchants containing
:,-,$, etc ✅ - savedSearch vs regular search - Ensure saved searches preserve original operators ✅
- Concurrent rapid searches - Rapid typing with text filters should not corrupt state ✅
- Backend error recovery - Network failure should preserve query integrity ✅
chuckdries
left a comment
There was a problem hiding this comment.
I'm a big fan of how you've done the unit tests for this. Very clean and easy to understand!
|
🚧 @chuckdries has triggered a test Expensify/App build. You can view the workflow run here. |
|
🧪🧪 Use the links below to test this adhoc build on Android, iOS, and Web. Happy testing! 🧪🧪
|
|
✋ This PR was not deployed to staging yet because QA is ongoing. It will be automatically deployed to staging after the next production release. |
|
🚀 Deployed to staging by https://github.com/chuckdries in version: 9.3.51-0 🚀
Bundle Size Analysis (Sentry): |
|
🤖 Help Site Review for PR #86598 I reviewed the changes in this PR against the help site articles under Affected article: Yes, changes are required. The article should be updated to reflect the new partial matching behavior. Here are the specific recommended edits: 1. Key rules section — add partial matching noteIn the "Key rules for using search operators" section, add a new bullet after the quotes rule: - Add quotes for exact phrases: `description:"team lunch"`.
+- Text fields like `merchant:` and `description:` perform partial matching — `merchant:coffee` matches "Coffee Shop Downtown".
- Supports relative dates: `date:this-week`.2. Expense filters — clarify merchant matchingUpdate the -- `merchant:` – expense merchant name
+- `merchant:` – expense merchant name (partial match — `merchant:coffee` matches "Coffee Shop")3. Task filters — clarify description matchingUpdate the -- `description:` – task description
+- `description:` – task description (partial match)Other articles reviewed (no changes needed):
Note: I cannot create the PR directly because |
|
🚀 Deployed to production by https://github.com/jasperhuangg in version: 9.3.51-10 🚀
|
| * for free-text filter fields (merchant, description). This enables partial/substring | ||
| * matching on the backend for text searches while preserving the user-facing `:` syntax. | ||
| */ | ||
| function applyContainsOperatorToTextFields(node: ASTNode): ASTNode { |
There was a problem hiding this comment.
Coming from Expensify/Expensify#643664 bugZero checklist:
This PR caused a regression where exporting a specific merchant group (e.g. Amazon) also exported expenses from merchants whose name contains it (e.g. Amazon Marketplace).
applyContainsOperatorToTextFields rewrote every merchant eq to contains — correct for free-text search, but it also rewrote the machine-generated group filter built by buildSpecificGroupQuery, so the backend returned substring matches and the exported rows drifted from the selected group.
The current fix in #97046 addresses this by adding an exactMatchFilterKeys allow-list to serializeQueryJSONForBackend / applyContainsOperatorToTextFields, so generated merchant-group filters keep eq while normal merchant: searches still use partial matching.
Explanation of Change
When searching
merchant:coffee, the search parser maps:to theeq(exact match) operator for all fields uniformly. Text fields likemerchantanddescriptionneed thecontains(substring) operator so thatmerchant:coffeematches "Coffee shop", "My coffee", etc.The
search()function inSearch.tspassed the AST filters unchanged to the backend, so thecontainsoperator that already existed inCONST.SEARCH.SYNTAX_OPERATORSwas never used.Fix: added
applyContainsOperatorToTextFields()inSearchQueryUtils.tsthat recursively traverses the AST and replaceseqwithcontainsfor free-text filter fields (merchant,description). It is called insearch()before the query is sent to the backend. The transformation happens at the query construction layer — no parser grammar changes needed, existing parser tests are unaffected, and adding future text fields is a one-line Set addition.Fixed Issues
$ #86235
PROPOSAL: #86235 (comment)
Tests
Coffee Shop Downtownand descriptionLunch with clientmerchant:coffeein the search bardescription:lunchin the search baramount,date, andcurrencystill perform exact matching (unaffected)Offline tests
This change only affects API query construction — offline behavior is unchanged.
Coffee Shop Downtownand descriptionLunch with clientmerchant:coffeein the search bardescription:lunchin the search barQA Steps
Coffee Shop Downtownand descriptionLunch with clientmerchant:coffeein the search bardescription:lunchin the search barPR Author Checklist
### Fixed Issuessection aboveTestssectionOffline stepssectionQA stepssectiontoggleReportand notonIconClick)src/languages/*files and using the translation methodSTYLE.md) were followedAvatar, I verified the components usingAvatarare working as expected)StyleUtils.getBackgroundAndBorderStyle(theme.componentBG))npm run compress-svg)Avataris modified, I verified thatAvataris working as expected in all cases)Designlabel and/or tagged@Expensify/designso the design team can review the changes.ScrollViewcomponent to make it scrollable when more elements are added to the page.mainbranch was merged into this PR after a review, I tested again and verified the outcome was still expected according to theTeststeps.Screenshots/Videos
Android: Native
Kapture.2026-03-29.at.03.04.55.mp4
Android: mWeb Chrome
Kapture.2026-03-29.at.03.08.27.mp4
iOS: Native
Kapture.2026-03-29.at.02.12.16.mp4
iOS: mWeb Safari
Kapture.2026-03-29.at.02.48.33.mp4
MacOS: Chrome / Safari
Kapture.2026-03-29.at.02.06.09.mp4