fix: name-match boost prevents popular unrelated artists from dominating
Searching 'the teenagers' ranked The Beatles (#2) and Rolling Stones (#3) above the actual band because MB text search matches the word 'the' at score ~54, and 142M LB listens with 60% popularity weight overwhelmed the low text relevance. Added boostNameMatches() as a post-reranking step that stable-sorts results by name-match tier: 0 = exact match ('the teenagers' == 'the teenagers') 1 = name starts with query 2 = query is a substring of the name 3 = no substring match (only individual words matched) Within each tier, the existing popularity-blended order is preserved. This ensures The Teenagers (all variants) always rank above The Beatles for this query, while The Beatles still rank highly among tier-3 results. Also added the second Various Artists MBID (89ad4ac3) to the SPA blocklist.
This commit is contained in:
@@ -513,7 +513,12 @@ func (e *Service) Search(query string) (*MBSearchResult, error) {
|
||||
// Phase 4: merge local index hits into results, dedup by MBID.
|
||||
mergeIndexHits(&result, indexHits)
|
||||
|
||||
// Phase 5: filter low-scoring results and cap counts.
|
||||
// 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)
|
||||
|
||||
// Phase 6: filter low-scoring results and cap counts.
|
||||
filterAndCap(&result)
|
||||
|
||||
totalDur := time.Since(searchStart)
|
||||
@@ -925,7 +930,8 @@ var mbSpecialPurposeArtists = map[string]bool{
|
||||
"eec63d3c-3b81-4ad4-b1e4-7c147c4d2b61": true, // [no artist]
|
||||
"9be7f096-97ec-4615-8957-8c3b659f51b4": true, // [traditional]
|
||||
"80a8851f-444c-4539-892b-ad2a49f7f0d0": true, // [Church bells]
|
||||
"ae636985-40e8-4fe2-80cb-9c1a21c6e30a": true, // Various Artists (not an SPA but often pollutes artist results)
|
||||
"ae636985-40e8-4fe2-80cb-9c1a21c6e30a": true, // Various Artists (SPA, accumulates bogus popularity)
|
||||
"89ad4ac3-39f7-470e-963a-56509c546377": true, // Various Artists (regular MBID, same issue)
|
||||
}
|
||||
|
||||
// boostWithIndexPopularity reranks MB search results using
|
||||
@@ -1068,6 +1074,73 @@ func (e *Service) boostWithPopularity(result *MBSearchResult) {
|
||||
rerankReleaseGroups(result.ReleaseGroups, rgPop)
|
||||
}
|
||||
|
||||
// boostNameMatches re-sorts artists and release groups so that
|
||||
// exact or substring name matches rank above results that only
|
||||
// matched on common words like "the". Without this, a search
|
||||
// for "the teenagers" would rank The Beatles above The Teenagers
|
||||
// because The Beatles' massive popularity compensates for their
|
||||
// weak text relevance on the word "the".
|
||||
//
|
||||
// The boost is applied after popularity reranking so it acts as
|
||||
// a final tiebreaker that respects user intent.
|
||||
func boostNameMatches(query string, result *MBSearchResult) {
|
||||
q := strings.ToLower(strings.TrimSpace(query))
|
||||
if q == "" {
|
||||
return
|
||||
}
|
||||
|
||||
// Boost artists whose name contains the full query.
|
||||
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
|
||||
}
|
||||
|
||||
return false // preserve existing order within same tier
|
||||
})
|
||||
}
|
||||
|
||||
// Boost release groups whose title contains the full query.
|
||||
if len(result.ReleaseGroups) > 1 {
|
||||
sort.SliceStable(result.ReleaseGroups, func(i, j int) bool {
|
||||
iMatch := nameMatchTier(q, strings.ToLower(result.ReleaseGroups[i].Title))
|
||||
jMatch := nameMatchTier(q, strings.ToLower(result.ReleaseGroups[j].Title))
|
||||
|
||||
if iMatch != jMatch {
|
||||
return iMatch < jMatch
|
||||
}
|
||||
|
||||
return false
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// nameMatchTier returns a tier value for how well a name matches
|
||||
// the query. Lower is better:
|
||||
//
|
||||
// 0 = exact match ("the teenagers" == "the teenagers")
|
||||
// 1 = name starts with query ("the teenagers" in "the teenagers feat. X")
|
||||
// 2 = query is a substring ("the teenagers" in "al supersonic & the teenagers")
|
||||
// 3 = no substring match (only individual words matched)
|
||||
func nameMatchTier(query, name string) int {
|
||||
if name == query {
|
||||
return 0
|
||||
}
|
||||
|
||||
if strings.HasPrefix(name, query) {
|
||||
return 1
|
||||
}
|
||||
|
||||
if strings.Contains(name, query) {
|
||||
return 2
|
||||
}
|
||||
|
||||
return 3
|
||||
}
|
||||
|
||||
// rerankArtists sorts artists by blended score and updates their
|
||||
// Score field to the new value (0–100 scale).
|
||||
func rerankArtists(artists []MBArtist, pop map[string]int) {
|
||||
|
||||
Reference in New Issue
Block a user