diff --git a/internal/database/queries.sql.go b/internal/database/queries.sql.go index fe885ab..b054422 100644 --- a/internal/database/queries.sql.go +++ b/internal/database/queries.sql.go @@ -7259,9 +7259,17 @@ WHERE COALESCE(lv.is_visible, true) = true SELECT 1 FROM unnest(mi.tags_search) AS tag WHERE word_similarity($7, tag) > 0.3 )) - -- Year range (exact) - AND ($8 = 0 OR mi.copyright_year >= $8) - AND ($9 = 0 OR mi.copyright_year <= $9) + -- 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) + ) + AND ( + $9 = 0 OR + EXTRACT(YEAR FROM mi.date_published) <= $9 OR + (mi.date_published IS NULL AND mi.copyright_year <= $9) + ) -- Boolean (exact) AND ($10 = false OR mi.cover_image_path IS NOT NULL) -- Search query (fuzzy or exact based on quotes) @@ -7284,12 +7292,21 @@ WHERE COALESCE(lv.is_visible, true) = true ) ) OR -- 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 ( - mi.title ILIKE $13 OR - mi.author ILIKE $13 OR - mi.series ILIKE $13 OR - $13 = ANY(mi.tags_search) OR - $13 = ANY(mi.contributors_search) + 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) ) ) ORDER BY @@ -7313,40 +7330,40 @@ ORDER BY END DESC, -- Secondary sort: user-specified sort parameter CASE - WHEN $14 = 'title ASC' THEN mi.title + WHEN $13 = 'title ASC' THEN mi.title ELSE '' END ASC, CASE - WHEN $14 = 'title DESC' THEN mi.title + WHEN $13 = 'title DESC' THEN mi.title ELSE '' END DESC, CASE - WHEN $14 = 'author ASC' THEN COALESCE(mi.author, '') + WHEN $13 = 'author ASC' THEN COALESCE(mi.author, '') ELSE '' END ASC, CASE - WHEN $14 = 'author DESC' THEN COALESCE(mi.author, '') + WHEN $13 = 'author DESC' THEN COALESCE(mi.author, '') ELSE '' END DESC, CASE - WHEN $14 = 'created_at ASC' THEN mi.created_at + WHEN $13 = 'created_at ASC' THEN mi.created_at ELSE '1970-01-01'::timestamp END ASC, CASE - WHEN $14 = 'created_at DESC' THEN mi.created_at + WHEN $13 = 'created_at DESC' THEN mi.created_at ELSE '1970-01-01'::timestamp END DESC, 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 '' END ASC, 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 '' END DESC, -- Tertiary sort: title (default fallback) mi.title ASC -LIMIT $16 OFFSET $15 +LIMIT $15 OFFSET $14 ` type SearchMediaItemsUnifiedParams struct { @@ -7362,7 +7379,6 @@ type SearchMediaItemsUnifiedParams struct { HasCover interface{} `db:"has_cover" json:"has_cover"` SearchQuery interface{} `db:"search_query" json:"search_query"` 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"` Offset pgtype.Int4 `db:"offset" json:"offset"` Limit pgtype.Int4 `db:"limit" json:"limit"` @@ -7431,7 +7447,6 @@ func (q *Queries) SearchMediaItemsUnified(ctx context.Context, arg SearchMediaIt arg.HasCover, arg.SearchQuery, arg.IsExactSearch, - arg.SearchPattern, arg.Sort, arg.Offset, arg.Limit, diff --git a/internal/database/queries/queries.sql b/internal/database/queries/queries.sql index 2f68ab9..78808ee 100644 --- a/internal/database/queries/queries.sql +++ b/internal/database/queries/queries.sql @@ -440,9 +440,17 @@ WHERE COALESCE(lv.is_visible, true) = true SELECT 1 FROM unnest(mi.tags_search) AS tag WHERE word_similarity(sqlc.narg('tags_filter'), tag) > 0.3 )) - -- Year range (exact) - AND (sqlc.narg('year_min') = 0 OR mi.copyright_year >= sqlc.narg('year_min')) - AND (sqlc.narg('year_max') = 0 OR mi.copyright_year <= sqlc.narg('year_max')) + -- Year range (exact) - prioritize date_published, fallback to copyright_year + AND ( + 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) AND (sqlc.narg('has_cover') = false OR mi.cover_image_path IS NOT NULL) -- Search query (fuzzy or exact based on quotes) @@ -465,12 +473,21 @@ WHERE COALESCE(lv.is_visible, true) = true ) ) OR -- 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 ( - 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) + mi.title = sqlc.narg('search_query') OR + COALESCE(mi.author, '') = sqlc.narg('search_query') OR + COALESCE(mi.series, '') = sqlc.narg('search_query') OR + sqlc.narg('search_query') = ANY(mi.tags_search) OR + sqlc.narg('search_query') = ANY(mi.contributors_search) ) ) ORDER BY diff --git a/internal/router/frontend.go b/internal/router/frontend.go index 8783430..4d495a1 100644 --- a/internal/router/frontend.go +++ b/internal/router/frontend.go @@ -208,10 +208,10 @@ func registerFrontendRoutes(cfg *Config) { HasCover: pgtype.Bool{Bool: false, Valid: true}, // Changed SearchQuery: pgtype.Text{String: "", Valid: true}, // NEW: Required IsExactSearch: pgtype.Bool{Bool: false, Valid: true}, // NEW: Required - SearchPattern: pgtype.Text{Valid: false}, // NEW: Empty for no exact search - Sort: pgtype.Text{String: "created_at DESC", Valid: true}, - Limit: pgtype.Int4{Int32: int32(limit), Valid: true}, - Offset: pgtype.Int4{Int32: int32(offset), Valid: true}, + // SearchPattern: pgtype.Text{Valid: false}, // NEW: Empty for no exact search + Sort: pgtype.Text{String: "created_at DESC", Valid: true}, + Limit: pgtype.Int4{Int32: int32(limit), Valid: true}, + Offset: pgtype.Int4{Int32: int32(offset), Valid: true}, }) if err != nil { diff --git a/internal/services/search.go b/internal/services/search.go index 7a20861..9f2aa22 100644 --- a/internal/services/search.go +++ b/internal/services/search.go @@ -59,10 +59,11 @@ func (s *SearchService) parseSearchQuery(query string) (bool, string) { func (s *SearchService) SearchMediaItemsUnified(ctx context.Context, params SearchParams) ([]database.SearchMediaItemsUnifiedRow, error) { // Parse search query for exact match detection isExact, searchQuery := s.parseSearchQuery(params.SearchQuery) - searchPattern := "" - if isExact { - searchPattern = "%" + searchQuery + "%" - } + // 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 + // searchPattern := "" + // if isExact { + // searchPattern = "%" + searchQuery + "%" + // } // Build database parameters - only set valid true if filter dbParams := database.SearchMediaItemsUnifiedParams{ @@ -77,10 +78,10 @@ func (s *SearchService) SearchMediaItemsUnified(ctx context.Context, params Sear HasCover: pgtype.Bool{Bool: params.HasCover, Valid: true}, SearchQuery: pgtype.Text{String: searchQuery, Valid: true}, IsExactSearch: pgtype.Bool{Bool: isExact, Valid: true}, - SearchPattern: pgtype.Text{String: searchPattern, Valid: isExact}, - Sort: pgtype.Text{String: params.Sort, Valid: true}, - Limit: pgtype.Int4{Int32: int32(params.Limit), Valid: true}, - Offset: pgtype.Int4{Int32: int32(params.Offset), Valid: true}, + // SearchPattern: pgtype.Text{String: searchPattern, Valid: isExact}, + Sort: pgtype.Text{String: params.Sort, Valid: true}, + Limit: pgtype.Int4{Int32: int32(params.Limit), Valid: true}, + Offset: pgtype.Int4{Int32: int32(params.Offset), Valid: true}, } if params.LibraryID.Valid {