The search candidate cut runs over the whole narrowing, and there is no fallback when the ranking matches nothing #88
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Two ways
/Items?SearchTerm=still answers a different page from upstream, both found while closing #86 and both deliberately left out of that branch. Neither is visible toportable.json.The candidate cut is taken over the wrong narrowing
Upstream splits a search in two.
SqlSearchProviderpicks candidates using only its own filters — non-placeholder, non-virtual, theCleanName/OriginalTitlematch, include/exclude types, media types, parent, and user access — scores them, orders by score then id, and takeslimit * 3(100 where there is no limit). Only then doesfolder.GetItemsapply everything else the request asked for, andTotalRecordCountis what survives that second pass.Ours applies the whole narrowing inside the ranked subquery, so every filter upstream applies after the cut, we apply before it:
years,genre_ids,is_favorite,is_played,is_resumable,is_folder,name_starts_with,series_id,parent_index_number, the location-type virtual choice, and the non-recursive parent.Reachable two ways.
Upstream ranks 150 rows and keeps the favourites among them — a short page, and a total below 150. Ours ranks 150 favourites — a full page, and a total of 150.
And with no extra filter at all, because
SqlSearchProvider.ApplyParentFilterisParentId == pid || Parents.Any(...)regardless ofRecursive, while ours only reaches descendants whenRecursive=true:already selects a different candidate set. On the anchor's
SearchTerm=aall 150 candidates survive the second pass, which is why the totals happen to agree at 150 there.Cost.
narrowing()emits oneFROM+WHEREthat every query form shares. Closing this means emitting two buckets — the provider's filters, and the rest — and joining a ranked subquery built from the first against a narrowing built from the second, withcount()changing in step: its total must count the second pass, not the cut. It touches every caller ofnarrowing(), so the by-name and count forms need re-checking.There is no fallback when the ranking matches nothing
When the provider returns zero rows, upstream leaves
SearchTermon theInternalItemsQueryand runs an entirely different search:CleanName.Containsplus anOriginalTitleLIKE(with a wildcard branch when the cleaned term holds one), ordered byMapSearchRelevanceOrder— four tiers ascending (0 exact, 1 term-as-first-word, 2 prefix, 3 rest), withOriginalTitlein the exact and prefix tiers, which is a different tier definition from the provider's 100/80/75/50 — with the request's own limit and an uncapped total.That path notably does not exclude virtual items:
comes back from upstream and not from us. Any library with missing or unaired episodes reaches it.
The same flavour is what
/UserItems/Resume?searchTerm=uses — it never goes throughSearchManagerat all, so it gets repository tiers, no cut, andDatePlayed DESCafter relevance, where we still apply provider tiers and the candidate cut.Cost. A second search shape in the query builder (its own ranking expression, no candidate cut, no forced virtual exclusion), plus a retry in the caller rather than the builder —
select()andcount()are pure generators with no way to observe an empty result, so the "provider found nothing, run the other query" decision has to live in the route or the repository wrapper. The resume route then selects the repository flavour outright instead of retrying.Both halves closed, measured against the anchor with a pair on each side of the change.
The cut now knows only the provider's filters.
narrowing()emits two clauses: the ranked candidates — the term, the types, the media types, the folder aimed at, and the libraries in reach, with what never aired always excluded — joined against a narrowing built from everything else the request asked for. The total counts what that second pass kept, not the cut.A ranking that picks nothing falls back to the query's own search. Different tiers (the whole name, the term as first word, the front of the name, the rest — counting up, with the released title in the first and third), no cut, an uncapped total, and what never aired selected as the request said rather than always excluded. The choice is made on whether the ranking had any candidate, not on whether the page came back empty: a cut the rest of the query narrowed away is still a search that found something, and it answers with nothing.
That is also the search
/UserItems/Resume?searchTerm=runs, which never goes near the ranking.portable.jsonis unchanged at 60 responses, 101 differences, 59 agreeing — the 101 are/Yearsand belong to #89.One thing found on the way: the integration fixture's unaired episode was inserted with no
CleanName, so no search could ever have matched it. The same vacuity #86 fixed for the fixture's main insert; fixed here too, which is what let the fallback be tested at all.