Skip to content

feat: tokenized item search — all terms must match, quoted phrases supported#1542

Closed
fedorlitau wants to merge 4 commits into
sysadminsmedia:mainfrom
fedorlitau:feat/tokenized-search
Closed

feat: tokenized item search — all terms must match, quoted phrases supported#1542
fedorlitau wants to merge 4 commits into
sysadminsmedia:mainfrom
fedorlitau:feat/tokenized-search

Conversation

@fedorlitau

Copy link
Copy Markdown

What type of PR is this?

  • feature

What this PR does / why we need it:

Item search currently treats the whole query as one literal substring, so
blue chair only matches items containing that exact phrase. This PR tokenizes
the query and requires every token to match at least one of the six searched
fields (AND across tokens, OR across fields), with double-quoted phrases kept as
single tokens.

  • backend/pkgs/textutils/tokenize.go (new): TokenizeSearchQuery() splits on
    Unicode whitespace; double-quoted segments become single tokens (quotes
    stripped, inner whitespace preserved); an unbalanced quote degrades gracefully
    to a plain token. 13 table-driven unit tests in tokenize_test.go.
  • backend/internal/data/repo/repo_entities.go: QueryByGroup now builds its
    search predicate via a new entitySearchPredicate() helper — AND over
    per-token six-field ContainsFold ORs (previously a single six-field OR over
    the raw query). Extracted as a helper to stay under the gocyclo limit.
  • backend/internal/data/repo/repo_items_search_test.go: 8 DB-backed
    integration subtests (TestEntityRepository_TokenizedSearch).
  • docs/src/content/docs/en/user-guide/tips-tricks.mdx: new "Search Syntax"
    section documenting the behavior.

Design decisions: AND semantics across tokens; tokens + quoted phrases only (no
boolean operators in this iteration). The #<assetID> search shortcut is
intentionally untouched.

Which issue(s) this PR fixes:

Implements the tokenized-search part of #438 (originally requested in #397).

Special notes for your reviewer:

QueryByGroup sits exactly at the gocyclo limit, which is why the predicate
lives in a separate helper. No API surface change, so swagger/typescript-types
were not regenerated.

Testing

  • go test ./pkgs/textutils/... ./internal/data/repo/... — all pass (unit +
    DB-backed integration tests).
  • golangci-lint run ./... — no issues in changed files (one pre-existing
    staticcheck hit in app/api/main.go unrelated to this PR).
  • Full build (frontend embed + backend binary) and manual smoke run on a fresh
    SQLite DB.

fedorlitau and others added 4 commits June 10, 2026 15:38
Splits search input on whitespace into tokens, keeping double-quoted
segments as single tokens for exact-phrase matching. Groundwork for
tokenized item search (sysadminsmedia#397).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Search input is now split into tokens (with double-quoted phrases kept
intact); each token must match at least one searchable field, so
"item blue" finds "Item, long description, blue" regardless of word
order. Single-token searches behave exactly as before.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…cyclo limit

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Summary by CodeRabbit

  • New Features

    • Search now tokenizes queries, requiring all terms to match for more precise results
    • Support for quoted phrases to search for exact multi-word matches
  • Documentation

    • Added "Search Syntax" guide explaining multi-term searches and quoted phrase usage

Walkthrough

This PR implements tokenized multi-term search for the entity repository. Instead of matching a single search string across fields with OR logic, queries are now split into tokens (with quoted phrase support), and all tokens must match at least one searchable field. A new TokenizeSearchQuery utility handles parsing, and entity filtering logic is updated to enforce all-tokens-required semantics.

Changes

Tokenized Search for Entities

Layer / File(s) Summary
Tokenizer utility with quote and whitespace handling
backend/pkgs/textutils/tokenize.go, backend/pkgs/textutils/tokenize_test.go
TokenizeSearchQuery parses input by splitting on Unicode whitespace, preserving whitespace and content within double-quoted regions (quotes removed), and omitting empty tokens. Comprehensive test coverage includes balanced/unbalanced quotes, adjacent quoted phrases, empty quotes, punctuation preservation, and Unicode handling.
Entity search predicate with tokenized matching
backend/internal/data/repo/repo_entities.go
New entitySearchPredicate helper tokenizes search input and builds a predicate requiring all tokens to match at least one of name, description, serial, model, manufacturer, or notes fields. QueryByGroup conditionally applies this predicate when search is non-empty, replacing prior field-OR logic.
Entity repository tokenized search validation
backend/internal/data/repo/repo_items_search_test.go
useNamedEntity helper creates test entities with optional manufacturer and cleanup. TestEntityRepository_TokenizedSearch validates substring matching, all-tokens-required enforcement, order-independence, case-insensitivity, multi-field matching, quoted phrase contiguous matching, and no-match cases.
User guide search syntax documentation
docs/src/content/docs/en/user-guide/tips-tricks.mdx
New "Search Syntax" section documents how search terms narrow results across item fields, exact phrase matching via double quotes, and combining quoted and unquoted terms.

