soulseek: match share filter word by word

A folder's name is rarely something you can type in full — you remember the band and
one word of the album, not the year, the format tag or where the apostrophe went.
Whole-string matching made those two halves useless together: "pogues hell" matched
nothing, because no path has them adjacent. Each word is now its own substring test,
AND-ed and order-independent, so the two ends you do remember narrow the tree between them.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-30 02:11:09 +00:00
co-authored by Claude Opus 4.8
parent b7015b1ede
commit fc00d3861c
@@ -290,9 +290,15 @@ export async function searchSoulseekBrowseTree(params: BrowseTreeSearchParams):
const term = q.trim();
if (snapshotId === null || !term) return { nodes: [], matched: [], total: 0, truncated: false };
// Words are AND-ed, each matched anywhere in the path, so 'pogues hell' finds
// '…\The Pogues\… - Hell's Ditch …' — a folder you can name from two ends but never type in full.
// ILIKE with a leading wildcard can't use the btree index, so this was already a scan of the snapshot's
// ~18k rows — which is why folding the column here costs nothing on top.
const where = and(eq(soulseekBrowseDirs.snapshotId, snapshotId), sql`${foldedName} ilike ${likePattern(term)}`);
// ~18k rows — which is why folding the column, once per word, costs nothing on top.
const words = term.split(/\s+/).filter(Boolean);
const where = and(
eq(soulseekBrowseDirs.snapshotId, snapshotId),
...words.map((word) => sql`${foldedName} ilike ${likePattern(word)}`),
);
const [matches, [count]] = await Promise.all([
db.select(treeCols).from(soulseekBrowseDirs).where(where).orderBy(asc(soulseekBrowseDirs.name)).limit(limit),