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
@@ -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');
-- 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
JOIN libraries l ON mi.library_id = l.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')
WHERE COALESCE(lv.is_visible, true) = true
WHERE COALESCE(lv.is_visible, true) = true
AND (sqlc.narg('library_id')::uuid IS NULL
OR mi.library_id = sqlc.narg('library_id')::uuid)
-- 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
mi.id,
-- Primary sort: relevance score when searching
CASE
WHEN sqlc.narg('search_query') != '' THEN