feat: add unified search queries with fuzzy matching
Add comprehensive search queries supporting both fuzzy and exact matching: 1. SearchMediaItemsUnified - Main search query with: - Fuzzy matching on author, series, genre, language filters - Fuzzy search on title, author, series, tags, contributors - Exact matching with quotes (is_exact_search flag) - Year range and boolean filters - Relevance-based ordering using word_similarity scores 2. Field-specific autocomplete queries: - SearchAuthorValues, SearchGenreValues, SearchSeriesValues, SearchLanguageValues - Each returns distinct values with counts and similarity scores - Threshold of 0.3 for word_similarity filter - Ordered by relevance (score DESC, count DESC) Note: Using 4 separate field value queries instead of 1 complex query due to sqlc v1.30.0 limitation with CASE expressions in GROUP BY clauses.
This commit is contained in:
@@ -458,6 +458,141 @@ WHERE COALESCE(lv.is_visible, true) = true
|
|||||||
mi.title ASC
|
mi.title ASC
|
||||||
LIMIT sqlc.narg('limit') OFFSET sqlc.narg('offset');
|
LIMIT sqlc.narg('limit') OFFSET sqlc.narg('offset');
|
||||||
|
|
||||||
|
-- name: SearchMediaItemsUnified :many
|
||||||
|
SELECT mi.*, l.name as library_name, lt.name as library_type_name
|
||||||
|
FROM media_items mi
|
||||||
|
JOIN libraries l ON mi.library_id = l.id
|
||||||
|
JOIN library_types lt ON l.library_type_id = lt.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 mi.library_id = sqlc.narg('library_id')
|
||||||
|
-- Fuzzy author filter
|
||||||
|
AND (sqlc.narg('author_filter') = '' OR word_similarity(sqlc.narg('author_filter'), COALESCE(mi.author, '')) > 0.3)
|
||||||
|
-- Fuzzy series filter
|
||||||
|
AND (sqlc.narg('series_filter') = '' OR word_similarity(sqlc.narg('series_filter'), COALESCE(mi.series, '')) > 0.3)
|
||||||
|
-- Fuzzy genre filter
|
||||||
|
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)
|
||||||
|
-- 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'))
|
||||||
|
-- Boolean (exact)
|
||||||
|
AND (sqlc.narg('has_cover') = false OR mi.cover_image_path IS NOT NULL)
|
||||||
|
-- Search query (fuzzy or exact based on quotes)
|
||||||
|
AND (
|
||||||
|
sqlc.narg('search_query') = '' OR
|
||||||
|
-- Fuzzy search (default)
|
||||||
|
sqlc.narg('is_exact_search') = false AND (
|
||||||
|
word_similarity(sqlc.narg('search_query'), mi.title) > 0.3 OR
|
||||||
|
word_similarity(sqlc.narg('search_query'), COALESCE(mi.author, '')) > 0.3 OR
|
||||||
|
word_similarity(sqlc.narg('search_query'), COALESCE(mi.series, '')) > 0.3 OR
|
||||||
|
EXISTS (
|
||||||
|
SELECT 1 FROM unnest(mi.tags_search) AS tag
|
||||||
|
WHERE word_similarity(sqlc.narg('search_query'), tag) > 0.3
|
||||||
|
LIMIT 1
|
||||||
|
) OR
|
||||||
|
EXISTS (
|
||||||
|
SELECT 1 FROM unnest(mi.contributors_search) AS contributor
|
||||||
|
WHERE word_similarity(sqlc.narg('search_query'), contributor) > 0.3
|
||||||
|
LIMIT 1
|
||||||
|
)
|
||||||
|
) OR
|
||||||
|
-- Exact search (with quotes)
|
||||||
|
sqlc.narg('is_exact_search') = true AND (
|
||||||
|
mi.title ILIKE sqlc.narg('search_pattern') OR
|
||||||
|
mi.author ILIKE sqlc.narg('search_pattern') OR
|
||||||
|
mi.series ILIKE sqlc.narg('search_pattern') OR
|
||||||
|
sqlc.narg('search_pattern') = ANY(mi.tags_search) OR
|
||||||
|
sqlc.narg('search_pattern') = ANY(mi.contributors_search)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
ORDER BY
|
||||||
|
CASE
|
||||||
|
WHEN sqlc.narg('search_query') != '' THEN
|
||||||
|
GREATEST(
|
||||||
|
CASE WHEN sqlc.narg('is_exact_search') = false THEN
|
||||||
|
word_similarity(sqlc.narg('search_query'), mi.title)
|
||||||
|
ELSE 0 END,
|
||||||
|
CASE WHEN sqlc.narg('is_exact_search') = false THEN
|
||||||
|
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, ''))
|
||||||
|
)
|
||||||
|
ELSE 0
|
||||||
|
END DESC,
|
||||||
|
mi.title ASC
|
||||||
|
LIMIT sqlc.narg('limit') OFFSET sqlc.narg('offset');
|
||||||
|
|
||||||
|
-- name: SearchAuthorValues :many
|
||||||
|
SELECT
|
||||||
|
mi.author as value,
|
||||||
|
COUNT(*) as count,
|
||||||
|
word_similarity(sqlc.narg('search_query'), COALESCE(mi.author, ''))::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 mi.library_id = sqlc.narg('library_id')
|
||||||
|
AND word_similarity(sqlc.narg('search_query'), COALESCE(mi.author, '')) > 0.3
|
||||||
|
AND mi.author IS NOT NULL
|
||||||
|
AND mi.author != ''
|
||||||
|
GROUP BY mi.author, word_similarity(sqlc.narg('search_query'), COALESCE(mi.author, ''))::float8
|
||||||
|
ORDER BY score DESC, count DESC
|
||||||
|
LIMIT sqlc.narg('limit') OFFSET sqlc.narg('offset');
|
||||||
|
|
||||||
|
-- name: SearchGenreValues :many
|
||||||
|
SELECT
|
||||||
|
mi.genre as value,
|
||||||
|
COUNT(*) as count,
|
||||||
|
word_similarity(sqlc.narg('search_query'), COALESCE(mi.genre, ''))::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 mi.library_id = sqlc.narg('library_id')
|
||||||
|
AND word_similarity(sqlc.narg('search_query'), COALESCE(mi.genre, '')) > 0.3
|
||||||
|
AND mi.genre IS NOT NULL
|
||||||
|
AND mi.genre != ''
|
||||||
|
GROUP BY mi.genre, word_similarity(sqlc.narg('search_query'), COALESCE(mi.genre, ''))::float8
|
||||||
|
ORDER BY score DESC, count DESC
|
||||||
|
LIMIT sqlc.narg('limit') OFFSET sqlc.narg('offset');
|
||||||
|
|
||||||
|
-- name: SearchSeriesValues :many
|
||||||
|
SELECT
|
||||||
|
mi.series as value,
|
||||||
|
COUNT(*) as count,
|
||||||
|
word_similarity(sqlc.narg('search_query'), COALESCE(mi.series, ''))::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 mi.library_id = sqlc.narg('library_id')
|
||||||
|
AND word_similarity(sqlc.narg('search_query'), COALESCE(mi.series, '')) > 0.3
|
||||||
|
AND mi.series IS NOT NULL
|
||||||
|
AND mi.series != ''
|
||||||
|
GROUP BY mi.series, word_similarity(sqlc.narg('search_query'), COALESCE(mi.series, ''))::float8
|
||||||
|
ORDER BY score DESC, count DESC
|
||||||
|
LIMIT sqlc.narg('limit') OFFSET sqlc.narg('offset');
|
||||||
|
|
||||||
|
-- name: SearchLanguageValues :many
|
||||||
|
SELECT
|
||||||
|
mi.language as value,
|
||||||
|
COUNT(*) as count,
|
||||||
|
word_similarity(sqlc.narg('search_query'), COALESCE(mi.language, ''))::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 mi.library_id = sqlc.narg('library_id')
|
||||||
|
AND word_similarity(sqlc.narg('search_query'), COALESCE(mi.language, '')) > 0.3
|
||||||
|
AND mi.language IS NOT NULL
|
||||||
|
AND mi.language != ''
|
||||||
|
GROUP BY mi.language, word_similarity(sqlc.narg('search_query'), COALESCE(mi.language, ''))::float8
|
||||||
|
ORDER BY score DESC, count DESC
|
||||||
|
LIMIT sqlc.narg('limit') OFFSET sqlc.narg('offset');
|
||||||
|
|
||||||
-- Media Notes queries
|
-- Media Notes queries
|
||||||
-- name: CreateMediaNote :one
|
-- name: CreateMediaNote :one
|
||||||
INSERT INTO media_notes (media_item_id, user_id, content, position)
|
INSERT INTO media_notes (media_item_id, user_id, content, position)
|
||||||
|
|||||||
Reference in New Issue
Block a user