perf(tmdb): send catalog title matches in small requests (#1891)

CatalogTitleMatchService.matchTitles sent a caller's whole batch as one DB_MATCH_TITLES request, and the DB worker answers one request at a time: a recommendations batch held every other read for 95 s on a 3.9M-title library. The service now deduplicates the titles, keeps the 200-title cap, and sends requests of five titles one after another, concatenating the answers in order. The answer stays all-or-nothing: a failed request returns no matches, a lock that starts withholding everything stops the batch, and a batch starts over when ParentalLockService.version() changes between requests (no matches after three changing attempts).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
4grayandClaude Fable 5.1 authored and GitHub committed 2026-10-11 14:04:31 +02:00
1 parent 43358e10f2
commit dd815676b4
7 files changed
+287 -20

No files matched your search

+27 -5
View File
@@ -208,7 +208,8 @@ component for detail→detail navigation.
`CrossPortalSimilarService` (`libs/services`) extends the rail across
portals: recommendations are matched against ALL imported Xtream
playlists with one batched `DB_MATCH_TITLES` request (Electron only,
playlists with one batched match (`DB_MATCH_TITLES`, sent in small
requests, see "Sending a batched match"; Electron only,
same two-tier + year rule). Stalker detail views — where the local
catalog is server-paginated and unmatchable — get their "Similar" rail
purely from these cross-portal matches (shared `VodDetailsComponent` for
@@ -353,7 +354,8 @@ Filmography has two scopes:
rest open the portal search prefilled with the title (`?q=`). Stalker
has no local catalog, so every title goes through the portal search.
- **All portals** (Electron only, toggle hidden in the PWA): one batched
`DB_MATCH_TITLES` worker request runs a trigram-FTS lookup per title
match (`DB_MATCH_TITLES` worker requests of five titles, see "Sending a
batched match") runs a trigram-FTS lookup per title
over ALL imported Xtream playlists
(`operations/title-match.operations.ts`), confirming candidates with
the same two-tier normalized-title matching the renderer uses
@@ -363,6 +365,24 @@ Filmography has two scopes:
This also works from Stalker actor pages — the one place the Stalker
catalog limitation is lifted.
### Sending a batched match
`CatalogTitleMatchService.matchTitles` deduplicates the titles, keeps the
first `TITLE_MATCH_TITLE_LIMIT` (200, the worker's own cap) and sends them
in `DB_MATCH_TITLES` requests of `TITLE_MATCH_REQUEST_SIZE` (5), one after
another, concatenating the answers in order. The DB worker answers one
request at a time, so a whole rail's batch in one request held every other
read behind it: on a 3.9M-title library one recommendations batch took
95 s cold and kept the dashboard's playback positions (and with them the
Continue Watching rail) waiting; now other reads wait for five titles at
most. The answer stays all-or-nothing: a failed
request, or a parental lock that starts withholding everything midway,
returns no matches, as one failed request did before. One request ran
under one parental-lock state, so a batch does too: when
`ParentalLockService.version()` changes between requests (a relock, an
unlock, a changed lock list) the batch starts over, and after
`TITLE_MATCH_LOCK_ATTEMPTS` (3) changing attempts it returns no matches.
### Resolving a batched match
Every `DB_MATCH_TITLES` consumer — the Trending rail, the "Because you
@@ -645,7 +665,8 @@ since shipped.)
toggle): `DashboardTrendingService`
(`libs/workspace/dashboard/data-access`) pulls the weekly TMDB trending
lists (cached one day per language) and runs ONE batched
`CatalogTitleMatchService.matchTitles` request against the imported
`CatalogTitleMatchService.matchTitles` call (worker requests of five
titles, see "Sending a batched match") against the imported
Xtream playlists — matched cards navigate straight to their detail view
and show the playlist name; unmatched cards open the global search
prefilled. Requires both the TMDB opt-in and the Electron DB worker
@@ -665,7 +686,8 @@ since shipped.)
interleaved round-robin, deduplicated by TMDB id, stripped of anything
already watched or favorited, and matched against
the imported libraries with ONE batched
`CatalogTitleMatchService.matchTitles` request. The watched/favorited
`CatalogTitleMatchService.matchTitles` call (worker requests of five
titles). The watched/favorited
exclusion index is built through the same lookup-attempt builder the
seeds use, so an activity row is indexed under more than its display
title: under the media type the detail view enriched with (a Stalker
@@ -719,7 +741,7 @@ since shipped.)
on its behalf would drop its recommendations for the session; a seed
that has no TMDB match never resolves either, so that user's rail
re-runs each visit, which is bounded work (cached enrichment misses
plus one batched worker call). Latched loads are keyed by the TMDB language
plus one batched title match). Latched loads are keyed by the TMDB language
(payloads are localized; the facade exposes `language()` for this), the
seed set, the watched/favorited exclusion set AND the imported-playlist
id set — watching something new re-seeds on the next dashboard visit,