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:
@@ -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,
|
||||||
|
|||||||
@@ -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)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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},
|
||||||
|
|||||||
Reference in New Issue
Block a user