fix: release group ranking uses blended score + artist credit matching
Two changes: 1. rerankReleaseGroups now uses blended scoring (text relevance + popularity) like artists, instead of pure popularity. This prevents obscure albums with high listen counts from outranking direct MB search matches. 2. boostNameMatches now uses rgMatchTier() for release groups, which checks artist credit before title. Albums BY the searched artist (tier 0: exact credit match) rank above albums that merely mention the artist in the title (tier 3: title substring). For 'hop along': Painted Shut by Hop Along → tier 0, but Simple Demands: A Hop Along Tribute by Various Artists → tier 3. Within the same tier, blended score breaks ties so more popular albums by the same artist rank first.
This commit is contained in:
@@ -1089,16 +1089,25 @@ func (e *Service) boostNameMatches(query string, result *MBSearchResult) {
|
||||
e.disambiguateSameNameArtists(q, result.Artists)
|
||||
}
|
||||
|
||||
// Boost release groups whose title contains the full query.
|
||||
// Boost release groups whose title or artist credit contains the 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))
|
||||
iMatch := rgMatchTier(q,
|
||||
strings.ToLower(result.ReleaseGroups[i].Title),
|
||||
strings.ToLower(result.ReleaseGroups[i].ArtistCredit))
|
||||
jMatch := rgMatchTier(q,
|
||||
strings.ToLower(result.ReleaseGroups[j].Title),
|
||||
strings.ToLower(result.ReleaseGroups[j].ArtistCredit))
|
||||
|
||||
if iMatch != jMatch {
|
||||
return iMatch < jMatch
|
||||
}
|
||||
|
||||
// Within same tier, prefer higher blended score.
|
||||
if result.ReleaseGroups[i].Score != result.ReleaseGroups[j].Score {
|
||||
return result.ReleaseGroups[i].Score > result.ReleaseGroups[j].Score
|
||||
}
|
||||
|
||||
return false
|
||||
})
|
||||
}
|
||||
@@ -1171,6 +1180,39 @@ func nameMatchTier(query, name string) int {
|
||||
return 3
|
||||
}
|
||||
|
||||
// rgMatchTier returns a tier for release groups considering both
|
||||
// the title and artist credit. An album by "Hop Along" called
|
||||
// "Painted Shut" should rank above a tribute album called
|
||||
// "A Hop Along Tribute" by Various Artists.
|
||||
//
|
||||
// 0 = artist credit matches query exactly ("hop along" == "hop along")
|
||||
// 1 = artist credit starts with or contains query
|
||||
// 2 = title matches query exactly
|
||||
// 3 = title starts with or contains query
|
||||
// 4 = no match in either field
|
||||
func rgMatchTier(query, title, artistCredit string) int {
|
||||
// Artist credit match is stronger — it means the album is BY
|
||||
// the searched artist, not just mentioning them in the title.
|
||||
if artistCredit == query {
|
||||
return 0
|
||||
}
|
||||
|
||||
if strings.Contains(artistCredit, query) {
|
||||
return 1
|
||||
}
|
||||
|
||||
// Title match — the album name contains the query.
|
||||
if title == query {
|
||||
return 2
|
||||
}
|
||||
|
||||
if strings.Contains(title, query) {
|
||||
return 3
|
||||
}
|
||||
|
||||
return 4
|
||||
}
|
||||
|
||||
// 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) {
|
||||
@@ -1219,16 +1261,26 @@ func rerankRecordings(recordings []MBRecording, pop map[string]int) {
|
||||
}
|
||||
}
|
||||
|
||||
// rerankReleaseGroups sorts release groups by popularity only
|
||||
// (they have no MB score field).
|
||||
// rerankReleaseGroups sorts release groups by blended score
|
||||
// (text relevance + popularity) and updates their Score field.
|
||||
func rerankReleaseGroups(rgs []MBReleaseGroup, pop map[string]int) {
|
||||
if len(rgs) == 0 || len(pop) == 0 {
|
||||
if len(rgs) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
maxPop := maxListenCount(pop)
|
||||
|
||||
sort.SliceStable(rgs, func(i, j int) bool {
|
||||
return pop[rgs[i].MBID] > pop[rgs[j].MBID]
|
||||
si := blendedScore(float64(rgs[i].Score)/100.0, pop[rgs[i].MBID], maxPop)
|
||||
sj := blendedScore(float64(rgs[j].Score)/100.0, pop[rgs[j].MBID], maxPop)
|
||||
|
||||
return si > sj
|
||||
})
|
||||
|
||||
for i := range rgs {
|
||||
s := blendedScore(float64(rgs[i].Score)/100.0, pop[rgs[i].MBID], maxPop)
|
||||
rgs[i].Score = int(s * 100)
|
||||
}
|
||||
}
|
||||
|
||||
// blendedScore computes relevanceWeight*relevance + popularityWeight*logPop.
|
||||
|
||||
@@ -441,6 +441,7 @@ func convertReleaseGroup(rg musicbrainzws2.ReleaseGroup) MBReleaseGroup {
|
||||
SecondaryTypes: rg.SecondaryTypes,
|
||||
FirstReleaseDate: rg.FirstReleaseDate.String(),
|
||||
ArtistCredit: rg.ArtistCredit.String(),
|
||||
Score: rg.Score,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -37,6 +37,7 @@ type MBReleaseGroup struct {
|
||||
SecondaryTypes []string `json:"secondaryTypes,omitempty"`
|
||||
FirstReleaseDate string `json:"firstReleaseDate"`
|
||||
ArtistCredit string `json:"artistCredit"`
|
||||
Score int `json:"-"` // MB search relevance, used for reranking
|
||||
}
|
||||
|
||||
// MBRelease is a Wails-friendly projection of a MusicBrainz release.
|
||||
|
||||
Reference in New Issue
Block a user