Skip to content

[Due for Payment 2026-04-09][$250] Search - Multi-word merchants are not searchable using keywords #86235

Closed

Description

@nlemma

If you haven’t already, check out our contributing guidelines for onboarding and email contributors@expensify.com to request to join our Slack channel!


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

Action Performed:

  1. Go to https://staging.new.expensify.com
  2. Navigate to a conversation
  3. Submit a expense with the merchant set to “Coffee shop”
  4. Navigate to another conversation
  5. Click on the Search icon on the header of the conversation (should be top right)
  6. Type “merchant:coffee” and enter

Expected Result:

Expenses matching the entered keyword are displayed

Actual Result:

Multi-word merchants are not searchable using keywords

Workaround:

Unknown

Platforms:

  • Android: App
  • Android: mWeb Chrome
  • iOS: App
  • iOS: mWeb Safari
  • iOS: mWeb Chrome
  • Windows: Chrome
  • MacOS: Chrome / Safari

Screenshots/Videos

Bug7111787_1774361481786.Recording__1884.mp4

View all open jobs on GitHub

Issue OwnerCurrent Issue Owner: @mallenexpensify
Upwork Automation - Do Not Edit
  • Upwork Job URL: https://www.upwork.com/jobs/~022036469900322154796
  • Upwork Job ID: 2036469900322154796
  • Last Price Increase: 2026-03-24
  • Automatic offers:
    • wildan-m | Contributor | 110917736

Activity

  1. added
    BugSomething is broken. Auto assigns a BugZero manager.
    on Mar 24, 2026
  2. MelvinBot commented on Mar 24, 2026

    @MelvinBot
    Contributor

    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 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)

    1. 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.

    2. 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.

    3. 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.

  3. added
    ExternalAdded to denote the issue can be worked on by a contributor
    on Mar 24, 2026
  4. added
    Help WantedApply this label when an issue is open to proposals by contributors
    on Mar 24, 2026
  5. melvin-bot commented on Mar 24, 2026

    @melvin-bot

    Triggered auto assignment to Contributor-plus team member for initial proposal review - @ikevin127 (External)

  6. 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
  7. melvin-bot commented on Mar 24, 2026

    @melvin-bot
  8. wildan-m commented on Mar 24, 2026

    @wildan-m
    Contributor

    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 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:

    CONTAINS: 'contains',

    [CONST.SEARCH.SYNTAX_OPERATORS.CONTAINS]: '*:' as const,

    When search() builds the JSON query, it passes the AST filters through unchanged:

    const {flatFilters, limit, ...queryJSONWithoutFlatFilters} = queryJSON;
    const query = {
    ...queryJSONWithoutFlatFilters,
    searchKey,
    offset,
    filters: queryJSONWithoutFlatFilters.filters ?? null,
    shouldCalculateTotals,
    // Backend expects 'maximumResults' instead of 'limit'
    ...(limit !== undefined && {maximumResults: limit}),
    };
    const jsonQuery = 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

    https://github.com/Expensify/App/compare/main...wildan-m:App:wildan/86235-search-text-field-contains?expand=1

  9. jahir07 commented on Mar 24, 2026

    @jahir07

    Proposal

    Restating the problem:

    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:

    1. Quoted values should stay exact match — if someone types merchant:"Coffee shop", that's intentional and should not get the contains treatment.
    2. 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.
    3. 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
  10. ikevin127 commented on Mar 25, 2026

    @ikevin127
    Contributor

    🎉 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 😓

  11. MelvinBot commented on Mar 25, 2026

    @MelvinBot
    Contributor

    🤖 Draft PR created: #86299

    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.

  12. 28 remaining items

  13. melvin-bot commented on Apr 7, 2026

    @melvin-bot

    Triggered auto assignment to @mallenexpensify (Awaiting Payment)

  14. 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
  15. mallenexpensify commented on Apr 10, 2026

    @mallenexpensify
    Contributor

    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

  16. ikevin127 commented on Apr 10, 2026

    @ikevin127
    Contributor

    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

    1. Create an workspace expense with merchant name Coffee Shop Downtown and description Lunch with client.
    2. Click on the Search icon on the header of the conversation (should be top right).
    3. Enter merchant:coffee in the search bar and press enter.
    4. Verify the expense with merchant "Coffee Shop Downtown" appears in results.
    5. Enter description:lunch in the search bar and press enter.
    6. Verify expenses with partial description matches appear.
    7. Verify filter fields like amount, date, and currency still perform exact matching (unchanged).

    Do we agree 👍 or 👎.

    @mallenexpensify

    Zapier Logs Run ID: 00040eee-4db1-ae72-cace-2ff583c41f54
  17. trjExpensify commented on Apr 13, 2026

    @trjExpensify
    Contributor

    $250 approved for @ikevin127.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

Awaiting PaymentAuto-added when associated PR is deployed to productionBugSomething is broken. Auto assigns a BugZero manager.DailyKSv2ExternalAdded to denote the issue can be worked on by a contributor

Type

No type

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions