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,86 +11340,88 @@ 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 {
|
||||||
UserID pgtype.UUID `db:"user_id" json:"user_id"`
|
UserID pgtype.UUID `db:"user_id" json:"user_id"`
|
||||||
LibraryID pgtype.UUID `db:"library_id" json:"library_id"`
|
LibraryID pgtype.UUID `db:"library_id" json:"library_id"`
|
||||||
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"`
|
||||||
LanguageFilter interface{} `db:"language_filter" json:"language_filter"`
|
PublisherFilter interface{} `db:"publisher_filter" json:"publisher_filter"`
|
||||||
TagsFilter interface{} `db:"tags_filter" json:"tags_filter"`
|
LanguageFilter interface{} `db:"language_filter" json:"language_filter"`
|
||||||
YearMin interface{} `db:"year_min" json:"year_min"`
|
TagsFilter interface{} `db:"tags_filter" json:"tags_filter"`
|
||||||
YearMax interface{} `db:"year_max" json:"year_max"`
|
YearMin interface{} `db:"year_min" json:"year_min"`
|
||||||
HasCover pgtype.Bool `db:"has_cover" json:"has_cover"`
|
YearMax interface{} `db:"year_max" json:"year_max"`
|
||||||
SearchQuery interface{} `db:"search_query" json:"search_query"`
|
HasCover pgtype.Bool `db:"has_cover" json:"has_cover"`
|
||||||
IsExactSearch interface{} `db:"is_exact_search" json:"is_exact_search"`
|
SearchQuery interface{} `db:"search_query" json:"search_query"`
|
||||||
Sort interface{} `db:"sort" json:"sort"`
|
IsExactSearch interface{} `db:"is_exact_search" json:"is_exact_search"`
|
||||||
Offset pgtype.Int4 `db:"offset" json:"offset"`
|
Sort interface{} `db:"sort" json:"sort"`
|
||||||
Limit pgtype.Int4 `db:"limit" json:"limit"`
|
Offset pgtype.Int4 `db:"offset" json:"offset"`
|
||||||
|
Limit pgtype.Int4 `db:"limit" json:"limit"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type SearchMediaItemsUnifiedRow struct {
|
type SearchMediaItemsUnifiedRow struct {
|
||||||
@@ -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)
|
||||||
)
|
)
|
||||||
|
|||||||
+16
-14
@@ -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"))
|
||||||
@@ -2152,20 +2153,21 @@ func (mh *MediaHandler) SearchMediaItems(c *echo.Context) error {
|
|||||||
|
|
||||||
// Build search params
|
// Build search params
|
||||||
params := services.SearchParams{
|
params := services.SearchParams{
|
||||||
UserID: userID.ID,
|
UserID: userID.ID,
|
||||||
LibraryID: libUUID,
|
LibraryID: libUUID,
|
||||||
SearchQuery: query,
|
SearchQuery: query,
|
||||||
AuthorFilter: authorFilter,
|
AuthorFilter: authorFilter,
|
||||||
SeriesFilter: seriesFilter,
|
SeriesFilter: seriesFilter,
|
||||||
GenreFilter: genreFilter,
|
GenreFilter: genreFilter,
|
||||||
TagsFilter: tagsFilter,
|
PublisherFilter: publisherFilter,
|
||||||
LanguageFilter: languageFilter,
|
TagsFilter: tagsFilter,
|
||||||
YearMin: yearMin,
|
LanguageFilter: languageFilter,
|
||||||
YearMax: yearMax,
|
YearMin: yearMin,
|
||||||
HasCover: pgtype.Bool{Bool: hasCover, Valid: hasCoverValid},
|
YearMax: yearMax,
|
||||||
Sort: sortParam,
|
HasCover: pgtype.Bool{Bool: hasCover, Valid: hasCoverValid},
|
||||||
Limit: limit,
|
Sort: sortParam,
|
||||||
Offset: offset,
|
Limit: limit,
|
||||||
|
Offset: offset,
|
||||||
}
|
}
|
||||||
|
|
||||||
c.Logger().Info("SearchMediaItems called",
|
c.Logger().Info("SearchMediaItems called",
|
||||||
|
|||||||
+27
-25
@@ -22,20 +22,21 @@ func NewSearchService(db *database.Queries) *SearchService {
|
|||||||
|
|
||||||
// SearchParams contains parameters for unified search
|
// SearchParams contains parameters for unified search
|
||||||
type SearchParams struct {
|
type SearchParams struct {
|
||||||
UserID pgtype.UUID
|
UserID pgtype.UUID
|
||||||
LibraryID pgtype.UUID
|
LibraryID pgtype.UUID
|
||||||
AuthorFilter string
|
AuthorFilter string
|
||||||
SeriesFilter string
|
SeriesFilter string
|
||||||
GenreFilter string
|
GenreFilter string
|
||||||
TagsFilter string
|
PublisherFilter string
|
||||||
LanguageFilter string
|
TagsFilter string
|
||||||
YearMin int
|
LanguageFilter string
|
||||||
YearMax int
|
YearMin int
|
||||||
HasCover pgtype.Bool
|
YearMax int
|
||||||
SearchQuery string
|
HasCover pgtype.Bool
|
||||||
Sort string
|
SearchQuery string
|
||||||
Limit int
|
Sort string
|
||||||
Offset int
|
Limit int
|
||||||
|
Offset int
|
||||||
}
|
}
|
||||||
|
|
||||||
// parseSearchQuery detects quoted strings for exact match search
|
// parseSearchQuery detects quoted strings for exact match search
|
||||||
@@ -69,17 +70,18 @@ func (s *SearchService) SearchMediaItemsUnified(ctx context.Context, params Sear
|
|||||||
|
|
||||||
// Build database parameters - only set valid true if filter
|
// Build database parameters - only set valid true if filter
|
||||||
dbParams := database.SearchMediaItemsUnifiedParams{
|
dbParams := database.SearchMediaItemsUnifiedParams{
|
||||||
UserID: params.UserID,
|
UserID: params.UserID,
|
||||||
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},
|
||||||
TagsFilter: pgtype.Text{String: params.TagsFilter, Valid: true},
|
PublisherFilter: pgtype.Text{String: params.PublisherFilter, Valid: true},
|
||||||
LanguageFilter: pgtype.Text{String: params.LanguageFilter, Valid: true},
|
TagsFilter: pgtype.Text{String: params.TagsFilter, Valid: true},
|
||||||
YearMin: pgtype.Int4{Int32: int32(params.YearMin), Valid: true},
|
LanguageFilter: pgtype.Text{String: params.LanguageFilter, Valid: true},
|
||||||
YearMax: pgtype.Int4{Int32: int32(params.YearMax), Valid: true},
|
YearMin: pgtype.Int4{Int32: int32(params.YearMin), Valid: true},
|
||||||
HasCover: params.HasCover,
|
YearMax: pgtype.Int4{Int32: int32(params.YearMax), Valid: true},
|
||||||
SearchQuery: pgtype.Text{String: searchQuery, Valid: true},
|
HasCover: params.HasCover,
|
||||||
IsExactSearch: pgtype.Bool{Bool: isExact, Valid: true},
|
SearchQuery: pgtype.Text{String: searchQuery, Valid: true},
|
||||||
|
IsExactSearch: pgtype.Bool{Bool: isExact, Valid: true},
|
||||||
// SearchPattern: pgtype.Text{String: searchPattern, Valid: isExact},
|
// SearchPattern: pgtype.Text{String: searchPattern, Valid: isExact},
|
||||||
Sort: pgtype.Text{String: params.Sort, Valid: true},
|
Sort: pgtype.Text{String: params.Sort, Valid: true},
|
||||||
Limit: pgtype.Int4{Int32: int32(params.Limit), Valid: true},
|
Limit: pgtype.Int4{Int32: int32(params.Limit), Valid: true},
|
||||||
|
|||||||
Reference in New Issue
Block a user