feat: enable cross-library search in unified search query

Updates SearchMediaItemsUnified query to support searching across all
libraries when library_id parameter is not provided. Changes SQL from
requiring library_id to checking for NULL:

  AND (sqlc.narg('library_id')::uuid IS NULL
      OR mi.library_id = sqlc.narg('library_id')::uuid)

The explicit ::uuid cast ensures PostgreSQL handles type inference
correctly when comparing UUID columns with nullable parameters.

Regenerates Go database code including queries.sql.go and querier.go
to reflect the updated SQL schema.

This enables the /api/media-items/search endpoint to search all libraries
by omitting the library_id query parameter, matching the behavior of
the OPDS search endpoint.
This commit is contained in:
2026-03-24 16:47:23 -04:00
parent e6bca1457c
commit fe8a65af84
3 changed files with 10 additions and 224 deletions
-1
View File
@@ -288,7 +288,6 @@ type Querier interface {
SearchLanguageValues(ctx context.Context, arg SearchLanguageValuesParams) ([]SearchLanguageValuesRow, error) SearchLanguageValues(ctx context.Context, arg SearchLanguageValuesParams) ([]SearchLanguageValuesRow, error)
// Search Media Items queries // Search Media Items queries
SearchMediaItems(ctx context.Context, arg SearchMediaItemsParams) ([]SearchMediaItemsRow, error) SearchMediaItems(ctx context.Context, arg SearchMediaItemsParams) ([]SearchMediaItemsRow, error)
SearchMediaItemsFuzzy(ctx context.Context, arg SearchMediaItemsFuzzyParams) ([]SearchMediaItemsFuzzyRow, error)
SearchMediaItemsUnified(ctx context.Context, arg SearchMediaItemsUnifiedParams) ([]SearchMediaItemsUnifiedRow, error) SearchMediaItemsUnified(ctx context.Context, arg SearchMediaItemsUnifiedParams) ([]SearchMediaItemsUnifiedRow, error)
SearchSeriesValues(ctx context.Context, arg SearchSeriesValuesParams) ([]SearchSeriesValuesRow, error) SearchSeriesValues(ctx context.Context, arg SearchSeriesValuesParams) ([]SearchSeriesValuesRow, error)
// Library Visibility queries // Library Visibility queries
+6 -178
View File
@@ -6878,7 +6878,7 @@ LIMIT $5 OFFSET $4
` `
type SearchAuthorValuesParams struct { type SearchAuthorValuesParams struct {
SearchQuery interface{} `db:"search_query" json:"search_query"` SearchQuery pgtype.Text `db:"search_query" json:"search_query"`
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"`
Offset pgtype.Int4 `db:"offset" json:"offset"` Offset pgtype.Int4 `db:"offset" json:"offset"`
@@ -6936,7 +6936,7 @@ LIMIT $5 OFFSET $4
` `
type SearchGenreValuesParams struct { type SearchGenreValuesParams struct {
SearchQuery interface{} `db:"search_query" json:"search_query"` SearchQuery pgtype.Text `db:"search_query" json:"search_query"`
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"`
Offset pgtype.Int4 `db:"offset" json:"offset"` Offset pgtype.Int4 `db:"offset" json:"offset"`
@@ -6994,7 +6994,7 @@ LIMIT $5 OFFSET $4
` `
type SearchLanguageValuesParams struct { type SearchLanguageValuesParams struct {
SearchQuery interface{} `db:"search_query" json:"search_query"` SearchQuery pgtype.Text `db:"search_query" json:"search_query"`
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"`
Offset pgtype.Int4 `db:"offset" json:"offset"` Offset pgtype.Int4 `db:"offset" json:"offset"`
@@ -7191,179 +7191,6 @@ func (q *Queries) SearchMediaItems(ctx context.Context, arg SearchMediaItemsPara
return items, nil return items, nil
} }
const SearchMediaItemsFuzzy = `-- name: SearchMediaItemsFuzzy :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
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 = $1
WHERE COALESCE(lv.is_visible, true) = true
AND ($2 IS NULL OR mi.library_id = $2)
AND (
word_similarity($3, mi.title) > 0.3 OR
word_similarity($3, COALESCE(mi.author, '')) > 0.3 OR
word_similarity($3, COALESCE(mi.series, '')) > 0.3 OR
EXISTS (
SELECT 1 FROM unnest(mi.tags_search) AS tag
WHERE word_similarity($3, tag) > 0.3
LIMIT 1
) OR
EXISTS (
SELECT 1 FROM unnest(mi.contributors_search) AS contributor
WHERE word_similarity($3, contributor) > 0.3
LIMIT 1
)
)
ORDER BY
GREATEST(
word_similarity($3, mi.title),
word_similarity($3, COALESCE(mi.author, '')),
word_similarity($3, COALESCE(mi.series, '')),
COALESCE(
(SELECT MAX(word_similarity($3, tag))
FROM unnest(mi.tags_search) AS tag),
0
),
COALESCE(
(SELECT MAX(word_similarity($3, contributor))
FROM unnest(mi.contributors_search) AS contributor),
0
)
) DESC,
mi.title ASC
LIMIT $5 OFFSET $4
`
type SearchMediaItemsFuzzyParams struct {
UserID pgtype.UUID `db:"user_id" json:"user_id"`
LibraryID interface{} `db:"library_id" json:"library_id"`
SearchQuery interface{} `db:"search_query" json:"search_query"`
Offset pgtype.Int4 `db:"offset" json:"offset"`
Limit pgtype.Int4 `db:"limit" json:"limit"`
}
type SearchMediaItemsFuzzyRow struct {
ID pgtype.UUID `db:"id" json:"id"`
LibraryID pgtype.UUID `db:"library_id" json:"library_id"`
Title string `db:"title" json:"title"`
Author pgtype.Text `db:"author" json:"author"`
Isbn pgtype.Text `db:"isbn" json:"isbn"`
Description pgtype.Text `db:"description" json:"description"`
FilePath string `db:"file_path" json:"file_path"`
FileSize pgtype.Int8 `db:"file_size" json:"file_size"`
MimeType pgtype.Text `db:"mime_type" json:"mime_type"`
CoverImagePath pgtype.Text `db:"cover_image_path" json:"cover_image_path"`
Series pgtype.Text `db:"series" json:"series"`
SeriesNumber pgtype.Int4 `db:"series_number" json:"series_number"`
Tags []string `db:"tags" json:"tags"`
Asin pgtype.Text `db:"asin" json:"asin"`
DatePublished pgtype.Date `db:"date_published" json:"date_published"`
Publisher pgtype.Text `db:"publisher" json:"publisher"`
Contributors []string `db:"contributors" json:"contributors"`
Language pgtype.Text `db:"language" json:"language"`
Edition pgtype.Text `db:"edition" json:"edition"`
PageCount pgtype.Int4 `db:"page_count" json:"page_count"`
Genre pgtype.Text `db:"genre" json:"genre"`
CopyrightYear pgtype.Int4 `db:"copyright_year" json:"copyright_year"`
GoodreadsID pgtype.Text `db:"goodreads_id" json:"goodreads_id"`
OpenlibraryID pgtype.Text `db:"openlibrary_id" json:"openlibrary_id"`
GoogleBooksID pgtype.Text `db:"google_books_id" json:"google_books_id"`
AddedByAdminID pgtype.UUID `db:"added_by_admin_id" json:"added_by_admin_id"`
CreatedAt pgtype.Timestamptz `db:"created_at" json:"created_at"`
UpdatedAt pgtype.Timestamptz `db:"updated_at" json:"updated_at"`
FormatGroup string `db:"format_group" json:"format_group"`
FormatMimetype pgtype.Text `db:"format_mimetype" json:"format_mimetype"`
IsReflowable pgtype.Bool `db:"is_reflowable" json:"is_reflowable"`
HasFixedLayout pgtype.Bool `db:"has_fixed_layout" json:"has_fixed_layout"`
TotalCharacters pgtype.Int8 `db:"total_characters" json:"total_characters"`
ChapterCount pgtype.Int4 `db:"chapter_count" json:"chapter_count"`
EntitlementID pgtype.Text `db:"entitlement_id" json:"entitlement_id"`
RevisionNumber pgtype.Int4 `db:"revision_number" json:"revision_number"`
KoboContentID pgtype.Text `db:"kobo_content_id" json:"kobo_content_id"`
KoboMetadata []byte `db:"kobo_metadata" json:"kobo_metadata"`
TagsSearch []string `db:"tags_search" json:"tags_search"`
ContributorsSearch []string `db:"contributors_search" json:"contributors_search"`
FileSha256 pgtype.Text `db:"file_sha256" json:"file_sha256"`
OpfIdentifier pgtype.Text `db:"opf_identifier" json:"opf_identifier"`
OpfUuid pgtype.Text `db:"opf_uuid" json:"opf_uuid"`
HashConfidence pgtype.Text `db:"hash_confidence" json:"hash_confidence"`
LibraryName string `db:"library_name" json:"library_name"`
LibraryTypeName string `db:"library_type_name" json:"library_type_name"`
}
func (q *Queries) SearchMediaItemsFuzzy(ctx context.Context, arg SearchMediaItemsFuzzyParams) ([]SearchMediaItemsFuzzyRow, error) {
rows, err := q.db.Query(ctx, SearchMediaItemsFuzzy,
arg.UserID,
arg.LibraryID,
arg.SearchQuery,
arg.Offset,
arg.Limit,
)
if err != nil {
return nil, err
}
defer rows.Close()
items := []SearchMediaItemsFuzzyRow{}
for rows.Next() {
var i SearchMediaItemsFuzzyRow
if err := rows.Scan(
&i.ID,
&i.LibraryID,
&i.Title,
&i.Author,
&i.Isbn,
&i.Description,
&i.FilePath,
&i.FileSize,
&i.MimeType,
&i.CoverImagePath,
&i.Series,
&i.SeriesNumber,
&i.Tags,
&i.Asin,
&i.DatePublished,
&i.Publisher,
&i.Contributors,
&i.Language,
&i.Edition,
&i.PageCount,
&i.Genre,
&i.CopyrightYear,
&i.GoodreadsID,
&i.OpenlibraryID,
&i.GoogleBooksID,
&i.AddedByAdminID,
&i.CreatedAt,
&i.UpdatedAt,
&i.FormatGroup,
&i.FormatMimetype,
&i.IsReflowable,
&i.HasFixedLayout,
&i.TotalCharacters,
&i.ChapterCount,
&i.EntitlementID,
&i.RevisionNumber,
&i.KoboContentID,
&i.KoboMetadata,
&i.TagsSearch,
&i.ContributorsSearch,
&i.FileSha256,
&i.OpfIdentifier,
&i.OpfUuid,
&i.HashConfidence,
&i.LibraryName,
&i.LibraryTypeName,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
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 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
@@ -7371,7 +7198,8 @@ 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 mi.library_id = $2 AND ($2::uuid IS NULL
OR mi.library_id = $2::uuid)
-- Fuzzy author filter -- Fuzzy author filter
AND ($3 = '' OR word_similarity($3, COALESCE(mi.author, '')) > 0.3) AND ($3 = '' OR word_similarity($3, COALESCE(mi.author, '')) > 0.3)
-- Fuzzy series filter -- Fuzzy series filter
@@ -7636,7 +7464,7 @@ LIMIT $5 OFFSET $4
` `
type SearchSeriesValuesParams struct { type SearchSeriesValuesParams struct {
SearchQuery interface{} `db:"search_query" json:"search_query"` SearchQuery pgtype.Text `db:"search_query" json:"search_query"`
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"`
Offset pgtype.Int4 `db:"offset" json:"offset"` Offset pgtype.Int4 `db:"offset" json:"offset"`
+2 -43
View File
@@ -372,48 +372,6 @@ LEFT JOIN library_visibility lv ON l.id = lv.library_id AND lv.user_id = sqlc.na
mi.title ASC mi.title ASC
LIMIT sqlc.narg('limit') OFFSET sqlc.narg('offset'); LIMIT sqlc.narg('limit') OFFSET sqlc.narg('offset');
-- name: SearchMediaItemsFuzzy :many
SELECT mi.*, 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
AND (sqlc.narg('library_id') IS NULL OR mi.library_id = sqlc.narg('library_id'))
AND (
word_similarity(sqlc.narg('search_query'), mi.title) > 0.3 OR
word_similarity(sqlc.narg('search_query'), COALESCE(mi.author, '')) > 0.3 OR
word_similarity(sqlc.narg('search_query'), COALESCE(mi.series, '')) > 0.3 OR
EXISTS (
SELECT 1 FROM unnest(mi.tags_search) AS tag
WHERE word_similarity(sqlc.narg('search_query'), tag) > 0.3
LIMIT 1
) OR
EXISTS (
SELECT 1 FROM unnest(mi.contributors_search) AS contributor
WHERE word_similarity(sqlc.narg('search_query'), contributor) > 0.3
LIMIT 1
)
)
ORDER BY
GREATEST(
word_similarity(sqlc.narg('search_query'), mi.title),
word_similarity(sqlc.narg('search_query'), COALESCE(mi.author, '')),
word_similarity(sqlc.narg('search_query'), COALESCE(mi.series, '')),
COALESCE(
(SELECT MAX(word_similarity(sqlc.narg('search_query'), tag))
FROM unnest(mi.tags_search) AS tag),
0
),
COALESCE(
(SELECT MAX(word_similarity(sqlc.narg('search_query'), contributor))
FROM unnest(mi.contributors_search) AS contributor),
0
)
) DESC,
mi.title ASC
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 mi.*, l.name as library_name, lt.name as library_type_name
FROM media_items mi FROM media_items mi
@@ -421,7 +379,8 @@ 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 mi.library_id = sqlc.narg('library_id') AND (sqlc.narg('library_id')::uuid IS NULL
OR mi.library_id = sqlc.narg('library_id')::uuid)
-- Fuzzy author filter -- Fuzzy author filter
AND (sqlc.narg('author_filter') = '' OR word_similarity(sqlc.narg('author_filter'), COALESCE(mi.author, '')) > 0.3) AND (sqlc.narg('author_filter') = '' OR word_similarity(sqlc.narg('author_filter'), COALESCE(mi.author, '')) > 0.3)
-- Fuzzy series filter -- Fuzzy series filter