1# unified search 2 3global search across tracks, artists, albums, and tags with fuzzy matching. 4 5## usage 6 7**keyboard shortcut**: `Cmd+K` (mac) or `Ctrl+K` (windows/linux) 8 9the search modal opens as an overlay with: 10- instant fuzzy matching as you type 11- results grouped by type with relevance scores 12- keyboard navigation (arrow keys, enter, esc) 13- artwork/avatars displayed when available 14 15## architecture 16 17### frontend 18 19**state management**: `frontend/src/lib/search.svelte.ts` 20 21```typescript 22import { search } from '$lib/search.svelte'; 23 24// open/close 25search.open(); 26search.close(); 27search.toggle(); 28 29// reactive state 30search.isOpen // boolean 31search.query // string 32search.results // SearchResult[] 33search.loading // boolean 34search.error // string | null 35``` 36 37**component**: `frontend/src/lib/components/SearchModal.svelte` 38 39renders the search overlay with: 40- debounced input (150ms) 41- keyboard navigation 42- lazy-loaded images with fallback 43- platform-aware shortcut hints 44 45**keyboard handler**: `frontend/src/routes/+layout.svelte` 46 47```typescript 48// Cmd/Ctrl+K toggles search from anywhere 49if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === 'k') { 50 event.preventDefault(); 51 search.toggle(); 52} 53``` 54 55### backend 56 57**endpoint**: `GET /search/` 58 59``` 60GET /search/?q=query&type=tracks,artists&limit=10 61``` 62 63**parameters**: 64- `q` (required): search query, 2-100 characters 65- `type` (optional): filter by type(s), comma-separated: `tracks`, `artists`, `albums`, `tags` 66- `limit` (optional): max results per type, 1-50, default 20 67 68**response**: 69 70```json 71{ 72 "results": [ 73 { 74 "type": "track", 75 "id": 123, 76 "title": "song name", 77 "artist_handle": "artist.bsky.social", 78 "artist_display_name": "artist name", 79 "image_url": "https://...", 80 "relevance": 0.85 81 }, 82 { 83 "type": "artist", 84 "did": "did:plc:...", 85 "handle": "artist.bsky.social", 86 "display_name": "artist name", 87 "avatar_url": "https://...", 88 "relevance": 0.72 89 } 90 ], 91 "counts": { 92 "tracks": 5, 93 "artists": 2, 94 "albums": 1, 95 "tags": 0 96 } 97} 98``` 99 100**implementation**: `backend/src/backend/api/search.py` 101 102### database 103 104**extension**: `pg_trgm` for trigram-based fuzzy matching 105 106**indexes** (GIN with `gin_trgm_ops`): 107- `ix_tracks_title_trgm` on `tracks.title` 108- `ix_artists_handle_trgm` on `artists.handle` 109- `ix_artists_display_name_trgm` on `artists.display_name` 110- `ix_albums_title_trgm` on `albums.title` 111- `ix_tags_name_trgm` on `tags.name` 112 113**migration**: `backend/alembic/versions/2025_12_03_..._add_pg_trgm_extension_and_search_indexes.py` 114 115## fuzzy matching 116 117uses postgresql's `similarity()` function from `pg_trgm`: 118 119```sql 120SELECT title, similarity(title, 'query') as relevance 121FROM tracks 122WHERE similarity(title, 'query') > 0.1 123ORDER BY relevance DESC 124``` 125 126**threshold**: 0.1 minimum similarity (configurable) 127 128**scoring**: 0.0 to 1.0, where 1.0 is exact match 129 130**examples**: 131- "bufo" matches "bufo" (1.0), "bufo mix" (0.6), "buffalo" (0.4) 132- "zz" matches "zzstoatzz" (0.3), "jazz" (0.25) 133 134## result types 135 136### tracks 137 138- links to `/track/{id}` 139- shows artwork if available 140- subtitle: "by {artist_display_name}" 141 142### artists 143 144- links to `/u/{handle}` 145- shows avatar if available 146- subtitle: "@{handle}" 147 148### albums 149 150- links to `/u/{artist_handle}/album/{slug}` 151- shows cover art if available 152- subtitle: "by {artist_display_name}" 153 154### tags 155 156- links to `/tag/{name}` 157- no artwork 158- subtitle: "{count} tracks" 159 160## error handling 161 162**client-side validation**: 163- minimum 2 characters to search 164- maximum 100 characters (shows inline error) 165 166**api errors**: 167- 422: query validation failed 168- displayed as error message in modal 169 170**image loading**: 171- lazy loading via `loading="lazy"` 172- on error: hides image, shows fallback icon 173 174## scaling 175 176pg_trgm with GIN indexes scales well: 177- handles millions of rows efficiently 178- index size grows ~3x text size 179- queries remain sub-millisecond for typical workloads 180 181current production scale (~100 entities) is trivial. 182 183## future enhancements 184 185- search trigger button in header (for discoverability) 186- recent searches history 187- search within specific entity type tabs 188- full-text search with `tsvector` for longer content 189- search suggestions/autocomplete