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:
+70
-15
@@ -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:
|
||||
//
|
||||
|
||||
Reference in New Issue
Block a user