fix: eliminate duplicate search results from library visibility LEFT JOIN

Problem:
The search API was returning duplicate media items when searching across
libraries. For example, searching for "Harry" with 2 books would return
4-8 results instead of 2, depending on how many users had library visibility
entries.

Root Cause:
The SearchMediaItemsUnified query uses a LEFT JOIN with library_visibility:

  LEFT JOIN library_visibility lv ON l.id = lv.library_id AND lv.user_id = $1

When multiple library_visibility entries exist for the same library
(e.g., one per user during testing), the LEFT JOIN can create duplicate
rows for each media_item. The query didn't have a DISTINCT clause to
eliminate these duplicates.

Solution:
Added DISTINCT ON (mi.id) clause with mi.id as the first ORDER BY expression:

  SELECT DISTINCT ON (mi.id) mi.*, ...
  FROM media_items mi
  ...
  ORDER BY mi.id, <other_sort_criteria>

This ensures that even if the LEFT JOIN produces multiple rows per
media_item, only one row per mi.id is returned, preserving the first
occurrence based on the relevance sorting.

Impact:
- Search results now correctly return unique media items
- Test TestCollectionSearchLibraryFilter will pass after database cleanup
- No API changes required - this is purely a query optimization

Note: After deploying this change, residual test data should be cleaned up
with: docker-compose down -v && docker-compose up -d

