feat(search): publisher_filter for the media search endpoint

Adds publisher_filter to SearchMediaItems/SearchMediaItemsUnified
(fuzzy word_similarity against mi.publisher, mirroring genre_filter),
plumbs it through services.SearchParams and the search handler, and
extends the GREATEST relevance ranking to include publisher matches.
Serves the app's author/publisher/genre/tag click-through browses
(UX pass item 5 — publisher was the only facet without a server
filter).
This commit is contained in:
John O'Keefe
2026-09-27 19:55:58 -04:00
parent 6aea6a4e2c
commit 3d88f0b960
4 changed files with 106 additions and 94 deletions
+60 -55
View File
@@ -11285,46 +11285,48 @@ WHERE COALESCE(lv.is_visible, true) = true
AND ($4 = '' OR word_similarity($4, COALESCE(mi.series, '')) > 0.3)
-- Fuzzy genre filter
AND ($5 = '' OR word_similarity($5, COALESCE(mi.genre, '')) > 0.3)
-- Fuzzy publisher filter
AND ($6 = '' OR word_similarity($6, COALESCE(mi.publisher, '')) > 0.3)
-- Fuzzy language filter
AND ($6 = '' OR word_similarity($6, COALESCE(mi.language, '')) > 0.3)
AND ($7 = '' OR word_similarity($7, COALESCE(mi.language, '')) > 0.3)
-- Tags filter (NEW - fuzzy match against tags array)
AND ($7 = '' OR EXISTS (
AND ($8 = '' OR EXISTS (
SELECT 1 FROM unnest(mi.tags_search) AS tag
WHERE word_similarity($7, tag) > 0.3
WHERE word_similarity($8, tag) > 0.3
))
-- Year range (exact) - prioritize date_published, fallback to copyright_year
AND (
$8 = 0 OR
EXTRACT(YEAR FROM mi.date_published) >= $8 OR
(mi.date_published IS NULL AND mi.copyright_year >= $8)
$9 = 0 OR
EXTRACT(YEAR FROM mi.date_published) >= $9 OR
(mi.date_published IS NULL AND mi.copyright_year >= $9)
)
AND (
$9 = 0 OR
EXTRACT(YEAR FROM mi.date_published) <= $9 OR
(mi.date_published IS NULL AND mi.copyright_year <= $9)
$10 = 0 OR
EXTRACT(YEAR FROM mi.date_published) <= $10 OR
(mi.date_published IS NULL AND mi.copyright_year <= $10)
)
-- Boolean (exact)
AND (
$10::bool IS NULL OR -- Not specified = show all
($10::bool IS TRUE AND mi.cover_image_path IS NOT NULL) OR
($10::bool IS FALSE AND mi.cover_image_path IS NULL)
$11::bool IS NULL OR -- Not specified = show all
($11::bool IS TRUE AND mi.cover_image_path IS NOT NULL) OR
($11::bool IS FALSE AND mi.cover_image_path IS NULL)
)
-- Search query (fuzzy or exact based on quotes)
AND (
$11 = '' OR
$12 = '' OR
-- Fuzzy search (default)
$12 = false AND (
word_similarity($11, mi.title) > 0.3 OR
word_similarity($11, COALESCE(mi.author, '')) > 0.3 OR
word_similarity($11, COALESCE(mi.series, '')) > 0.3 OR
$13 = false AND (
word_similarity($12, mi.title) > 0.3 OR
word_similarity($12, COALESCE(mi.author, '')) > 0.3 OR
word_similarity($12, COALESCE(mi.series, '')) > 0.3 OR
EXISTS (
SELECT 1 FROM unnest(mi.tags_search) AS tag
WHERE word_similarity($11, tag) > 0.3
WHERE word_similarity($12, tag) > 0.3
LIMIT 1
) OR
EXISTS (
SELECT 1 FROM unnest(mi.contributors_search) AS contributor
WHERE word_similarity($11, contributor) > 0.3
WHERE word_similarity($12, contributor) > 0.3
LIMIT 1
)
) OR
@@ -11338,86 +11340,88 @@ WHERE COALESCE(lv.is_visible, true) = true
-- sqlc.narg('search_pattern') = ANY(mi.contributors_search)
-- )
-- Exact search (with quotes) - true exact match, not substring
$12 = true AND (
mi.title = $11 OR
COALESCE(mi.author, '') = $11 OR
COALESCE(mi.series, '') = $11 OR
$11 = ANY(mi.tags_search) OR
$11 = ANY(mi.contributors_search)
$13 = true AND (
mi.title = $12 OR
COALESCE(mi.author, '') = $12 OR
COALESCE(mi.series, '') = $12 OR
$12 = ANY(mi.tags_search) OR
$12 = ANY(mi.contributors_search)
)
)
ORDER BY
-- Primary sort: relevance score when searching
CASE
WHEN $11 != '' THEN
WHEN $12 != '' THEN
GREATEST(
CASE WHEN $12 = false THEN
word_similarity($11, mi.title)
CASE WHEN $13 = false THEN
word_similarity($12, mi.title)
ELSE 0 END,
CASE WHEN $12 = false THEN
word_similarity($11, COALESCE(mi.author, ''))
CASE WHEN $13 = false THEN
word_similarity($12, COALESCE(mi.author, ''))
ELSE 0 END,
word_similarity($3, COALESCE(mi.author, '')),
word_similarity($5, COALESCE(mi.genre, '')),
(SELECT MAX(word_similarity($7, tag))
word_similarity($6, COALESCE(mi.publisher, '')),
(SELECT MAX(word_similarity($8, tag))
FROM unnest(mi.tags_search) AS tag)
)
ELSE 0
END DESC,
-- Secondary sort: user-specified sort parameter
CASE
WHEN $13 = 'title ASC' THEN mi.title
WHEN $14 = 'title ASC' THEN mi.title
ELSE ''
END ASC,
CASE
WHEN $13 = 'title DESC' THEN mi.title
WHEN $14 = 'title DESC' THEN mi.title
ELSE ''
END DESC,
CASE
WHEN $13 = 'author ASC' THEN COALESCE(mi.author, '')
WHEN $14 = 'author ASC' THEN COALESCE(mi.author, '')
ELSE ''
END ASC,
CASE
WHEN $13 = 'author DESC' THEN COALESCE(mi.author, '')
WHEN $14 = 'author DESC' THEN COALESCE(mi.author, '')
ELSE ''
END DESC,
CASE
WHEN $13 = 'created_at ASC' THEN mi.created_at
WHEN $14 = 'created_at ASC' THEN mi.created_at
ELSE '1970-01-01'::timestamp
END ASC,
CASE
WHEN $13 = 'created_at DESC' THEN mi.created_at
WHEN $14 = 'created_at DESC' THEN mi.created_at
ELSE '1970-01-01'::timestamp
END DESC,
CASE
WHEN $13 = 'page_count ASC' THEN COALESCE(mi.page_count::text, '0')
WHEN $14 = 'page_count ASC' THEN COALESCE(mi.page_count::text, '0')
ELSE ''
END ASC,
CASE
WHEN $13 = 'page_count DESC' THEN COALESCE(mi.page_count::text, '0')
WHEN $14 = 'page_count DESC' THEN COALESCE(mi.page_count::text, '0')
ELSE ''
END DESC,
-- Tertiary sort: title (default fallback)
mi.title ASC
LIMIT $15 OFFSET $14
LIMIT $16 OFFSET $15
`
type SearchMediaItemsUnifiedParams struct {
UserID pgtype.UUID `db:"user_id" json:"user_id"`
LibraryID pgtype.UUID `db:"library_id" json:"library_id"`
AuthorFilter interface{} `db:"author_filter" json:"author_filter"`
SeriesFilter interface{} `db:"series_filter" json:"series_filter"`
GenreFilter interface{} `db:"genre_filter" json:"genre_filter"`
LanguageFilter interface{} `db:"language_filter" json:"language_filter"`
TagsFilter interface{} `db:"tags_filter" json:"tags_filter"`
YearMin interface{} `db:"year_min" json:"year_min"`
YearMax interface{} `db:"year_max" json:"year_max"`
HasCover pgtype.Bool `db:"has_cover" json:"has_cover"`
SearchQuery interface{} `db:"search_query" json:"search_query"`
IsExactSearch interface{} `db:"is_exact_search" json:"is_exact_search"`
Sort interface{} `db:"sort" json:"sort"`
Offset pgtype.Int4 `db:"offset" json:"offset"`
Limit pgtype.Int4 `db:"limit" json:"limit"`
UserID pgtype.UUID `db:"user_id" json:"user_id"`
LibraryID pgtype.UUID `db:"library_id" json:"library_id"`
AuthorFilter interface{} `db:"author_filter" json:"author_filter"`
SeriesFilter interface{} `db:"series_filter" json:"series_filter"`
GenreFilter interface{} `db:"genre_filter" json:"genre_filter"`
PublisherFilter interface{} `db:"publisher_filter" json:"publisher_filter"`
LanguageFilter interface{} `db:"language_filter" json:"language_filter"`
TagsFilter interface{} `db:"tags_filter" json:"tags_filter"`
YearMin interface{} `db:"year_min" json:"year_min"`
YearMax interface{} `db:"year_max" json:"year_max"`
HasCover pgtype.Bool `db:"has_cover" json:"has_cover"`
SearchQuery interface{} `db:"search_query" json:"search_query"`
IsExactSearch interface{} `db:"is_exact_search" json:"is_exact_search"`
Sort interface{} `db:"sort" json:"sort"`
Offset pgtype.Int4 `db:"offset" json:"offset"`
Limit pgtype.Int4 `db:"limit" json:"limit"`
}
type SearchMediaItemsUnifiedRow struct {
@@ -11476,6 +11480,7 @@ func (q *Queries) SearchMediaItemsUnified(ctx context.Context, arg SearchMediaIt
arg.AuthorFilter,
arg.SeriesFilter,
arg.GenreFilter,
arg.PublisherFilter,
arg.LanguageFilter,
arg.TagsFilter,
arg.YearMin,
+3
View File
@@ -628,6 +628,8 @@ WHERE COALESCE(lv.is_visible, true) = true
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 publisher filter
AND (sqlc.narg('publisher_filter') = '' OR word_similarity(sqlc.narg('publisher_filter'), COALESCE(mi.publisher, '')) > 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)
@@ -702,6 +704,7 @@ ORDER BY
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('publisher_filter'), COALESCE(mi.publisher, '')),
(SELECT MAX(word_similarity(sqlc.narg('tags_filter'), tag))
FROM unnest(mi.tags_search) AS tag)
)