unified search#
global search across tracks, artists, albums, and tags with fuzzy matching.
usage#
keyboard shortcut: Cmd+K (mac) or Ctrl+K (windows/linux)
the search modal opens as an overlay with:
- instant fuzzy matching as you type
- results grouped by type with relevance scores
- keyboard navigation (arrow keys, enter, esc)
- artwork/avatars displayed when available
architecture#
frontend#
state management: frontend/src/lib/search.svelte.ts
import { search } from '$lib/search.svelte';
// open/close
search.open();
search.close();
search.toggle();
// reactive state
search.isOpen // boolean
search.query // string
search.results // SearchResult[]
search.loading // boolean
search.error // string | null
component: frontend/src/lib/components/SearchModal.svelte
renders the search overlay with:
- debounced input (150ms)
- keyboard navigation
- lazy-loaded images with fallback
- platform-aware shortcut hints
keyboard handler: frontend/src/routes/+layout.svelte
// Cmd/Ctrl+K toggles search from anywhere
if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === 'k') {
event.preventDefault();
search.toggle();
}
backend#
endpoint: GET /search/
GET /search/?q=query&type=tracks,artists&limit=10
parameters:
q(required): search query, 2-100 characterstype(optional): filter by type(s), comma-separated:tracks,artists,albums,tagslimit(optional): max results per type, 1-50, default 20
response:
{
"results": [
{
"type": "track",
"id": 123,
"title": "song name",
"artist_handle": "artist.bsky.social",
"artist_display_name": "artist name",
"image_url": "https://...",
"relevance": 0.85
},
{
"type": "artist",
"did": "did:plc:...",
"handle": "artist.bsky.social",
"display_name": "artist name",
"avatar_url": "https://...",
"relevance": 0.72
}
],
"counts": {
"tracks": 5,
"artists": 2,
"albums": 1,
"tags": 0
}
}
implementation: backend/src/backend/api/search.py
database#
extension: pg_trgm for trigram-based fuzzy matching
indexes (GIN with gin_trgm_ops):
ix_tracks_title_trgmontracks.titleix_artists_handle_trgmonartists.handleix_artists_display_name_trgmonartists.display_nameix_albums_title_trgmonalbums.titleix_tags_name_trgmontags.name
migration: backend/alembic/versions/2025_12_03_..._add_pg_trgm_extension_and_search_indexes.py
fuzzy matching#
uses postgresql's similarity() function from pg_trgm:
SELECT title, similarity(title, 'query') as relevance
FROM tracks
WHERE similarity(title, 'query') > 0.1
ORDER BY relevance DESC
threshold: 0.1 minimum similarity (configurable)
scoring: 0.0 to 1.0, where 1.0 is exact match
examples:
- "bufo" matches "bufo" (1.0), "bufo mix" (0.6), "buffalo" (0.4)
- "zz" matches "zzstoatzz" (0.3), "jazz" (0.25)
result types#
tracks#
- links to
/track/{id} - shows artwork if available
- subtitle: "by {artist_display_name}"
artists#
- links to
/u/{handle} - shows avatar if available
- subtitle: "@{handle}"
albums#
- links to
/u/{artist_handle}/album/{slug} - shows cover art if available
- subtitle: "by {artist_display_name}"
tags#
- links to
/tag/{name} - no artwork
- subtitle: "{count} tracks"
error handling#
client-side validation:
- minimum 2 characters to search
- maximum 100 characters (shows inline error)
api errors:
- 422: query validation failed
- displayed as error message in modal
image loading:
- lazy loading via
loading="lazy" - on error: hides image, shows fallback icon
scaling#
pg_trgm with GIN indexes scales well:
- handles millions of rows efficiently
- index size grows ~3x text size
- queries remain sub-millisecond for typical workloads
current production scale (~100 entities) is trivial.
future enhancements#
- search trigger button in header (for discoverability)
- recent searches history
- search within specific entity type tabs
- full-text search with
tsvectorfor longer content - search suggestions/autocomplete