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 - -
- -
- +
+
+ +
+ + + +
+ + +
+ + + +
+ + +
+ + + +
+ + +
+ + + +
+ + +
+ + +
+ +
+ + +
+ + +
+ + +
+
+ + +
+ +
-
+ ``` -**Similar changes for genre, series, language filters** +**Key changes:** +- ❌ No Alpine.js `x-data` wrapper (removed) +- ❌ No `hx-trigger="change"` (no immediate filtering) +- ✅ Added `@input.debounce.300ms` for autocomplete (300ms delay) +- ✅ Added `hx-trigger="keyup[key=='Enter'] from:#filter-form"` (Enter key only) +- ✅ All filter inputs wrapped in `
` +- ✅ Search button with `hx-get` to trigger filtering +- ✅ Clear filters button to reset all inputs +- ✅ Empty `` elements (populated by JavaScript) +- ✅ `list="author-datalist"` attribute for native HTML5 autocomplete -#### 4.3 Add Frontend Functions +#### 4.4 Add Frontend Functions **File:** `web/src/bookshelf.ts` @@ -615,13 +964,13 @@ async function fetchFieldValues( // Fetch author values for autocomplete async function fetchAuthorValues(input: HTMLInputElement): Promise { const search = input.value; - await fetchFieldValues("authors", search, "author-datalist"); + await fetchFieldValues("author", search, "author-datalist"); } // Fetch genre values for autocomplete async function fetchGenreValues(input: HTMLInputElement): Promise { const search = input.value; - await fetchFieldValues("genres", search, "genre-datalist"); + await fetchFieldValues("genre", search, "genre-datalist"); } // Fetch series values for autocomplete @@ -633,7 +982,7 @@ async function fetchSeriesValues(input: HTMLInputElement): Promise { // Fetch language values for autocomplete async function fetchLanguageValues(input: HTMLInputElement): Promise { const search = input.value; - await fetchFieldValues("languages", search, "language-datalist"); + await fetchFieldValues("language", search, "language-datalist"); } // Register functions globally @@ -650,7 +999,7 @@ cd web && npm run build --- -### Phase 5: Tests +### Phase 6: Tests **File:** `cmd/server/tests/search_unified_test.go` (NEW) @@ -768,7 +1117,7 @@ go test ./cmd/server/tests -v -run TestUnifiedSearch --- -### Phase 6: Documentation Updates +### Phase 7: Documentation Updates #### 6.1 Update API Documentation @@ -1005,7 +1354,7 @@ params: --- -### Phase 7: No Changes Needed (Skip) +### Phase 8: No Changes Needed (Skip) **No changes required to:** - `cmd/server/main.go` @@ -1021,7 +1370,7 @@ SearchService follows the pattern of `FiltersService` and `CollectionService` - --- -### Phase 8: Cleanup +### Phase 9: Cleanup #### 7.1 Delete Deprecated Filtered Endpoint @@ -1248,8 +1597,9 @@ git commit -m "chore: final verification of unified search implementation 1. **Saved filters breaking:** Frontend-only, should work seamlessly 2. **Performance degradation:** GIN indexes should prevent this -3. **Breaking mobile apps:** `/filtered` endpoint will be deleted -4. **Test coverage gaps:** Comprehensive tests in Phase 5 +3. **Breaking mobile apps:** `/filtered` endpoint will be deleted in Phase 9 +4. **Test coverage gaps:** Comprehensive tests in Phase 6 +5. **Handler integration incomplete:** Phase 4.1 handler updates were missing from original plan (NOW ADDED) ### Rollback Plan @@ -1267,19 +1617,20 @@ git show HEAD~1:templates/bookshelf.templ > templates/bookshelf.templ ## Timeline Estimate -- Phase 1 (Database): 30 minutes -- Phase 2 (SQL Queries): 1 hour -- Phase 3 (Search Service): 2 hours -- Phase 4 (Handler Update): 1 hour -- Phase 5 (No Changes): 0 minutes -- Phase 6 (Frontend): 2 hours -- Phase 7 (TypeScript): 1 hour -- Phase 8 (Tests): 2 hours -- Phase 9 (Documentation): 1 hour -- Phase 10 (Cleanup): 30 minutes -- Phase 11 (Verification): 1 hour +- Phase 1 (Database): 30 minutes ✅ COMPLETED +- Phase 2 (SQL Queries): 1 hour ✅ COMPLETED +- Phase 3 (Search Service): 2 hours ✅ COMPLETED +- Phase 4 (Handler + Frontend): 2 hours ⚠️ PARTIAL (code specified, ready to implement) +- Phase 5 (Frontend Build): 10 minutes ❌ NOT STARTED +- Phase 6 (Tests): 2 hours ❌ NOT STARTED +- Phase 7 (Documentation): 1 hour ❌ NOT STARTED +- Phase 8 (No Changes): 0 minutes ✅ SKIPPED +- Phase 9 (Cleanup): 30 minutes ❌ NOT STARTED +- Phase 10 (Verification): 1 hour ❌ NOT STARTED -**Total: ~11.5 hours** +**Total: ~10 hours** +**Completed: ~3.5 hours** (Phases 1-3) +**Remaining: ~3 hours** (Phases 4-10, all code specified, just implementation) ---