docs: update implementation plan with fuzzy matching decision
- Update SQL queries to use fuzzy matching for tags_filter
- Add ORDER BY clause changes for tag similarity scoring
- Update test code to use setupDeviceTest() instead of setupTestServer()
- Document fuzzy matching behavior throughout
- Update examples to show fuzzy matching ("Sci Fi" → "Science Fiction")
- Add missing comma fix to SQL ORDER BY clause
- Correct test helper function references
- Note that collection-rules.ts already supports both genre and tags
Updates the implementation plan to reflect the decision to use fuzzy
matching for tags_filter, making it consistent with other filters.
Includes corrections to test code and documentation improvements.
Relates to IMPLEMENTATION_TAGS_FILTER.md planning updates
This commit is contained in:
@@ -5,6 +5,7 @@
|
||||
**Type:** Full-Stack Task
|
||||
**Priority:** Medium
|
||||
**Estimated Time:** 2-3 hours
|
||||
**Note:** Tags filter uses fuzzy matching (consistent with author/series filters)
|
||||
|
||||
---
|
||||
|
||||
@@ -65,11 +66,13 @@ The current genre filter expects a single `genre` text field, which is always NU
|
||||
|
||||
## 📋 Implementation Checklist
|
||||
|
||||
### Phase 1: Backend - SQL Layer (15 minutes)
|
||||
### Phase 1: Backend - SQL Layer (25 minutes)
|
||||
- [ ] Add `tags_filter` parameter to `SearchMediaItemsUnified` query
|
||||
- [ ] Update filter logic from `word_similarity()` to `= ANY()` for array matching
|
||||
- [ ] Add fuzzy matching filter logic with EXISTS clause and word_similarity()
|
||||
- [ ] Add tag similarity scoring to ORDER BY clause (GREATEST function)
|
||||
- [ ] Keep `genre_filter` for backward compatibility
|
||||
- [ ] Regenerate Go code with `sqlc generate`
|
||||
- [ ] Test SQL query in database console (verify fuzzy matching works)
|
||||
|
||||
### Phase 2: Backend - Service Layer (20 minutes)
|
||||
- [ ] Add `TagsFilter string` to `SearchParams` struct
|
||||
@@ -96,10 +99,11 @@ The current genre filter expects a single `genre` text field, which is always NU
|
||||
- [ ] Regenerate template Go files with `templ generate`
|
||||
|
||||
### Phase 6: Bruno Collection Updates (15 minutes)
|
||||
- [ ] Create `Fuzzy Tags Filter.yml` (rename/update genre filter test)
|
||||
- [ ] Create `Field Values Search - Tags.yml` (rename genre autocomplete)
|
||||
- [ ] Create `Fuzzy Tags Filter.yml` (test fuzzy matching behavior)
|
||||
- [ ] Create `Field Values Search - Tags.yml` (test autocomplete)
|
||||
- [ ] Update `Combined Search and Filters.yml` (genre → tags_filter)
|
||||
- [ ] Add documentation for tags filter behavior
|
||||
- [ ] Add test cases for fuzzy matching (e.g., "Sci Fi" → "Science Fiction")
|
||||
- [ ] Add documentation for fuzzy tags filter behavior
|
||||
|
||||
### Phase 7: Documentation (20 minutes)
|
||||
- [ ] Create `docs/developer/api/media-items/search_media_items.md` (or update existing)
|
||||
@@ -142,8 +146,11 @@ AND (sqlc.narg('genre_filter') = '' OR word_similarity(sqlc.narg('genre_filter')
|
||||
-- AFTER:
|
||||
-- Fuzzy genre filter (kept for backward compatibility)
|
||||
AND (sqlc.narg('genre_filter') = '' OR word_similarity(sqlc.narg('genre_filter'), COALESCE(mi.genre, '')) > 0.3)
|
||||
-- Tags filter (NEW - matches against tags array)
|
||||
AND (sqlc.narg('tags_filter') = '' OR sqlc.narg('tags_filter') = ANY(mi.tags_search))
|
||||
-- Tags filter (NEW - fuzzy match against tags array)
|
||||
AND (sqlc.narg('tags_filter') = '' OR EXISTS (
|
||||
SELECT 1 FROM unnest(mi.tags_search) AS tag
|
||||
WHERE word_similarity(sqlc.narg('tags_filter'), tag) > 0.3
|
||||
))
|
||||
```
|
||||
|
||||
**New Query:** Add `SearchTagsValues` for autocomplete
|
||||
@@ -165,6 +172,26 @@ ORDER BY word_similarity(sqlc.narg('search_query'), unnest(mi.tags_search))::flo
|
||||
LIMIT sqlc.narg('limit') OFFSET sqlc.narg('offset');
|
||||
```
|
||||
|
||||
**ORDER BY Clause Update (line ~484):**
|
||||
Add tag similarity scoring to the GREATEST() calculation:
|
||||
```sql
|
||||
-- In the ORDER BY clause, add to the GREATEST() function:
|
||||
GREATEST(
|
||||
CASE WHEN sqlc.narg('is_exact_search') = false THEN
|
||||
word_similarity(sqlc.narg('search_query'), mi.title)
|
||||
ELSE 0 END,
|
||||
CASE WHEN sqlc.narg('is_exact_search') = false THEN
|
||||
word_similarity(sqlc.narg('search_query'), COALESCE(mi.author, ''))
|
||||
ELSE 0 END,
|
||||
word_similarity(sqlc.narg('author_filter'), COALESCE(mi.author, '')),
|
||||
word_similarity(sqlc.narg('genre_filter'), COALESCE(mi.genre, '')),
|
||||
(SELECT MAX(word_similarity(sqlc.narg('tags_filter'), tag))
|
||||
FROM unnest(mi.tags_search) AS tag) -- NEW: Tag similarity score
|
||||
)
|
||||
```
|
||||
|
||||
This ensures results with better tag matches appear first.
|
||||
|
||||
### 2. Service Layer Changes
|
||||
|
||||
**File:** `internal/services/search.go`
|
||||
@@ -289,19 +316,6 @@ async fetchTagValues(input: HTMLInputElement): Promise<void> {
|
||||
]
|
||||
```
|
||||
|
||||
**File:** `web/src/collection-rules.ts`
|
||||
|
||||
**Change:** Line 45
|
||||
```typescript
|
||||
// AFTER (add new field, keep old commented out):
|
||||
const ruleFields = {
|
||||
tags: "Tags",
|
||||
// TODO: Uncomment if genre field is populated in future
|
||||
// genre: "Genre",
|
||||
// ... other fields
|
||||
};
|
||||
```
|
||||
|
||||
### 5. Template Changes
|
||||
|
||||
**File:** `templates/bookshelf.templ` (source file)
|
||||
@@ -360,7 +374,7 @@ import (
|
||||
)
|
||||
|
||||
func TestTagsFilter(t *testing.T) {
|
||||
setup := setupTestServer(t) // ✅ Using test_helpers
|
||||
setup := setupDeviceTest(t) // ✅ Using test_helpers
|
||||
defer setup.Server.Close()
|
||||
|
||||
libraryID := setup.CreateLibrary(t, "Test Tags Library", "ebooks")
|
||||
@@ -392,12 +406,20 @@ func TestTagsFilter(t *testing.T) {
|
||||
createBook("The Hobbit", "Tolkien", []string{"Fantasy", "Adventure"})
|
||||
|
||||
t.Run("Tags filter - Science Fiction", func(t *testing.T) {
|
||||
req := httptest.NewRequest("GET", "/api/media-items/search?library_id="+libraryID+"&tags_filter=Science+Foundation", nil)
|
||||
req := httptest.NewRequest("GET", "/api/media-items/search?library_id="+libraryID+"&tags_filter=Science+Fiction", nil)
|
||||
req.Header.Set("Authorization", "Bearer "+setup.UserToken)
|
||||
// ... test implementation
|
||||
// Should return Foundation, Dune, Neuromancer (all have "Science Fiction" tag)
|
||||
})
|
||||
|
||||
t.Run("Tags filter - fuzzy match", func(t *testing.T) {
|
||||
req := httptest.NewRequest("GET", "/api/media-items/search?library_id="+libraryID+"&tags_filter=Sci+Fi", nil)
|
||||
req.Header.Set("Authorization", "Bearer "+setup.UserToken)
|
||||
// ... test implementation
|
||||
// Should return Foundation, Dune, Neuromancer (fuzzy matches "Science Fiction")
|
||||
// Results should be ranked by similarity score
|
||||
})
|
||||
|
||||
t.Run("Tags filter - Fantasy", func(t *testing.T) {
|
||||
// Should return only The Hobbit
|
||||
})
|
||||
@@ -405,7 +427,7 @@ func TestTagsFilter(t *testing.T) {
|
||||
t.Run("Tags autocomplete", func(t *testing.T) {
|
||||
req := httptest.NewRequest("GET", "/api/media-items/search?tags=Sci&library_id="+libraryID, nil)
|
||||
req.Header.Set("Authorization", "Bearer "+setup.UserToken)
|
||||
// Should return "Science Fiction" in autocomplete results
|
||||
// Should return "Science Fiction" in autocomplete results with fuzzy matching
|
||||
})
|
||||
|
||||
t.Run("Backward compatibility - genre_filter still works", func(t *testing.T) {
|
||||
@@ -413,7 +435,7 @@ func TestTagsFilter(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("Combined filters - tags + author", func(t *testing.T) {
|
||||
// tags_filter=Science+Fiction&author_filter=Asimov
|
||||
// tags_filter=Sci+Fi&author_filter=Asimov
|
||||
// Should return only Foundation
|
||||
})
|
||||
}
|
||||
@@ -422,6 +444,8 @@ func TestTagsFilter(t *testing.T) {
|
||||
### Manual QA Checklist
|
||||
|
||||
- [ ] Tags filter returns books with matching tags
|
||||
- [ ] Tags filter uses fuzzy matching (e.g., "Sci Fi" matches "Science Fiction")
|
||||
- [ ] Results are ranked by tag similarity score (best matches first)
|
||||
- [ ] Tags autocomplete shows tag values as you type
|
||||
- [ ] Multiple tags in book are all searchable
|
||||
- [ ] Combined filters work (tags + author + series)
|
||||
@@ -455,7 +479,7 @@ Search and filter media items with fuzzy matching and tag-based filtering.
|
||||
| `library_id` | string | No | Filter by library UUID |
|
||||
| `author_filter` | string | No | Fuzzy match author field |
|
||||
| `series_filter` | string | No | Fuzzy match series field |
|
||||
| `tags_filter` | string | No | **Exact match tags array** (NEW) |
|
||||
| `tags_filter` | string | No | **Fuzzy match tags array** (NEW) |
|
||||
| `language_filter` | string | No | Fuzzy match language field |
|
||||
| `year_min` | integer | No | Minimum copyright year |
|
||||
| `year_max` | integer | No | Maximum copyright year |
|
||||
@@ -468,21 +492,29 @@ Search and filter media items with fuzzy matching and tag-based filtering.
|
||||
|
||||
**New in v1.x:** The `tags_filter` parameter filters books by their tags array.
|
||||
|
||||
- **Matching:** Exact match (not fuzzy) - the filter value must equal a tag
|
||||
- **Matching:** Fuzzy match using `word_similarity()` - matches tags with similarity > 0.3
|
||||
- **Calibre Integration:** Works seamlessly with Calibre tags (which include genre)
|
||||
- **Result Ranking:** Best tag matches appear first in results
|
||||
- **Example Tags:** `["Science Fiction", "Adventure", "Dystopian"]`
|
||||
|
||||
**Examples:**
|
||||
**Fuzzy Matching Examples:**
|
||||
|
||||
```bash
|
||||
# Filter by "Science Fiction" tag
|
||||
# Exact match: "Science Fiction"
|
||||
GET /api/media-items/search?tags_filter=Science+Fiction
|
||||
# Returns: All books with "Science Fiction" tag
|
||||
|
||||
# Fuzzy match: "Sci Fi" → "Science Fiction"
|
||||
GET /api/media-items/search?tags_filter=Sci+Fi
|
||||
# Returns: Books with "Science Fiction", "Sci-Fi", "Scifi" (score > 0.3)
|
||||
|
||||
# Combine tags with author
|
||||
GET /api/media-items/search?tags_filter=Science+Fiction&author_filter=Asimov
|
||||
GET /api/media-items/search?tags_filter=Sci+Fi&author_filter=Asimov
|
||||
# Returns: Science Fiction books by Asimov, best matches first
|
||||
|
||||
# Filter by multiple tags (returns books with ANY matching tag)
|
||||
GET /api/media-items/search?tags_filter=Cyberpunk
|
||||
# Fuzzy match with variations
|
||||
GET /api/media-items/search?tags_filter=Cyber
|
||||
# Returns: Books with "Cyberpunk", "Cyber", "Cyberthriller" tags
|
||||
```
|
||||
|
||||
### Backward Compatibility
|
||||
@@ -519,7 +551,6 @@ tags_filter=Science+Fiction
|
||||
}
|
||||
]
|
||||
```
|
||||
```
|
||||
|
||||
### User Documentation
|
||||
|
||||
@@ -560,8 +591,9 @@ You can combine tags with other filters:
|
||||
|
||||
- Tags from Calibre are automatically imported
|
||||
- You can add custom tags when editing book metadata
|
||||
- Tags are case-insensitive for searching
|
||||
- Autocomplete shows existing tags as you type
|
||||
- Tags use fuzzy matching - "Sci Fi" will match "Science Fiction"
|
||||
- Best matching tags appear first in results
|
||||
- Autocomplete shows existing tags as you type with relevance scores
|
||||
```
|
||||
|
||||
---
|
||||
@@ -570,9 +602,11 @@ You can combine tags with other filters:
|
||||
|
||||
### Commit 1: Backend SQL Layer
|
||||
```
|
||||
feat: add tags_filter to search query
|
||||
feat: add fuzzy tags_filter to search query
|
||||
|
||||
- Add tags_filter parameter to SearchMediaItemsUnified
|
||||
- Add fuzzy tags_filter parameter to SearchMediaItemsUnified
|
||||
- Add EXISTS clause with word_similarity() for fuzzy tag matching
|
||||
- Add tag similarity scoring to ORDER BY clause
|
||||
- Add SearchTagsValues query for autocomplete
|
||||
- Keep genre_filter for backward compatibility
|
||||
|
||||
|
||||
Reference in New Issue
Block a user