diff --git a/UNIFIED_SEARCH_IMPLEMENTATION.md b/UNIFIED_SEARCH_IMPLEMENTATION.md index 9b04620..4493e07 100644 --- a/UNIFIED_SEARCH_IMPLEMENTATION.md +++ b/UNIFIED_SEARCH_IMPLEMENTATION.md @@ -16,6 +16,54 @@ Autocomplete dropdowns use 4 separate simple queries (one per field type) instea --- +## Implementation Status (as of March 23, 2026) + +### ✅ COMPLETED +- **Phase 1:** Database Schema - GIN indexes for pg_trgm fuzzy search +- **Phase 2:** SQL Queries - SearchMediaItemsUnified + 4 field value queries +- **Phase 3:** Search Service - SearchMediaItemsUnified() + SearchFieldValues() methods +- **Phase 8:** No Changes Needed - Correctly follows established patterns + +### ⚠️ PARTIAL (Ready to Implement) +- **Phase 4.1:** Backend Handler - Code specified in plan, ready to implement + - ✅ Complete handler code with autocomplete detection + - ✅ Complete handleFieldValuesSearch() method + - ✅ Fixed QueryParam bug (echo doesn't support default values) + - ⏳ Ready to paste into media.go + +- **Phase 4.2-4.3:** Frontend Templates - Updated in plan with search button approach + - ✅ Search button + Enter key triggers (no blur trigger) + - ✅ Pure HTML5 datalist (no Alpine.js state) + - ✅ All filter inputs with autocomplete + - ⏳ Ready to paste into bookshelf.templ + +- **Phase 4.4:** Frontend TypeScript - Specified in plan + - ✅ fetchFieldValues() function with native DOM manipulation + - ✅ fetchAuthorValues(), fetchGenreValues(), etc. helper functions + - ✅ Fixed query param names (singular: author, genre, etc.) + - ⏳ Ready to add to bookshelf.ts + +### ❌ NOT STARTED +- **Phase 5:** Frontend Build Verification +- **Phase 6:** Tests - search_unified_test.go needs to be created +- **Phase 7:** Documentation Updates - docs/developer/api/media-items/search_media_items.md needs update +- **Phase 9:** Cleanup - Remove deprecated /filtered endpoint after testing +- **Phase 10:** Verification - Manual testing of all features + +### 📋 IMPLEMENTATION ORDER (Recommended) +1. **Phase 4.1** - Backend Handler (media.go) - ~30 min +2. **Phase 4.2-4.3** - Frontend Templates (bookshelf.templ) - ~20 min +3. **Phase 4.4** - Frontend TypeScript (bookshelf.ts) - ~15 min +4. **Phase 5** - Build & Verify (npm run build, go build) - ~5 min +5. **Phase 6** - Manual Testing (autocomplete, filters, search) - ~15 min +6. **Phase 7** - Tests (create search_unified_test.go) - ~1 hour +7. **Phase 8** - Documentation updates - ~30 min +8. **Phase 9** - Cleanup (remove /filtered) - ~10 min + +**Total remaining: ~3 hours** + +--- + ## Current State Analysis ### Existing Endpoints @@ -473,20 +521,182 @@ func (s *SearchService) SearchFieldValues(ctx context.Context, params FieldSearc **File:** `internal/handlers/media.go` -#### 4.1 Fix Search Box Endpoint +#### 4.1 Update SearchMediaItems Handler to Use SearchService + +**Replace the entire `SearchMediaItems` function (lines 1421-1495) with:** + +```go +// SearchMediaItems handles GET /api/media-items/search +// Supports two modes: +// 1. Autocomplete: author=value, genre=value, etc. → returns field values for dropdowns +// 2. Search: q=value with optional filters → returns media items +func (mh *MediaHandler) SearchMediaItems(c *echo.Context) error { + // Safely get user from context + userID, ok := c.Get("user").(database.Users) + if !ok { + return c.JSON(http.StatusUnauthorized, map[string]string{"error": "user not authenticated"}) + } + + // Detect autocomplete queries first (author=value, genre=value, etc.) + if author := c.QueryParam("author"); author != "" { + return mh.handleFieldValuesSearch(c, userID.ID, "author", author) + } + if genre := c.QueryParam("genre"); genre != "" { + return mh.handleFieldValuesSearch(c, userID.ID, "genre", genre) + } + if series := c.QueryParam("series"); series != "" { + return mh.handleFieldValuesSearch(c, userID.ID, "series", series) + } + if language := c.QueryParam("language"); language != "" { + return mh.handleFieldValuesSearch(c, userID.ID, "language", language) + } + + // Handle media item search with filters + query := c.QueryParam("q") + libraryID := c.QueryParam("library_id") + + // Validate library_id if provided + var libUUID pgtype.UUID + if libraryID != "" { + lib, err := uuid.Parse(libraryID) + if err != nil { + return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid library_id"}) + } + libUUID = pgtype.UUID{Bytes: lib, Valid: true} + } + + limitStr := c.QueryParam("limit") + limit, _ := strconv.Atoi(limitStr) + if limit == 0 { + limit = 50 + } + + offsetStr := c.QueryParam("offset") + offset, _ := strconv.Atoi(offsetStr) + + // Extract filter parameters + authorFilter := c.QueryParam("author_filter") + seriesFilter := c.QueryParam("series_filter") + genreFilter := c.QueryParam("genre_filter") + languageFilter := c.QueryParam("language_filter") + + yearMinStr := c.QueryParam("year_min") + yearMin, _ := strconv.Atoi(yearMinStr) + + yearMaxStr := c.QueryParam("year_max") + yearMax, _ := strconv.Atoi(yearMaxStr) + + hasCover := c.QueryParam("has_cover") == "true" + + // Build search params + params := services.SearchParams{ + UserID: userID.ID, + LibraryID: libUUID, + SearchQuery: query, + AuthorFilter: authorFilter, + SeriesFilter: seriesFilter, + GenreFilter: genreFilter, + LanguageFilter: languageFilter, + YearMin: yearMin, + YearMax: yearMax, + HasCover: hasCover, + Limit: limit, + Offset: offset, + } + + // Call SearchService instead of DB directly + results, err := mh.searchService.SearchMediaItemsUnified(c.Request().Context(), params) + if err != nil && err != pgx.ErrNoRows { + c.Logger().Error("search error", "error", err.Error()) + return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()}) + } + + if len(results) == 0 { + return c.JSON(http.StatusNotFound, map[string]interface{}{ + "error": "no results found", + "query": query, + "results": []interface{}{}, + }) + } + + return c.JSON(http.StatusOK, results) +} + +// handleFieldValuesSearch handles autocomplete queries (author=value, genre=value, etc.) +// Returns distinct field values with counts and similarity scores for dropdown population +func (mh *MediaHandler) handleFieldValuesSearch(c *echo.Context, userID pgtype.UUID, fieldType, searchQuery string) error { + libraryID := c.QueryParam("library_id") + if libraryID == "" { + return c.JSON(http.StatusBadRequest, map[string]string{"error": "library_id is required"}) + } + + libUUID, err := uuid.Parse(libraryID) + if err != nil { + return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid library_id"}) + } + + limitStr := c.QueryParam("limit") + limit, _ := strconv.Atoi(limitStr) + if limit == 0 { + limit = 50 + } + + offsetStr := c.QueryParam("offset") + offset, _ := strconv.Atoi(offsetStr) + + params := services.FieldSearchParams{ + UserID: userID, + LibraryID: pgtype.UUID{Bytes: libUUID, Valid: true}, + FieldType: fieldType, + SearchQuery: searchQuery, + Limit: limit, + Offset: offset, + } + + results, err := mh.searchService.SearchFieldValues(c.Request().Context(), params) + if err != nil { + c.Logger().Error("field values search error", "error", err.Error(), "fieldType", fieldType) + return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()}) + } + + return c.JSON(http.StatusOK, map[string]interface{}{ + "results": results, + "total": len(results), + }) +} +``` + +#### 4.2 Fix Search Box and Pagination **File:** `templates/bookshelf.templ` -**Change line 79:** +**Update global search box (around line 73-83):** + ```html - + - + ``` -**Also fix pagination buttons (lines 298, 311):** +**Update pagination buttons (lines 298, 311):** + ```html hx-get="/api/media-items/filtered?library_id={ currentLibraryID }&limit={ limit }&offset={ offset - limit }" @@ -495,56 +705,195 @@ hx-get="/api/media-items/filtered?library_id={ currentLibraryID }&limit={ limit hx-get="/api/media-items/search?library_id={ currentLibraryID }&limit={ limit }&offset={ offset - limit }" ``` -**Change filter field names (lines 92, 109, 126, etc.):** -```html - - - +**Note:** Change parameter name from `search` to `q` to match backend handler expectation. - - - -``` - -#### 4.2 Add Autocomplete Dropdowns +#### 4.3 Add Autocomplete Dropdowns with Search Button **File:** `templates/bookshelf.templ` -**Replace author filter (lines 85-101):** +**Replace filter form section (includes all filters) with:** ```html - -