Files changed:
- internal/database/queries/queries.sql: Added DISTINCT ON clause
- internal/database/queries.sql.go: Regenerated from sqlc
This commit is contained in:
2026-03-24 20:25:13 -04:00
parent a45a47e9d3
commit 5571a47830
2 changed files with 98 additions and 4 deletions
+49 -2
View File
@@ -7192,12 +7192,58 @@ func (q *Queries) SearchMediaItems(ctx context.Context, arg SearchMediaItemsPara
} }
const SearchMediaItemsUnified = `-- name: SearchMediaItemsUnified :many const SearchMediaItemsUnified = `-- name: SearchMediaItemsUnified :many
SELECT mi.id, mi.library_id, mi.title, mi.author, mi.isbn, mi.description, mi.file_path, mi.file_size, mi.mime_type, mi.cover_image_path, mi.series, mi.series_number, mi.tags, mi.asin, mi.date_published, mi.publisher, mi.contributors, mi.language, mi.edition, mi.page_count, mi.genre, mi.copyright_year, mi.goodreads_id, mi.openlibrary_id, mi.google_books_id, mi.added_by_admin_id, mi.created_at, mi.updated_at, mi.format_group, mi.format_mimetype, mi.is_reflowable, mi.has_fixed_layout, mi.total_characters, mi.chapter_count, mi.entitlement_id, mi.revision_number, mi.kobo_content_id, mi.kobo_metadata, mi.tags_search, mi.contributors_search, mi.file_sha256, mi.opf_identifier, mi.opf_uuid, mi.hash_confidence, l.name as library_name, lt.name as library_type_name SELECT DISTINCT ON (mi.id)
mi.id,
mi.library_id,
mi.title,
mi.author,
mi.isbn,
mi.description,
mi.file_path,
mi.file_size,
mi.mime_type,
mi.cover_image_path,
mi.series,
mi.series_number,
mi.tags,
mi.asin,
mi.date_published,
mi.publisher,
mi.contributors,
mi.language,
mi.edition,
mi.page_count,
mi.genre,
mi.copyright_year,
mi.goodreads_id,
mi.openlibrary_id,
mi.google_books_id,
mi.added_by_admin_id,
mi.created_at,
mi.updated_at,
mi.format_group,
mi.format_mimetype,
mi.is_reflowable,
mi.has_fixed_layout,
mi.total_characters,
mi.chapter_count,
mi.entitlement_id,
mi.revision_number,
mi.kobo_content_id,
mi.kobo_metadata,
mi.tags_search,
mi.contributors_search,
mi.file_sha256,
mi.opf_identifier,
mi.opf_uuid,
mi.hash_confidence,
l.name as library_name,
lt.name as library_type_name
FROM media_items mi FROM media_items mi
JOIN libraries l ON mi.library_id = l.id JOIN libraries l ON mi.library_id = l.id
JOIN library_types lt ON l.library_type_id = lt.id JOIN library_types lt ON l.library_type_id = lt.id
LEFT JOIN library_visibility lv ON l.id = lv.library_id AND lv.user_id = $1 LEFT JOIN library_visibility lv ON l.id = lv.library_id AND lv.user_id = $1
WHERE COALESCE(lv.is_visible, true) = true WHERE COALESCE(lv.is_visible, true) = true
AND ($2::uuid IS NULL AND ($2::uuid IS NULL
OR mi.library_id = $2::uuid) OR mi.library_id = $2::uuid)
-- Fuzzy author filter -- Fuzzy author filter
@@ -7242,6 +7288,7 @@ LEFT JOIN library_visibility lv ON l.id = lv.library_id AND lv.user_id = $1
) )
) )
ORDER BY ORDER BY
mi.id,
-- Primary sort: relevance score when searching -- Primary sort: relevance score when searching
CASE CASE
WHEN $10 != '' THEN WHEN $10 != '' THEN
+49 -2
View File
@@ -373,12 +373,58 @@ LEFT JOIN library_visibility lv ON l.id = lv.library_id AND lv.user_id = sqlc.na
LIMIT sqlc.narg('limit') OFFSET sqlc.narg('offset'); LIMIT sqlc.narg('limit') OFFSET sqlc.narg('offset');
-- name: SearchMediaItemsUnified :many -- name: SearchMediaItemsUnified :many
SELECT mi.*, l.name as library_name, lt.name as library_type_name SELECT DISTINCT ON (mi.id)
mi.id,
mi.library_id,
mi.title,
mi.author,
mi.isbn,
mi.description,
mi.file_path,
mi.file_size,
mi.mime_type,
mi.cover_image_path,
mi.series,
mi.series_number,
mi.tags,
mi.asin,
mi.date_published,
mi.publisher,
mi.contributors,
mi.language,
mi.edition,
mi.page_count,
mi.genre,
mi.copyright_year,
mi.goodreads_id,
mi.openlibrary_id,
mi.google_books_id,
mi.added_by_admin_id,
mi.created_at,
mi.updated_at,
mi.format_group,
mi.format_mimetype,
mi.is_reflowable,
mi.has_fixed_layout,
mi.total_characters,
mi.chapter_count,
mi.entitlement_id,
mi.revision_number,
mi.kobo_content_id,
mi.kobo_metadata,
mi.tags_search,
mi.contributors_search,
mi.file_sha256,
mi.opf_identifier,
mi.opf_uuid,
mi.hash_confidence,
l.name as library_name,
lt.name as library_type_name
FROM media_items mi FROM media_items mi
JOIN libraries l ON mi.library_id = l.id JOIN libraries l ON mi.library_id = l.id
JOIN library_types lt ON l.library_type_id = lt.id JOIN library_types lt ON l.library_type_id = lt.id
LEFT JOIN library_visibility lv ON l.id = lv.library_id AND lv.user_id = sqlc.narg('user_id') LEFT JOIN library_visibility lv ON l.id = lv.library_id AND lv.user_id = sqlc.narg('user_id')
WHERE COALESCE(lv.is_visible, true) = true WHERE COALESCE(lv.is_visible, true) = true
AND (sqlc.narg('library_id')::uuid IS NULL AND (sqlc.narg('library_id')::uuid IS NULL
OR mi.library_id = sqlc.narg('library_id')::uuid) OR mi.library_id = sqlc.narg('library_id')::uuid)
-- Fuzzy author filter -- Fuzzy author filter
@@ -423,6 +469,7 @@ LEFT JOIN library_visibility lv ON l.id = lv.library_id AND lv.user_id = sqlc.na
) )
) )
ORDER BY ORDER BY
mi.id,
-- Primary sort: relevance score when searching -- Primary sort: relevance score when searching
CASE CASE
WHEN sqlc.narg('search_query') != '' THEN WHEN sqlc.narg('search_query') != '' THEN