Security Recommendations

Input validation and quote handling:

  • The tokenizer correctly handles unbalanced quotes by consuming remaining input as part of the final token, but ensure this behavior is intentional and documented. Consider whether malformed input (e.g., many opening quotes) could cause performance issues or excessive memory use on very large strings.
  • Verify that the tokenizer's Unicode whitespace handling using unicode.IsSpace() is appropriate for all supported input locales and does not create unexpected token boundaries.

Query predicate construction:

  • The entitySearchPredicate builds nested OR/AND predicates. Ensure that the underlying predicate builder safely escapes or parameterizes field values to prevent SQL injection if the repository uses SQL query building.
  • Confirm that the tokenizer output is always passed through a trusted layer (the predicate builder) and is never directly interpolated into queries.

Performance considerations:

  • Tokenizing on every query is appropriate, but verify that the AND-of-ORs predicate logic does not create excessive database query complexity for large token counts or large field sets.

🎯 3 (Moderate) | ⏱️ ~25 minutes

Suggested Reviewers

  • tankerkiller125

🔍 Tokens now split your search so clear,
All must match for results to appear,
Quotes keep phrases tight and near,
Multi-term narrowing, far from mere,
Better finds, crystal clear! ✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the main change: implementing tokenized search where all terms must match and quoted phrases are supported.
Description check ✅ Passed The description is comprehensive, including all required sections: PR type (feature), detailed explanation of changes with file-by-file breakdown, linked issues (#438, #397), special notes, and testing details.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
✨ Simplify code
  • Create PR with simplified code

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot requested a review from tankerkiller125 June 10, 2026 16:15

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (4)
docs/src/content/docs/en/user-guide/tips-tricks.mdx (3)

25-28: ⚡ Quick win

Consider mentioning case-insensitivity explicitly.

The search is case-insensitive (using ContainsFold in the implementation), but this isn't stated in the documentation. Adding a brief note would help users understand that drill and DRILL are equivalent.

📝 Suggested addition
 The search bar splits your input into individual terms, and every term must match somewhere in the item (name,
 description, serial number, model number, manufacturer, or notes). Each additional term narrows the results: searching
-`drill cordless` only returns items that match both `drill` and `cordless`, even if the words appear in different
-fields or in a different order.
+`drill cordless` only returns items that match both `drill` and `cordless`, even if the words appear in different fields
+or in a different order. Searches are case-insensitive, so `drill`, `Drill`, and `DRILL` are all equivalent.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/src/content/docs/en/user-guide/tips-tricks.mdx` around lines 25 - 28,
The documentation text about how the search splits input lacks mention of
case-insensitivity; update the paragraph describing search behavior to add a
short sentence such as "Searches are case-insensitive (e.g., 'drill' and 'DRILL'
are equivalent)" and optionally note the implementation uses ContainsFold for
matching by referencing ContainsFold in the docs so readers know the matching is
case-insensitive; modify the existing paragraph around the sentence that starts
with "The search bar splits your input..." to include this brief note.

30-31: 💤 Low value

Consider documenting unbalanced quote behavior.

The implementation treats an unbalanced quote as part of the final token, but users might not expect this behavior. While this is an edge case, a brief note could prevent confusion.

📝 Optional addition
 To match an exact phrase, wrap it in double quotes: `"blue storage box"` only matches items containing that exact
-text. Quoted phrases and plain terms can be combined, e.g. `drill "18 V"`.
+text. Quoted phrases and plain terms can be combined, e.g. `drill "18 V"`. If a quote is not closed, it will be
+treated as part of the search term.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/src/content/docs/en/user-guide/tips-tricks.mdx` around lines 30 - 31,
Add a brief note to the existing tip about quoted phrases (the sentence using
`"blue storage box"` and the `drill "18 V"` example) explaining that unbalanced
quotes are treated as part of the final token by the search/tokenizer (i.e., a
missing closing quote will be included in the last token rather than causing an
error), so users understand the edge-case behavior; place this one-line
clarification immediately after the current examples.

23-32: ⚡ Quick win

Clarify how the # asset ID syntax interacts with tokenized search.

The file documents the #000-001 asset ID search syntax at line 41, but the new Search Syntax section doesn't mention whether this special syntax is affected by tokenization. Users might wonder if # gets tokenized or if quotes work with asset IDs.

📝 Suggested addition

Add a note after line 31:

 To match an exact phrase, wrap it in double quotes: `"blue storage box"` only matches items containing that exact
 text. Quoted phrases and plain terms can be combined, e.g. `drill "18 V"`.
+
+> [!NOTE]
+> The special `#` syntax for searching asset IDs (see [Managing Asset IDs](`#managing-asset-ids`)) works independently
+> of the tokenized search described above.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/src/content/docs/en/user-guide/tips-tricks.mdx` around lines 23 - 32,
The Search Syntax section needs a clarifying note about the special asset ID
syntax `#000-001`: explain that the `#` prefix and the full asset ID are treated
as a literal identifier and are not subject to tokenized term matching, so
searching `#000-001` will match that exact asset ID even if tokenization would
otherwise split terms; also state that quoted phrases are optional for asset IDs
(quotes may be used but are not required) and that combining an asset ID with
other terms still requires all terms to match. Add this brief note after the
Search Syntax paragraph referring to the `#000-001` example so users understand
how asset ID searches interact with tokenization and quotes.
backend/internal/data/repo/repo_items_search_test.go (1)

252-341: ⚡ Quick win

Well-structured test with comprehensive coverage.

The test cases thoroughly validate the tokenized search behavior including AND semantics, case-insensitivity, multi-field matching, and quoted phrase handling.

📋 Optional: Consider adding test cases for mixed quoted/unquoted tokens

The PR description mentions that quoted and unquoted terms can be combined, but there's no test case validating this. Adding test cases like:

{
    name:     "mixed quoted and unquoted tokens",
    search:   `blue "long description"`,
    contains: []EntityOut{blueItem, blueObject},
    excludes: []EntityOut{redItem, drill},
},
{
    name:     "unbalanced quote treated as plain token",
    search:   `Item "blue`,
    contains: []EntityOut{blueItem},
    excludes: []EntityOut{redItem, blueObject, drill},
},

This would provide additional confidence that the tokenizer and predicate logic work correctly together for complex real-world queries.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/internal/data/repo/repo_items_search_test.go` around lines 252 - 341,
Add two test cases to TestEntityRepository_TokenizedSearch to cover mixed
quoted/unquoted tokens and unbalanced quotes: update the testCases slice used by
searchIDs (which calls tRepos.Entities.QueryByGroup with EntityQuery) to include
a "mixed quoted and unquoted tokens" case like `blue "long description"`
expecting blueItem and blueObject to match, and an "unbalanced quote treated as
plain token" case like `Item "blue` expecting blueItem only; ensure the new
cases use the same contains/excludes pattern with EntityOut values so the
assertions after calling searchIDs validate tokenizer+predicate behavior for
these scenarios.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@backend/internal/data/repo/repo_items_search_test.go`:
- Around line 252-341: Add two test cases to
TestEntityRepository_TokenizedSearch to cover mixed quoted/unquoted tokens and
unbalanced quotes: update the testCases slice used by searchIDs (which calls
tRepos.Entities.QueryByGroup with EntityQuery) to include a "mixed quoted and
unquoted tokens" case like `blue "long description"` expecting blueItem and
blueObject to match, and an "unbalanced quote treated as plain token" case like
`Item "blue` expecting blueItem only; ensure the new cases use the same
contains/excludes pattern with EntityOut values so the assertions after calling
searchIDs validate tokenizer+predicate behavior for these scenarios.

In `@docs/src/content/docs/en/user-guide/tips-tricks.mdx`:
- Around line 25-28: The documentation text about how the search splits input
lacks mention of case-insensitivity; update the paragraph describing search
behavior to add a short sentence such as "Searches are case-insensitive (e.g.,
'drill' and 'DRILL' are equivalent)" and optionally note the implementation uses
ContainsFold for matching by referencing ContainsFold in the docs so readers
know the matching is case-insensitive; modify the existing paragraph around the
sentence that starts with "The search bar splits your input..." to include this
brief note.
- Around line 30-31: Add a brief note to the existing tip about quoted phrases
(the sentence using `"blue storage box"` and the `drill "18 V"` example)
explaining that unbalanced quotes are treated as part of the final token by the
search/tokenizer (i.e., a missing closing quote will be included in the last
token rather than causing an error), so users understand the edge-case behavior;
place this one-line clarification immediately after the current examples.
- Around line 23-32: The Search Syntax section needs a clarifying note about the
special asset ID syntax `#000-001`: explain that the `#` prefix and the full
asset ID are treated as a literal identifier and are not subject to tokenized
term matching, so searching `#000-001` will match that exact asset ID even if
tokenization would otherwise split terms; also state that quoted phrases are
optional for asset IDs (quotes may be used but are not required) and that
combining an asset ID with other terms still requires all terms to match. Add
this brief note after the Search Syntax paragraph referring to the `#000-001`
example so users understand how asset ID searches interact with tokenization and
quotes.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 137a6328-a6e3-48c7-afa7-7e89534fd668

📥 Commits

Reviewing files that changed from the base of the PR and between c152c49 and dd779cf.

📒 Files selected for processing (5)
  • backend/internal/data/repo/repo_entities.go
  • backend/internal/data/repo/repo_items_search_test.go
  • backend/pkgs/textutils/tokenize.go
  • backend/pkgs/textutils/tokenize_test.go
  • docs/src/content/docs/en/user-guide/tips-tricks.mdx

@tankerkiller125

Copy link
Copy Markdown
Contributor

Please note that this is being superseded by #1550 (which includes major search refactoring).

Please consider going over that PR and checking that it implements tokenizing correctly.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants