fix: disambiguate same-named artists via targeted LB popularity lookup

When multiple artists share the exact same name (e.g. 'The Teenagers'
US vs FR), the index fast path often has zero popularity for both,
causing the MB text relevance score to determine ordering. MB gave
the obscure US band score 100 vs the well-known FR band score 93,
so the wrong one ranked first.

Added disambiguateSameNameArtists(): after the name-match tier sort
groups exact matches at the top, it checks if the same-name block
has undifferentiated scores. If so, it fires a single targeted
ArtistPopularity POST with just those 2-6 MBIDs and re-sorts by
global listen count. The FR Teenagers (1.3M listens) now correctly
rank above the US Teenagers (23K listens).

This only fires when needed — most searches have no same-name
collisions and skip the check entirely.
This commit is contained in:
2026-03-30 01:46:27 -04:00
parent 0a72455f37
commit 6adfd5a68c
+70 -15
View File
@@ -516,7 +516,7 @@ func (e *Service) Search(query string) (*MBSearchResult, error) {
// Phase 5: boost exact/substring name matches so a search for
// "the teenagers" ranks "The Teenagers" above "The Beatles"
// even when The Beatles have vastly more listens.
boostNameMatches(query, &result)
e.boostNameMatches(query, &result)
// Phase 6: filter low-scoring results and cap counts.
filterAndCap(&result)
@@ -1083,32 +1083,26 @@ func (e *Service) boostWithPopularity(result *MBSearchResult) {
//
// The boost is applied after popularity reranking so it acts as
// a final tiebreaker that respects user intent.
func boostNameMatches(query string, result *MBSearchResult) {
func (e *Service) boostNameMatches(query string, result *MBSearchResult) {
q := strings.ToLower(strings.TrimSpace(query))
if q == "" {
return
}
// Boost artists whose name contains the full query.
// Within the same tier, sort by original MB relevance score
// (not the library-boosted blended score) so the most globally
// relevant exact match ranks first.
if len(result.Artists) > 1 {
sort.SliceStable(result.Artists, func(i, j int) bool {
iMatch := nameMatchTier(q, strings.ToLower(result.Artists[i].Name))
jMatch := nameMatchTier(q, strings.ToLower(result.Artists[j].Name))
if iMatch != jMatch {
return iMatch < jMatch // lower tier = better match
}
// Within same tier, prefer higher blended score.
if result.Artists[i].Score != result.Artists[j].Score {
return result.Artists[i].Score > result.Artists[j].Score
}
return false // preserve existing order as last resort
return iMatch < jMatch
})
// For same-named artists in tier 0, resolve ordering via
// a targeted LB popularity lookup. This handles the case
// where multiple artists share a name (e.g. "The Teenagers"
// US vs FR) and the index has no popularity for either.
e.disambiguateSameNameArtists(q, result.Artists)
}
// Boost release groups whose title contains the full query.
@@ -1126,6 +1120,67 @@ func boostNameMatches(query string, result *MBSearchResult) {
}
}
// disambiguateSameNameArtists resolves ordering among artists
// that share the exact same name as the query by fetching their
// LB popularity. This is a targeted micro-lookup (typically 2-6
// MBIDs) that only fires when the index fast path left same-named
// artists with zero popularity.
func (e *Service) disambiguateSameNameArtists(query string, artists []MBArtist) {
// Find the contiguous block of tier-0 same-name artists at the front.
var sameNameEnd int
for sameNameEnd < len(artists) {
if strings.ToLower(artists[sameNameEnd].Name) != query {
break
}
sameNameEnd++
}
if sameNameEnd < 2 {
return // 0 or 1 same-name artists — nothing to disambiguate
}
// Check if they already have differentiated scores.
allSameScore := true
firstScore := artists[0].Score
for i := 1; i < sameNameEnd; i++ {
if artists[i].Score != firstScore {
allSameScore = false
break
}
}
if !allSameScore {
return // scores already differ — reranking handled it
}
// Collect MBIDs for the targeted LB lookup.
mbids := make([]string, 0, sameNameEnd)
for i := range sameNameEnd {
if artists[i].MBID != "" {
mbids = append(mbids, artists[i].MBID)
}
}
if len(mbids) < 2 {
return
}
pop, err := e.lb.ArtistPopularity(e.ctx, mbids)
if err != nil || len(pop) == 0 {
return
}
// Re-sort the same-name block by LB popularity descending.
sort.SliceStable(artists[:sameNameEnd], func(i, j int) bool {
return pop[artists[i].MBID] > pop[artists[j].MBID]
})
}
// nameMatchTier returns a tier value for how well a name matches
// the query. Lower is better:
//