Skip to content

feat(search): chip input and chip-based matching for advanced search - #1336

Open
ilyesbrh wants to merge 2 commits into
4gray:masterfrom
ilyesbrh:fix/tokenized-search
Open

feat(search): chip input and chip-based matching for advanced search#1336
ilyesbrh wants to merge 2 commits into
4gray:masterfrom
ilyesbrh:fix/tokenized-search

Conversation

@ilyesbrh

@ilyesbrh ilyesbrh commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

What changed

The global Advanced search bar becomes a chip input, and matching is now
chip-based:

  • Type a phrase and press Enter to commit it as a visible, removable chip.
    Short chips like FR are allowed; draft text does not search until committed.
  • Each chip is a joined unit — all of its words must be present in a title.
    A result matches if it satisfies any chip, ranked so results matching the
    most chips come first (single-chip matches still appear).
  • A plain one-chip query behaves exactly as before, so existing ?q= links and
    the tuned matcher (compound words #1161, diacritics) are unaffected.

Scope is intentionally the global Advanced search bar only — portal and playlist
search bars keep their plain text input. Chips serialize as newline-delimited
units over the existing string query, so the facade, ?q= param and IPC
contract stay string-typed (no new bridge methods).

Known limitation (called out for reviewers): within a chip, a short word (≤2
chars) anchors to the title start via the existing matcher — e.g. [fr bein]
matches FR beIN 1 but not BeIN Sports FR. Broadening short words to match
mid-title would require reworking candidate SQL and is left as a follow-up.

Why

Users want to search with several independent terms (e.g. FR + beIN +
1968) and see the closest matches first, instead of one query forcing every
word to appear in a single title.

Release note

  • Added a note under .changes/ (search-token-partial-match.md)
  • Not needed

Checks

  • Tests added or updated for the changed behavior
    • content-search.util.spec.ts (chip parsing + chip scoring)
    • content.operations.spec.ts (multi-chip OR ranking end-to-end)
    • workspace-shell-header.component.spec.ts (chip add/remove/dedupe/render)
  • pnpm run lint (electron-backend, workspace-shell-feature) and the
    affected pnpm nx test targets pass

@greptile-apps

greptile-apps Bot commented Aug 1, 2026

Copy link
Copy Markdown

Greptile Summary

This PR adds newline-serialized chips to the global advanced-search UI and updates backend candidate selection and ranking to use OR-across-chip matching.

  • Adds Material chip entry, removal, deduplication, and route-query synchronization.
  • Selects Xtream and M3U candidates per chip and ranks results by the number of chips matched.
  • Adds unit and operation-level coverage plus a user-facing release note.

Confidence Score: 4/5

The PR needs a fix before merging because M3U results can be ordered incorrectly when chips match different channel metadata fields.

M3U scoring computes chip counts independently for the channel name, TVG name, and group title, so the final score cannot represent the total chips satisfied across those searchable fields.

Files Needing Attention: apps/electron-backend/src/app/database/operations/content.operations.ts

Important Files Changed

Filename Overview
apps/electron-backend/src/app/database/operations/content-search.util.ts Adds chip parsing and count-dominant global-search scoring while preserving one-chip scoring behavior.
apps/electron-backend/src/app/database/operations/content.operations.ts Adds per-chip candidate selection and ranking, but M3U scoring fails to accumulate chips matched across separate channel metadata fields.
libs/workspace/shell/feature/src/lib/workspace-shell/components/workspace-shell-header/workspace-shell-header.component.ts Adds signal-based local chip state and emits complete chip lists on add or removal.
libs/workspace/shell/feature/src/lib/workspace-shell/services/workspace-shell-search.service.ts Restricts chip mode to global search and serializes committed chips through the existing string query state.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart LR
  U[Global search chip input] --> S[Newline-delimited query]
  S --> P[Parse committed chips]
  P --> X[Xtream per-chip candidate queries]
  P --> M[M3U OR payload prefilter]
  X --> R[Count matching chips]
  M --> F[Score M3U channel fields]
  F --> R
  R --> O[Sort and paginate global results]
Loading
Prompt To Fix All With AI
### Issue 1
apps/electron-backend/src/app/database/operations/content.operations.ts:630-636
**Field-local chip counts break ranking**

When an M3U channel satisfies chips across different searchable fields, such as `France` in `channel.name` and `1968` in `channel.group.title`, each field is scored independently and `Math.min` retains only one field's chip count. The channel is therefore ranked as a one-chip match and can appear below a result that satisfies the same number of chips within one field.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Reviews (1): Last reviewed commit: "feat(search): chip input for the advance..." | Re-trigger Greptile

Comment on lines 630 to 636
const scores = [
scoreSearchTextMatch(channel.name, searchTerm),
scoreSearchTextMatch(channel.tvg.name, searchTerm),
scoreSearchTextMatch(channel.group.title, searchTerm),
scoreGlobalSearchChips(channel.name, chips),
scoreGlobalSearchChips(channel.tvg.name, chips),
scoreGlobalSearchChips(channel.group.title, chips),
].filter((score): score is number => score !== null);

return scores.length > 0 ? Math.min(...scores) : null;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Field-local chip counts break ranking

When an M3U channel satisfies chips across different searchable fields, such as France in channel.name and 1968 in channel.group.title, each field is scored independently and Math.min retains only one field's chip count. The channel is therefore ranked as a one-chip match and can appear below a result that satisfies the same number of chips within one field.

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/electron-backend/src/app/database/operations/content.operations.ts
Line: 630-636

Comment:
**Field-local chip counts break ranking**

When an M3U channel satisfies chips across different searchable fields, such as `France` in `channel.name` and `1968` in `channel.group.title`, each field is scored independently and `Math.min` retains only one field's chip count. The channel is therefore ranked as a one-chip match and can appear below a result that satisfies the same number of chips within one field.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Advanced (global) search matches on committed search chips instead of the whole
query. Each chip is a joined unit (all its words required, via the existing
tuned matcher, so compound-word and diacritic handling is preserved); a result
matches if any chip matches, ranked by how many chips it satisfies. A plain
one-chip query keeps the exact prior behavior.

M3U channels score their chips across the searchable fields (name, TVG name,
group title) as one set, so a channel satisfying different chips in different
fields is ranked by the total chips matched, not per field.

Chips travel over the existing string query as newline-delimited units, so the
facade, ?q= param and IPC contract stay string-typed. Per-chip candidate
selection reuses the prefix/FTS/scan strategies, unioned per chip.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@ilyesbrh
ilyesbrh force-pushed the fix/tokenized-search branch from 099c475 to 24dfeec Compare August 1, 2026 22:25
@ilyesbrh

ilyesbrh commented Aug 1, 2026

Copy link
Copy Markdown
Contributor Author

Thanks @greptile-apps — the M3U ranking issue is valid and fixed.

scoreM3uChannel no longer scores each field independently and Math.mins them. scoreGlobalSearchChips now accepts the searchable fields (name, TVG name, group title) as one set and counts a chip as matched when it hits any field, so a channel satisfying different chips in different fields is ranked by the total distinct chips matched. Xtream (single title) behaviour is unchanged.

Added a regression test in content-search.util.spec.ts ("counts a chip as matched when it hits any of several fields"). Folded into the chip-based OR matching commit.

@greptile-apps

greptile-apps Bot commented Aug 1, 2026

Copy link
Copy Markdown

The fix looks correct. The implementation properly handles the multi-field case:

  • fields is built once from value (string or array)
  • Per chip, it iterates all fields and takes the best (min) score across them — so a chip matching TVG name when name doesn't match still counts
  • matchedChips counts distinct chips (not field×chip combinations), so cross-field satisfaction ranks correctly
  • The tie-breaker bestChipScore is also the best across fields, which is the right behavior

One minor note: when a chip matches multiple fields, you take Math.min(chipScore, fieldScore) which keeps the best match score. That's consistent with the existing scoreSearchTextMatch semantics (lower = better). The regression test you added should cover the cross-field case — does it also verify the ranking when chip A matches field 1 and chip B matches field 2 (i.e., both chips satisfied via different fields)?

Tip: You can customize Greptile's behavior for this repo with .greptile/rules.md and .greptile/config.json.

The global ("advanced") search bar becomes a chip input: type a phrase and
press Enter to commit it as a visible, removable chip. Each chip is a joined
search unit and results match any chip (ranked by chips matched). Short chips
like "FR" are allowed; draft text does not search until committed. Plain
portal/playlist search bars are unchanged.

Chips serialize to the existing string query as newline-delimited units, so the
facade, ?q= param and IPC contract stay string-typed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@ilyesbrh
ilyesbrh force-pushed the fix/tokenized-search branch from 24dfeec to b3250ea Compare August 1, 2026 22:46
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