feat: enhance search with date-prioritized year filtering and true exact matching
Improve media item search functionality with two key enhancements: 1. Date-prioritized year filtering: - Prioritize date_published over copyright_year for year range queries - Fall back to copyright_year when date_published is NULL - Extract year from date_published timestamp for comparison 2. True exact search matching: - Replace ILIKE pattern matching with exact equality for quoted queries - Use search_query directly instead of wildcard pattern for exact matches - Remove SearchPattern parameter and related wildcard logic - Add COALESCE handling for author/series NULL values in exact matches These changes make year filtering more accurate with published dates and provide genuine exact matching when users wrap queries in quotes. Refs internal/database/queries/queries.sql:475, internal/services/search.go:62
This commit is contained in:
@@ -7259,9 +7259,17 @@ WHERE COALESCE(lv.is_visible, true) = true
|
|||||||
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($7, tag) > 0.3
|
||||||
))
|
))
|
||||||
-- Year range (exact)
|
-- Year range (exact) - prioritize date_published, fallback to copyright_year
|
||||||
AND ($8 = 0 OR mi.copyright_year >= $8)
|
AND (
|
||||||
AND ($9 = 0 OR mi.copyright_year <= $9)
|
$8 = 0 OR
|
||||||
|
EXTRACT(YEAR FROM mi.date_published) >= $8 OR
|
||||||
|
(mi.date_published IS NULL AND mi.copyright_year >= $8)
|
||||||
|
)
|
||||||
|
AND (
|
||||||
|
$9 = 0 OR
|
||||||
|
EXTRACT(YEAR FROM mi.date_published) <= $9 OR
|
||||||
|
(mi.date_published IS NULL AND mi.copyright_year <= $9)
|
||||||
|
)
|
||||||
-- Boolean (exact)
|
-- Boolean (exact)
|
||||||
AND ($10 = false OR mi.cover_image_path IS NOT NULL)
|
AND ($10 = false OR mi.cover_image_path IS NOT NULL)
|
||||||
-- Search query (fuzzy or exact based on quotes)
|
-- Search query (fuzzy or exact based on quotes)
|
||||||
@@ -7284,12 +7292,21 @@ WHERE COALESCE(lv.is_visible, true) = true
|
|||||||
)
|
)
|
||||||
) OR
|
) OR
|
||||||
-- Exact search (with quotes)
|
-- Exact search (with quotes)
|
||||||
|
-- Keeping old pattern commented out in case we want wildcard exact back. See search.go line 62
|
||||||
|
-- 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)
|
||||||
|
-- )
|
||||||
|
-- Exact search (with quotes) - true exact match, not substring
|
||||||
$12 = true AND (
|
$12 = true AND (
|
||||||
mi.title ILIKE $13 OR
|
mi.title = $11 OR
|
||||||
mi.author ILIKE $13 OR
|
COALESCE(mi.author, '') = $11 OR
|
||||||
mi.series ILIKE $13 OR
|
COALESCE(mi.series, '') = $11 OR
|
||||||
$13 = ANY(mi.tags_search) OR
|
$11 = ANY(mi.tags_search) OR
|
||||||
$13 = ANY(mi.contributors_search)
|
$11 = ANY(mi.contributors_search)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
ORDER BY
|
ORDER BY
|
||||||
@@ -7313,40 +7330,40 @@ ORDER BY
|
|||||||
END DESC,
|
END DESC,
|
||||||
-- Secondary sort: user-specified sort parameter
|
-- Secondary sort: user-specified sort parameter
|
||||||
CASE
|
CASE
|
||||||
WHEN $14 = 'title ASC' THEN mi.title
|
WHEN $13 = 'title ASC' THEN mi.title
|
||||||
ELSE ''
|
ELSE ''
|
||||||
END ASC,
|
END ASC,
|
||||||
CASE
|
CASE
|
||||||
WHEN $14 = 'title DESC' THEN mi.title
|
WHEN $13 = 'title DESC' THEN mi.title
|
||||||
ELSE ''
|
ELSE ''
|
||||||
END DESC,
|
END DESC,
|
||||||
CASE
|
CASE
|
||||||
WHEN $14 = 'author ASC' THEN COALESCE(mi.author, '')
|
WHEN $13 = 'author ASC' THEN COALESCE(mi.author, '')
|
||||||
ELSE ''
|
ELSE ''
|
||||||
END ASC,
|
END ASC,
|
||||||
CASE
|
CASE
|
||||||
WHEN $14 = 'author DESC' THEN COALESCE(mi.author, '')
|
WHEN $13 = 'author DESC' THEN COALESCE(mi.author, '')
|
||||||
ELSE ''
|
ELSE ''
|
||||||
END DESC,
|
END DESC,
|
||||||
CASE
|
CASE
|
||||||
WHEN $14 = 'created_at ASC' THEN mi.created_at
|
WHEN $13 = 'created_at ASC' THEN mi.created_at
|
||||||
ELSE '1970-01-01'::timestamp
|
ELSE '1970-01-01'::timestamp
|
||||||
END ASC,
|
END ASC,
|
||||||
CASE
|
CASE
|
||||||
WHEN $14 = 'created_at DESC' THEN mi.created_at
|
WHEN $13 = 'created_at DESC' THEN mi.created_at
|
||||||
ELSE '1970-01-01'::timestamp
|
ELSE '1970-01-01'::timestamp
|
||||||
END DESC,
|
END DESC,
|
||||||
CASE
|
CASE
|
||||||
WHEN $14 = 'page_count ASC' THEN COALESCE(mi.page_count::text, '0')
|
WHEN $13 = 'page_count ASC' THEN COALESCE(mi.page_count::text, '0')
|
||||||
ELSE ''
|
ELSE ''
|
||||||
END ASC,
|
END ASC,
|
||||||
CASE
|
CASE
|
||||||
WHEN $14 = 'page_count DESC' THEN COALESCE(mi.page_count::text, '0')
|
WHEN $13 = '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 $16 OFFSET $15
|
LIMIT $15 OFFSET $14
|
||||||
`
|
`
|
||||||
|
|
||||||
type SearchMediaItemsUnifiedParams struct {
|
type SearchMediaItemsUnifiedParams struct {
|
||||||
@@ -7362,7 +7379,6 @@ type SearchMediaItemsUnifiedParams struct {
|
|||||||
HasCover interface{} `db:"has_cover" json:"has_cover"`
|
HasCover interface{} `db:"has_cover" json:"has_cover"`
|
||||||
SearchQuery interface{} `db:"search_query" json:"search_query"`
|
SearchQuery interface{} `db:"search_query" json:"search_query"`
|
||||||
IsExactSearch interface{} `db:"is_exact_search" json:"is_exact_search"`
|
IsExactSearch interface{} `db:"is_exact_search" json:"is_exact_search"`
|
||||||
SearchPattern pgtype.Text `db:"search_pattern" json:"search_pattern"`
|
|
||||||
Sort interface{} `db:"sort" json:"sort"`
|
Sort interface{} `db:"sort" json:"sort"`
|
||||||
Offset pgtype.Int4 `db:"offset" json:"offset"`
|
Offset pgtype.Int4 `db:"offset" json:"offset"`
|
||||||
Limit pgtype.Int4 `db:"limit" json:"limit"`
|
Limit pgtype.Int4 `db:"limit" json:"limit"`
|
||||||
@@ -7431,7 +7447,6 @@ func (q *Queries) SearchMediaItemsUnified(ctx context.Context, arg SearchMediaIt
|
|||||||
arg.HasCover,
|
arg.HasCover,
|
||||||
arg.SearchQuery,
|
arg.SearchQuery,
|
||||||
arg.IsExactSearch,
|
arg.IsExactSearch,
|
||||||
arg.SearchPattern,
|
|
||||||
arg.Sort,
|
arg.Sort,
|
||||||
arg.Offset,
|
arg.Offset,
|
||||||
arg.Limit,
|
arg.Limit,
|
||||||
|
|||||||
@@ -440,9 +440,17 @@ WHERE COALESCE(lv.is_visible, true) = true
|
|||||||
SELECT 1 FROM unnest(mi.tags_search) AS tag
|
SELECT 1 FROM unnest(mi.tags_search) AS tag
|
||||||
WHERE word_similarity(sqlc.narg('tags_filter'), tag) > 0.3
|
WHERE word_similarity(sqlc.narg('tags_filter'), tag) > 0.3
|
||||||
))
|
))
|
||||||
-- Year range (exact)
|
-- Year range (exact) - prioritize date_published, fallback to copyright_year
|
||||||
AND (sqlc.narg('year_min') = 0 OR mi.copyright_year >= sqlc.narg('year_min'))
|
AND (
|
||||||
AND (sqlc.narg('year_max') = 0 OR mi.copyright_year <= sqlc.narg('year_max'))
|
sqlc.narg('year_min') = 0 OR
|
||||||
|
EXTRACT(YEAR FROM mi.date_published) >= sqlc.narg('year_min') OR
|
||||||
|
(mi.date_published IS NULL AND mi.copyright_year >= sqlc.narg('year_min'))
|
||||||
|
)
|
||||||
|
AND (
|
||||||
|
sqlc.narg('year_max') = 0 OR
|
||||||
|
EXTRACT(YEAR FROM mi.date_published) <= sqlc.narg('year_max') OR
|
||||||
|
(mi.date_published IS NULL AND mi.copyright_year <= sqlc.narg('year_max'))
|
||||||
|
)
|
||||||
-- Boolean (exact)
|
-- Boolean (exact)
|
||||||
AND (sqlc.narg('has_cover') = false OR mi.cover_image_path IS NOT NULL)
|
AND (sqlc.narg('has_cover') = false OR mi.cover_image_path IS NOT NULL)
|
||||||
-- Search query (fuzzy or exact based on quotes)
|
-- Search query (fuzzy or exact based on quotes)
|
||||||
@@ -465,12 +473,21 @@ WHERE COALESCE(lv.is_visible, true) = true
|
|||||||
)
|
)
|
||||||
) OR
|
) OR
|
||||||
-- Exact search (with quotes)
|
-- Exact search (with quotes)
|
||||||
|
-- Keeping old pattern commented out in case we want wildcard exact back. See search.go line 62
|
||||||
|
-- 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)
|
||||||
|
-- )
|
||||||
|
-- Exact search (with quotes) - true exact match, not substring
|
||||||
sqlc.narg('is_exact_search') = true AND (
|
sqlc.narg('is_exact_search') = true AND (
|
||||||
mi.title ILIKE sqlc.narg('search_pattern') OR
|
mi.title = sqlc.narg('search_query') OR
|
||||||
mi.author ILIKE sqlc.narg('search_pattern') OR
|
COALESCE(mi.author, '') = sqlc.narg('search_query') OR
|
||||||
mi.series ILIKE sqlc.narg('search_pattern') OR
|
COALESCE(mi.series, '') = sqlc.narg('search_query') OR
|
||||||
sqlc.narg('search_pattern') = ANY(mi.tags_search) OR
|
sqlc.narg('search_query') = ANY(mi.tags_search) OR
|
||||||
sqlc.narg('search_pattern') = ANY(mi.contributors_search)
|
sqlc.narg('search_query') = ANY(mi.contributors_search)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
ORDER BY
|
ORDER BY
|
||||||
|
|||||||
@@ -208,7 +208,7 @@ func registerFrontendRoutes(cfg *Config) {
|
|||||||
HasCover: pgtype.Bool{Bool: false, Valid: true}, // Changed
|
HasCover: pgtype.Bool{Bool: false, Valid: true}, // Changed
|
||||||
SearchQuery: pgtype.Text{String: "", Valid: true}, // NEW: Required
|
SearchQuery: pgtype.Text{String: "", Valid: true}, // NEW: Required
|
||||||
IsExactSearch: pgtype.Bool{Bool: false, Valid: true}, // NEW: Required
|
IsExactSearch: pgtype.Bool{Bool: false, Valid: true}, // NEW: Required
|
||||||
SearchPattern: pgtype.Text{Valid: false}, // NEW: Empty for no exact search
|
// SearchPattern: pgtype.Text{Valid: false}, // NEW: Empty for no exact search
|
||||||
Sort: pgtype.Text{String: "created_at DESC", Valid: true},
|
Sort: pgtype.Text{String: "created_at DESC", Valid: true},
|
||||||
Limit: pgtype.Int4{Int32: int32(limit), Valid: true},
|
Limit: pgtype.Int4{Int32: int32(limit), Valid: true},
|
||||||
Offset: pgtype.Int4{Int32: int32(offset), Valid: true},
|
Offset: pgtype.Int4{Int32: int32(offset), Valid: true},
|
||||||
|
|||||||
@@ -59,10 +59,11 @@ func (s *SearchService) parseSearchQuery(query string) (bool, string) {
|
|||||||
func (s *SearchService) SearchMediaItemsUnified(ctx context.Context, params SearchParams) ([]database.SearchMediaItemsUnifiedRow, error) {
|
func (s *SearchService) SearchMediaItemsUnified(ctx context.Context, params SearchParams) ([]database.SearchMediaItemsUnifiedRow, error) {
|
||||||
// Parse search query for exact match detection
|
// Parse search query for exact match detection
|
||||||
isExact, searchQuery := s.parseSearchQuery(params.SearchQuery)
|
isExact, searchQuery := s.parseSearchQuery(params.SearchQuery)
|
||||||
searchPattern := ""
|
// For now implementing real exact search. If you want to reimplement wildcard patterns uncomment this and line 81 and check queries.sql line 475 and frontend.go line 211
|
||||||
if isExact {
|
// searchPattern := ""
|
||||||
searchPattern = "%" + searchQuery + "%"
|
// if isExact {
|
||||||
}
|
// searchPattern = "%" + searchQuery + "%"
|
||||||
|
// }
|
||||||
|
|
||||||
// Build database parameters - only set valid true if filter
|
// Build database parameters - only set valid true if filter
|
||||||
dbParams := database.SearchMediaItemsUnifiedParams{
|
dbParams := database.SearchMediaItemsUnifiedParams{
|
||||||
@@ -77,7 +78,7 @@ func (s *SearchService) SearchMediaItemsUnified(ctx context.Context, params Sear
|
|||||||
HasCover: pgtype.Bool{Bool: params.HasCover, Valid: true},
|
HasCover: pgtype.Bool{Bool: params.HasCover, Valid: true},
|
||||||
SearchQuery: pgtype.Text{String: searchQuery, Valid: true},
|
SearchQuery: pgtype.Text{String: searchQuery, Valid: true},
|
||||||
IsExactSearch: pgtype.Bool{Bool: isExact, 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},
|
||||||
Offset: pgtype.Int4{Int32: int32(params.Offset), Valid: true},
|
Offset: pgtype.Int4{Int32: int32(params.Offset), Valid: true},
|
||||||
|
|||||||
Reference in New Issue
Block a user