feat: add fuzzy tags_filter to search query

- Add tags_filter parameter to SearchMediaItemsUnified
- Add EXISTS clause with word_similarity() for fuzzy tag matching
- Add tag similarity scoring to ORDER BY clause (GREATEST function)
- Add SearchTagsValues query for autocomplete with ::TEXT cast
- Keep genre_filter for backward compatibility
- Regenerate Go code with sqlc generate

This enables filtering books by tags (from Calibre) instead of genre,
which is always NULL for imported books. Uses fuzzy matching consistent
with author/series filters, with best matches sorted first.

Relates to IMPLEMENTATION_TAGS_FILTER.md Phase 1
This commit is contained in:
2026-03-25 20:38:08 -04:00
parent a643bd43b8
commit 00840c2fe1
3 changed files with 122 additions and 32 deletions
+24 -1
View File
@@ -435,6 +435,11 @@ WHERE COALESCE(lv.is_visible, true) = true
AND (sqlc.narg('genre_filter') = '' OR word_similarity(sqlc.narg('genre_filter'), COALESCE(mi.genre, '')) > 0.3)
-- Fuzzy language filter
AND (sqlc.narg('language_filter') = '' OR word_similarity(sqlc.narg('language_filter'), COALESCE(mi.language, '')) > 0.3)
-- Tags filter (NEW - fuzzy match against tags array)
AND (sqlc.narg('tags_filter') = '' OR EXISTS (
SELECT 1 FROM unnest(mi.tags_search) AS tag
WHERE word_similarity(sqlc.narg('tags_filter'), tag) > 0.3
))
-- Year range (exact)
AND (sqlc.narg('year_min') = 0 OR mi.copyright_year >= sqlc.narg('year_min'))
AND (sqlc.narg('year_max') = 0 OR mi.copyright_year <= sqlc.narg('year_max'))
@@ -481,7 +486,9 @@ ORDER BY
word_similarity(sqlc.narg('search_query'), COALESCE(mi.author, ''))
ELSE 0 END,
word_similarity(sqlc.narg('author_filter'), COALESCE(mi.author, '')),
word_similarity(sqlc.narg('genre_filter'), COALESCE(mi.genre, ''))
word_similarity(sqlc.narg('genre_filter'), COALESCE(mi.genre, '')),
(SELECT MAX(word_similarity(sqlc.narg('tags_filter'), tag))
FROM unnest(mi.tags_search) AS tag)
)
ELSE 0
END DESC,
@@ -556,6 +563,22 @@ GROUP BY mi.genre, word_similarity(sqlc.narg('search_query'), COALESCE(mi.genre,
ORDER BY score DESC, count DESC
LIMIT sqlc.narg('limit') OFFSET sqlc.narg('offset');
-- name: SearchTagsValues :many
SELECT
unnest(mi.tags_search)::TEXT as value,
COUNT(*) as count,
word_similarity(sqlc.narg('search_query'), unnest(mi.tags_search))::float8 as score
FROM media_items mi
JOIN libraries l ON mi.library_id = l.id
LEFT JOIN library_visibility lv ON l.id = lv.library_id AND lv.user_id = sqlc.narg('user_id')
WHERE COALESCE(lv.is_visible, true) = true
AND (sqlc.narg('library_id')::uuid IS NULL OR mi.library_id = sqlc.narg('library_id')::uuid)
AND unnest(mi.tags_search) IS NOT NULL
AND word_similarity(sqlc.narg('search_query'), unnest(mi.tags_search)) > 0.3
GROUP BY unnest(mi.tags_search), word_similarity(sqlc.narg('search_query'), unnest(mi.tags_search))::float8
ORDER BY word_similarity(sqlc.narg('search_query'), unnest(mi.tags_search))::float8 DESC, COUNT DESC
LIMIT sqlc.narg('limit') OFFSET sqlc.narg('offset');
-- name: SearchSeriesValues :many
SELECT
mi.series as value,