feat: implement search endpoint with partial match and fuzzy fallback
- Add GET /api/media-items/search endpoint - Try partial matching first (ILIKE with wildcards) - Fallback to fuzzy search if no results found - Return 404 with 'no results found' when no matches - Limit results to 50 items by default - Supports search across title, author, series, tags, contributors - Respects library visibility settings per user
This commit is contained in:
@@ -96,6 +96,7 @@ func SetupRoutes(g *echo.Group, db *database.Queries) *Handler {
|
|||||||
|
|
||||||
// Media item routes (new library system)
|
// Media item routes (new library system)
|
||||||
g.GET("/media-items", h.ListMediaItems)
|
g.GET("/media-items", h.ListMediaItems)
|
||||||
|
g.GET("/media-items/search", h.SearchMediaItems)
|
||||||
g.GET("/media-items/:id", h.GetMediaItem)
|
g.GET("/media-items/:id", h.GetMediaItem)
|
||||||
g.POST("/media-items/:id/rating", h.CreateMediaRating)
|
g.POST("/media-items/:id/rating", h.CreateMediaRating)
|
||||||
g.GET("/media-items/:id/rating", h.GetMediaRating)
|
g.GET("/media-items/:id/rating", h.GetMediaRating)
|
||||||
@@ -1668,3 +1669,60 @@ func (h *Handler) DeleteEbookHighlight(c echo.Context) error {
|
|||||||
|
|
||||||
return c.NoContent(http.StatusNoContent)
|
return c.NoContent(http.StatusNoContent)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SearchMediaItems handles GET /api/media-items/search
|
||||||
|
// Performs partial matching search with fuzzy fallback if no results found
|
||||||
|
func (h *Handler) SearchMediaItems(c echo.Context) error {
|
||||||
|
query := c.QueryParam("q")
|
||||||
|
userID := c.Get("user_id").(string)
|
||||||
|
|
||||||
|
if query == "" {
|
||||||
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": "query parameter 'q' is required"})
|
||||||
|
}
|
||||||
|
|
||||||
|
userUUID, err := uuid.Parse(userID)
|
||||||
|
if err != nil {
|
||||||
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid user id"})
|
||||||
|
}
|
||||||
|
|
||||||
|
limit := int32(50)
|
||||||
|
offset := int32(0)
|
||||||
|
|
||||||
|
searchPattern := "%" + query + "%"
|
||||||
|
|
||||||
|
partialResults, err := h.db.SearchMediaItems(c.Request().Context(), database.SearchMediaItemsParams{
|
||||||
|
SearchPattern: pgtype.Text{String: searchPattern, Valid: true},
|
||||||
|
UserID: pgtype.UUID{Bytes: userUUID, Valid: true},
|
||||||
|
Limit: pgtype.Int4{Int32: limit, Valid: true},
|
||||||
|
Offset: pgtype.Int4{Int32: offset, Valid: true},
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil && err != pgx.ErrNoRows {
|
||||||
|
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(partialResults) > 0 {
|
||||||
|
return c.JSON(http.StatusOK, partialResults)
|
||||||
|
}
|
||||||
|
|
||||||
|
fuzzyResults, err := h.db.SearchMediaItemsFuzzy(c.Request().Context(), database.SearchMediaItemsFuzzyParams{
|
||||||
|
SearchQuery: pgtype.Text{String: query, Valid: true},
|
||||||
|
UserID: pgtype.UUID{Bytes: userUUID, Valid: true},
|
||||||
|
Limit: pgtype.Int4{Int32: limit, Valid: true},
|
||||||
|
Offset: pgtype.Int4{Int32: offset, Valid: true},
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil && err != pgx.ErrNoRows {
|
||||||
|
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(fuzzyResults) == 0 {
|
||||||
|
return c.JSON(http.StatusNotFound, map[string]interface{}{
|
||||||
|
"error": "no results found",
|
||||||
|
"query": query,
|
||||||
|
"results": []interface{}{},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.JSON(http.StatusOK, fuzzyResults)
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user