diff --git a/src/databases/officer_db/src/queries/soulseek.ts b/src/databases/officer_db/src/queries/soulseek.ts index cc123151..e86e21a5 100644 --- a/src/databases/officer_db/src/queries/soulseek.ts +++ b/src/databases/officer_db/src/queries/soulseek.ts @@ -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),