feat: add search queries with partial matching and fuzzy fallback
- Add SearchMediaItems query with ILIKE partial matching - Add SearchMediaItemsFuzzy query with word_similarity() - Use sqlc.narg() for named parameters (search_pattern, search_query) - Rank results by relevance: title > author > series > tags - Fuzzy threshold set to 0.3 for word_similarity - Generated Go models with proper parameter types
This commit is contained in:
@@ -89,6 +89,9 @@ type Querier interface {
|
|||||||
ListUsers(ctx context.Context) ([]ListUsersRow, error)
|
ListUsers(ctx context.Context) ([]ListUsersRow, error)
|
||||||
RevokeAllUserRefreshTokens(ctx context.Context, userID pgtype.UUID) error
|
RevokeAllUserRefreshTokens(ctx context.Context, userID pgtype.UUID) error
|
||||||
RevokeRefreshToken(ctx context.Context, token string) error
|
RevokeRefreshToken(ctx context.Context, token string) error
|
||||||
|
// Search Media Items queries
|
||||||
|
SearchMediaItems(ctx context.Context, arg SearchMediaItemsParams) ([]SearchMediaItemsRow, error)
|
||||||
|
SearchMediaItemsFuzzy(ctx context.Context, arg SearchMediaItemsFuzzyParams) ([]SearchMediaItemsFuzzyRow, error)
|
||||||
// Library Visibility queries
|
// Library Visibility queries
|
||||||
SetLibraryVisibility(ctx context.Context, arg SetLibraryVisibilityParams) (LibraryVisibility, error)
|
SetLibraryVisibility(ctx context.Context, arg SetLibraryVisibilityParams) (LibraryVisibility, error)
|
||||||
UpdateEbook(ctx context.Context, arg UpdateEbookParams) (MediaItems, error)
|
UpdateEbook(ctx context.Context, arg UpdateEbookParams) (MediaItems, error)
|
||||||
|
|||||||
@@ -1954,6 +1954,219 @@ func (q *Queries) RevokeRefreshToken(ctx context.Context, token string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const SearchMediaItems = `-- name: SearchMediaItems :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.added_by_admin_id, mi.created_at, mi.updated_at, 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 (
|
||||||
|
mi.title ILIKE $2 OR
|
||||||
|
mi.author ILIKE $2 OR
|
||||||
|
mi.series ILIKE $2 OR
|
||||||
|
mi.tags ILIKE $2 OR
|
||||||
|
mi.contributors ILIKE $2
|
||||||
|
)
|
||||||
|
ORDER BY
|
||||||
|
CASE
|
||||||
|
WHEN mi.title ILIKE $2 THEN 1
|
||||||
|
WHEN mi.author ILIKE $2 THEN 2
|
||||||
|
WHEN mi.series ILIKE $2 THEN 3
|
||||||
|
WHEN mi.tags ILIKE $2 THEN 4
|
||||||
|
ELSE 5
|
||||||
|
END,
|
||||||
|
mi.title ASC
|
||||||
|
LIMIT $4 OFFSET $3
|
||||||
|
`
|
||||||
|
|
||||||
|
type SearchMediaItemsParams struct {
|
||||||
|
UserID pgtype.UUID `db:"user_id" json:"user_id"`
|
||||||
|
SearchPattern pgtype.Text `db:"search_pattern" json:"search_pattern"`
|
||||||
|
Offset pgtype.Int4 `db:"offset" json:"offset"`
|
||||||
|
Limit pgtype.Int4 `db:"limit" json:"limit"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SearchMediaItemsRow 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 pgtype.Text `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 pgtype.Text `db:"contributors" json:"contributors"`
|
||||||
|
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"`
|
||||||
|
LibraryName string `db:"library_name" json:"library_name"`
|
||||||
|
LibraryTypeName string `db:"library_type_name" json:"library_type_name"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search Media Items queries
|
||||||
|
func (q *Queries) SearchMediaItems(ctx context.Context, arg SearchMediaItemsParams) ([]SearchMediaItemsRow, error) {
|
||||||
|
rows, err := q.db.Query(ctx, SearchMediaItems,
|
||||||
|
arg.UserID,
|
||||||
|
arg.SearchPattern,
|
||||||
|
arg.Offset,
|
||||||
|
arg.Limit,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
items := []SearchMediaItemsRow{}
|
||||||
|
for rows.Next() {
|
||||||
|
var i SearchMediaItemsRow
|
||||||
|
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.AddedByAdminID,
|
||||||
|
&i.CreatedAt,
|
||||||
|
&i.UpdatedAt,
|
||||||
|
&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 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.added_by_admin_id, mi.created_at, mi.updated_at, 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 (
|
||||||
|
word_similarity($2, mi.title) > 0.3 OR
|
||||||
|
word_similarity($2, COALESCE(mi.author, '')) > 0.3 OR
|
||||||
|
word_similarity($2, COALESCE(mi.series, '')) > 0.3 OR
|
||||||
|
word_similarity($2, COALESCE(mi.tags, '')) > 0.3 OR
|
||||||
|
word_similarity($2, COALESCE(mi.contributors, '')) > 0.3
|
||||||
|
)
|
||||||
|
ORDER BY
|
||||||
|
GREATEST(
|
||||||
|
word_similarity($2, mi.title),
|
||||||
|
word_similarity($2, COALESCE(mi.author, '')),
|
||||||
|
word_similarity($2, COALESCE(mi.series, '')),
|
||||||
|
word_similarity($2, COALESCE(mi.tags, '')),
|
||||||
|
word_similarity($2, COALESCE(mi.contributors, ''))
|
||||||
|
) DESC,
|
||||||
|
mi.title ASC
|
||||||
|
LIMIT $4 OFFSET $3
|
||||||
|
`
|
||||||
|
|
||||||
|
type SearchMediaItemsFuzzyParams struct {
|
||||||
|
UserID pgtype.UUID `db:"user_id" json:"user_id"`
|
||||||
|
SearchQuery pgtype.Text `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 pgtype.Text `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 pgtype.Text `db:"contributors" json:"contributors"`
|
||||||
|
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"`
|
||||||
|
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.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.AddedByAdminID,
|
||||||
|
&i.CreatedAt,
|
||||||
|
&i.UpdatedAt,
|
||||||
|
&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 SetLibraryVisibility = `-- name: SetLibraryVisibility :one
|
const SetLibraryVisibility = `-- name: SetLibraryVisibility :one
|
||||||
INSERT INTO library_visibility (user_id, library_id, is_visible)
|
INSERT INTO library_visibility (user_id, library_id, is_visible)
|
||||||
VALUES ($1, $2, $3)
|
VALUES ($1, $2, $3)
|
||||||
|
|||||||
@@ -284,6 +284,57 @@ DELETE FROM media_ratings WHERE media_item_id = $1 AND user_id = $2;
|
|||||||
-- name: GetEbookByFilePath :one
|
-- name: GetEbookByFilePath :one
|
||||||
SELECT * FROM ebooks WHERE file_path = $1;
|
SELECT * FROM ebooks WHERE file_path = $1;
|
||||||
|
|
||||||
|
-- Search Media Items queries
|
||||||
|
-- name: SearchMediaItems :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 (
|
||||||
|
mi.title ILIKE sqlc.narg('search_pattern') OR
|
||||||
|
mi.author ILIKE sqlc.narg('search_pattern') OR
|
||||||
|
mi.series ILIKE sqlc.narg('search_pattern') OR
|
||||||
|
mi.tags ILIKE sqlc.narg('search_pattern') OR
|
||||||
|
mi.contributors ILIKE sqlc.narg('search_pattern')
|
||||||
|
)
|
||||||
|
ORDER BY
|
||||||
|
CASE
|
||||||
|
WHEN mi.title ILIKE sqlc.narg('search_pattern') THEN 1
|
||||||
|
WHEN mi.author ILIKE sqlc.narg('search_pattern') THEN 2
|
||||||
|
WHEN mi.series ILIKE sqlc.narg('search_pattern') THEN 3
|
||||||
|
WHEN mi.tags ILIKE sqlc.narg('search_pattern') THEN 4
|
||||||
|
ELSE 5
|
||||||
|
END,
|
||||||
|
mi.title ASC
|
||||||
|
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 (
|
||||||
|
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
|
||||||
|
word_similarity(sqlc.narg('search_query'), COALESCE(mi.tags, '')) > 0.3 OR
|
||||||
|
word_similarity(sqlc.narg('search_query'), COALESCE(mi.contributors, '')) > 0.3
|
||||||
|
)
|
||||||
|
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, '')),
|
||||||
|
word_similarity(sqlc.narg('search_query'), COALESCE(mi.tags, '')),
|
||||||
|
word_similarity(sqlc.narg('search_query'), COALESCE(mi.contributors, ''))
|
||||||
|
) DESC,
|
||||||
|
mi.title ASC
|
||||||
|
LIMIT sqlc.narg('limit') OFFSET sqlc.narg('offset');
|
||||||
|
|
||||||
-- Media Notes queries
|
-- Media Notes queries
|
||||||
-- name: CreateMediaNote :one
|
-- name: CreateMediaNote :one
|
||||||
INSERT INTO media_notes (media_item_id, user_id, content, position)
|
INSERT INTO media_notes (media_item_id, user_id, content, position)
|
||||||
|
|||||||
Reference in New Issue
Block a user