From 59b32f482757152c8f9d337091af3449cb80430f Mon Sep 17 00:00:00 2001 From: John O'Keefe Date: Thu, 29 Jan 2026 20:20:35 -0500 Subject: [PATCH] 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 --- internal/handlers/ebook.go | 58 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/internal/handlers/ebook.go b/internal/handlers/ebook.go index bd4a44a..a129488 100644 --- a/internal/handlers/ebook.go +++ b/internal/handlers/ebook.go @@ -96,6 +96,7 @@ func SetupRoutes(g *echo.Group, db *database.Queries) *Handler { // Media item routes (new library system) g.GET("/media-items", h.ListMediaItems) + g.GET("/media-items/search", h.SearchMediaItems) g.GET("/media-items/:id", h.GetMediaItem) g.POST("/media-items/:id/rating", h.CreateMediaRating) 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) } + +// 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) +}