feat: artist aliases in FTS5 index + BM25 blended scoring

Migration 14: add aliases TEXT column to explore_index, rebuild FTS5
with 3 columns (title, artist_name, aliases), recreate sync triggers.
Clears index build timestamps to force alias population on next build.

Artist image provider fetches inc=url-rels+aliases (single call, no
extra cost). GetAliases() extracts alias names from cached MB rels.
indexOneArtist stores aliases as space-separated text after image
resolution populates the cache.

Search query now uses BM25 blended scoring:
  ORDER BY bm25(fts, 3.0, 1.0, 0.5) - (ln(popularity+1) * 0.5)

Column weights: title=3.0, artist_name=1.0, aliases=0.5
- Title matches score 3x higher than artist name matches
- Alias matches are helpful but don't dominate
- Popularity is a log-scaled boost, not an override
- Exact title match on niche entity beats weak match on mega-popular

Enables: 'rhcp' → Red Hot Chili Peppers, 'gnr' → Guns N' Roses,
'sabbath' → Black Sabbath (once index build runs with aliases).
This commit is contained in:
2026-03-28 09:46:54 -04:00
parent bb015092d4
commit 54b074eae7
3 changed files with 165 additions and 5 deletions
+33 -1
View File
@@ -168,7 +168,7 @@ func (p *ArtistImageProvider) fetchMBRels(artistMBID string) []mbRelation {
}
url := fmt.Sprintf(
"https://musicbrainz.org/ws/2/artist/%s?fmt=json&inc=url-rels",
"https://musicbrainz.org/ws/2/artist/%s?fmt=json&inc=url-rels+aliases",
artistMBID,
)
@@ -368,6 +368,38 @@ func wikimediaThumbURL(filename string) string {
)
}
// GetAliases returns the artist's aliases as a space-separated
// string, extracted from the cached MB rels response. Returns ""
// if no aliases are cached.
func (p *ArtistImageProvider) GetAliases(artistMBID string) string {
cacheKey := "mb:artist-rels:" + artistMBID
data, ok := p.cache.Get(cacheKey)
if !ok {
return ""
}
var envelope struct {
Aliases []struct {
Name string `json:"name"`
} `json:"aliases"`
}
if err := json.Unmarshal(data, &envelope); err != nil || len(envelope.Aliases) == 0 {
return ""
}
names := make([]string, 0, len(envelope.Aliases))
for _, a := range envelope.Aliases {
if a.Name != "" {
names = append(names, a.Name)
}
}
return strings.Join(names, " ")
}
func (p *ArtistImageProvider) fetchURL(url string) ([]byte, error) {
ctx, cancel := context.WithTimeout(context.Background(), artistImageTimeout)
defer cancel()