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
+45 -40
View File
@@ -11285,46 +11285,48 @@ WHERE COALESCE(lv.is_visible, true) = true
AND ($4 = '' OR word_similarity($4, COALESCE(mi.series, '')) > 0.3) AND ($4 = '' OR word_similarity($4, COALESCE(mi.series, '')) > 0.3)
-- Fuzzy genre filter -- Fuzzy genre filter
AND ($5 = '' OR word_similarity($5, COALESCE(mi.genre, '')) > 0.3) 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 -- 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) -- 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 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 -- Year range (exact) - prioritize date_published, fallback to copyright_year
AND ( AND (
$8 = 0 OR $9 = 0 OR
EXTRACT(YEAR FROM mi.date_published) >= $8 OR EXTRACT(YEAR FROM mi.date_published) >= $9 OR
(mi.date_published IS NULL AND mi.copyright_year >= $8) (mi.date_published IS NULL AND mi.copyright_year >= $9)
) )
AND ( AND (
$9 = 0 OR $10 = 0 OR
EXTRACT(YEAR FROM mi.date_published) <= $9 OR EXTRACT(YEAR FROM mi.date_published) <= $10 OR
(mi.date_published IS NULL AND mi.copyright_year <= $9) (mi.date_published IS NULL AND mi.copyright_year <= $10)
) )
-- Boolean (exact) -- Boolean (exact)
AND ( AND (
$10::bool IS NULL OR -- Not specified = show all $11::bool IS NULL OR -- Not specified = show all
($10::bool IS TRUE AND mi.cover_image_path IS NOT NULL) OR ($11::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 FALSE AND mi.cover_image_path IS NULL)
) )
-- Search query (fuzzy or exact based on quotes) -- Search query (fuzzy or exact based on quotes)
AND ( AND (
$11 = '' OR $12 = '' OR
-- Fuzzy search (default) -- Fuzzy search (default)
$12 = false AND ( $13 = false AND (
word_similarity($11, mi.title) > 0.3 OR word_similarity($12, mi.title) > 0.3 OR
word_similarity($11, COALESCE(mi.author, '')) > 0.3 OR word_similarity($12, COALESCE(mi.author, '')) > 0.3 OR
word_similarity($11, COALESCE(mi.series, '')) > 0.3 OR word_similarity($12, COALESCE(mi.series, '')) > 0.3 OR
EXISTS ( EXISTS (
SELECT 1 FROM unnest(mi.tags_search) AS tag 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 LIMIT 1
) OR ) OR
EXISTS ( EXISTS (
SELECT 1 FROM unnest(mi.contributors_search) AS contributor 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 LIMIT 1
) )
) OR ) OR
@@ -11338,68 +11340,69 @@ WHERE COALESCE(lv.is_visible, true) = true
-- sqlc.narg('search_pattern') = ANY(mi.contributors_search) -- sqlc.narg('search_pattern') = ANY(mi.contributors_search)
-- ) -- )
-- Exact search (with quotes) - true exact match, not substring -- Exact search (with quotes) - true exact match, not substring
$12 = true AND ( $13 = true AND (
mi.title = $11 OR mi.title = $12 OR
COALESCE(mi.author, '') = $11 OR COALESCE(mi.author, '') = $12 OR
COALESCE(mi.series, '') = $11 OR COALESCE(mi.series, '') = $12 OR
$11 = ANY(mi.tags_search) OR $12 = ANY(mi.tags_search) OR
$11 = ANY(mi.contributors_search) $12 = ANY(mi.contributors_search)
) )
) )
ORDER BY ORDER BY
-- Primary sort: relevance score when searching -- Primary sort: relevance score when searching
CASE CASE
WHEN $11 != '' THEN WHEN $12 != '' THEN
GREATEST( GREATEST(
CASE WHEN $12 = false THEN CASE WHEN $13 = false THEN
word_similarity($11, mi.title) word_similarity($12, mi.title)
ELSE 0 END, ELSE 0 END,
CASE WHEN $12 = false THEN CASE WHEN $13 = false THEN
word_similarity($11, COALESCE(mi.author, '')) word_similarity($12, COALESCE(mi.author, ''))
ELSE 0 END, ELSE 0 END,
word_similarity($3, COALESCE(mi.author, '')), word_similarity($3, COALESCE(mi.author, '')),
word_similarity($5, COALESCE(mi.genre, '')), 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) FROM unnest(mi.tags_search) AS tag)
) )
ELSE 0 ELSE 0
END DESC, END DESC,
-- Secondary sort: user-specified sort parameter -- Secondary sort: user-specified sort parameter
CASE CASE
WHEN $13 = 'title ASC' THEN mi.title WHEN $14 = 'title ASC' THEN mi.title
ELSE '' ELSE ''
END ASC, END ASC,
CASE CASE
WHEN $13 = 'title DESC' THEN mi.title WHEN $14 = 'title DESC' THEN mi.title
ELSE '' ELSE ''
END DESC, END DESC,
CASE CASE
WHEN $13 = 'author ASC' THEN COALESCE(mi.author, '') WHEN $14 = 'author ASC' THEN COALESCE(mi.author, '')
ELSE '' ELSE ''
END ASC, END ASC,
CASE CASE
WHEN $13 = 'author DESC' THEN COALESCE(mi.author, '') WHEN $14 = 'author DESC' THEN COALESCE(mi.author, '')
ELSE '' ELSE ''
END DESC, END DESC,
CASE CASE
WHEN $13 = 'created_at ASC' THEN mi.created_at WHEN $14 = 'created_at ASC' THEN mi.created_at
ELSE '1970-01-01'::timestamp ELSE '1970-01-01'::timestamp
END ASC, END ASC,
CASE CASE
WHEN $13 = 'created_at DESC' THEN mi.created_at WHEN $14 = 'created_at DESC' THEN mi.created_at
ELSE '1970-01-01'::timestamp ELSE '1970-01-01'::timestamp
END DESC, END DESC,
CASE 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 '' ELSE ''
END ASC, END ASC,
CASE 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 '' ELSE ''
END DESC, END DESC,
-- Tertiary sort: title (default fallback) -- Tertiary sort: title (default fallback)
mi.title ASC mi.title ASC
LIMIT $15 OFFSET $14 LIMIT $16 OFFSET $15
` `
type SearchMediaItemsUnifiedParams struct { type SearchMediaItemsUnifiedParams struct {
@@ -11408,6 +11411,7 @@ type SearchMediaItemsUnifiedParams struct {
AuthorFilter interface{} `db:"author_filter" json:"author_filter"` AuthorFilter interface{} `db:"author_filter" json:"author_filter"`
SeriesFilter interface{} `db:"series_filter" json:"series_filter"` SeriesFilter interface{} `db:"series_filter" json:"series_filter"`
GenreFilter interface{} `db:"genre_filter" json:"genre_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"` LanguageFilter interface{} `db:"language_filter" json:"language_filter"`
TagsFilter interface{} `db:"tags_filter" json:"tags_filter"` TagsFilter interface{} `db:"tags_filter" json:"tags_filter"`
YearMin interface{} `db:"year_min" json:"year_min"` YearMin interface{} `db:"year_min" json:"year_min"`
@@ -11476,6 +11480,7 @@ func (q *Queries) SearchMediaItemsUnified(ctx context.Context, arg SearchMediaIt
arg.AuthorFilter, arg.AuthorFilter,
arg.SeriesFilter, arg.SeriesFilter,
arg.GenreFilter, arg.GenreFilter,
arg.PublisherFilter,
arg.LanguageFilter, arg.LanguageFilter,
arg.TagsFilter, arg.TagsFilter,
arg.YearMin, 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) AND (sqlc.narg('series_filter') = '' OR word_similarity(sqlc.narg('series_filter'), COALESCE(mi.series, '')) > 0.3)
-- Fuzzy genre filter -- Fuzzy genre filter
AND (sqlc.narg('genre_filter') = '' OR word_similarity(sqlc.narg('genre_filter'), COALESCE(mi.genre, '')) > 0.3) 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 -- Fuzzy language filter
AND (sqlc.narg('language_filter') = '' OR word_similarity(sqlc.narg('language_filter'), COALESCE(mi.language, '')) > 0.3) 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) -- Tags filter (NEW - fuzzy match against tags array)
@@ -702,6 +704,7 @@ ORDER BY
ELSE 0 END, ELSE 0 END,
word_similarity(sqlc.narg('author_filter'), COALESCE(mi.author, '')), 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, '')),
word_similarity(sqlc.narg('publisher_filter'), COALESCE(mi.publisher, '')),
(SELECT MAX(word_similarity(sqlc.narg('tags_filter'), tag)) (SELECT MAX(word_similarity(sqlc.narg('tags_filter'), tag))
FROM unnest(mi.tags_search) AS tag) FROM unnest(mi.tags_search) AS tag)
) )
+2
View File
@@ -2133,6 +2133,7 @@ func (mh *MediaHandler) SearchMediaItems(c *echo.Context) error {
authorFilter := c.QueryParam("author_filter") authorFilter := c.QueryParam("author_filter")
seriesFilter := c.QueryParam("series_filter") seriesFilter := c.QueryParam("series_filter")
genreFilter := c.QueryParam("genre_filter") genreFilter := c.QueryParam("genre_filter")
publisherFilter := c.QueryParam("publisher_filter")
tagsFilter := c.QueryParam("tags_filter") tagsFilter := c.QueryParam("tags_filter")
languageFilter := c.QueryParam("language_filter") languageFilter := c.QueryParam("language_filter")
yearMin, _ := strconv.Atoi(c.QueryParam("year_min")) yearMin, _ := strconv.Atoi(c.QueryParam("year_min"))
@@ -2158,6 +2159,7 @@ func (mh *MediaHandler) SearchMediaItems(c *echo.Context) error {
AuthorFilter: authorFilter, AuthorFilter: authorFilter,
SeriesFilter: seriesFilter, SeriesFilter: seriesFilter,
GenreFilter: genreFilter, GenreFilter: genreFilter,
PublisherFilter: publisherFilter,
TagsFilter: tagsFilter, TagsFilter: tagsFilter,
LanguageFilter: languageFilter, LanguageFilter: languageFilter,
YearMin: yearMin, YearMin: yearMin,
+2
View File
@@ -27,6 +27,7 @@ type SearchParams struct {
AuthorFilter string AuthorFilter string
SeriesFilter string SeriesFilter string
GenreFilter string GenreFilter string
PublisherFilter string
TagsFilter string TagsFilter string
LanguageFilter string LanguageFilter string
YearMin int YearMin int
@@ -73,6 +74,7 @@ func (s *SearchService) SearchMediaItemsUnified(ctx context.Context, params Sear
AuthorFilter: pgtype.Text{String: params.AuthorFilter, Valid: true}, AuthorFilter: pgtype.Text{String: params.AuthorFilter, Valid: true},
SeriesFilter: pgtype.Text{String: params.SeriesFilter, Valid: true}, SeriesFilter: pgtype.Text{String: params.SeriesFilter, Valid: true},
GenreFilter: pgtype.Text{String: params.GenreFilter, Valid: true}, GenreFilter: pgtype.Text{String: params.GenreFilter, Valid: true},
PublisherFilter: pgtype.Text{String: params.PublisherFilter, Valid: true},
TagsFilter: pgtype.Text{String: params.TagsFilter, Valid: true}, TagsFilter: pgtype.Text{String: params.TagsFilter, Valid: true},
LanguageFilter: pgtype.Text{String: params.LanguageFilter, Valid: true}, LanguageFilter: pgtype.Text{String: params.LanguageFilter, Valid: true},
YearMin: pgtype.Int4{Int32: int32(params.YearMin), Valid: true}, YearMin: pgtype.Int4{Int32: int32(params.YearMin), Valid: true},