fix: library bonus as post-normalization additive, not pop contamination
The +10M library bonus was added directly to the popularity map, which made it the maxPop normalization denominator. With maxPop=10M, every non-library artist's log-normalized popularity collapsed to near-zero, making their blended score purely 40% of MB relevance. All non-indexed artists scored ~35 and ranked by MB noise. New approach: - Removed +10M from both GetPopularityBatch and boostWithPopularity - GetPopularityBatch now returns PopularityBatchResult with separate Popularity and InLibrary maps - rerankArtists takes a libraryMBIDs set and applies a fixed +25 score bonus AFTER blended scoring and normalization - maxPop reflects real popularity only, so log normalization works correctly across all artists Shannon Wright (766K listens) now properly outranks Shannon Kennedy (95 listens) because the popularity scale isn't contaminated.
This commit is contained in:
+40
-20
@@ -961,6 +961,11 @@ const (
|
|||||||
// popularity. The threshold slides between this and
|
// popularity. The threshold slides between this and
|
||||||
// minBlendedScore based on listen count.
|
// minBlendedScore based on listen count.
|
||||||
minScoreZeroPop = 60
|
minScoreZeroPop = 60
|
||||||
|
|
||||||
|
// libraryScoreBonus is added to library artists' blended scores
|
||||||
|
// after normalization. Applied post-blending so it doesn't
|
||||||
|
// pollute the maxPop denominator.
|
||||||
|
libraryScoreBonus = 25
|
||||||
)
|
)
|
||||||
|
|
||||||
// tierBonus maps artist name-match tiers to percentage score multipliers.
|
// tierBonus maps artist name-match tiers to percentage score multipliers.
|
||||||
@@ -1036,26 +1041,26 @@ func (e *Service) boostWithIndexPopularity(result *MBSearchResult) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Single batch query for all popularity + in_library data.
|
// Single batch query for all popularity + in_library data.
|
||||||
popMap := e.index.GetPopularityBatch(allMBIDs)
|
batch := e.index.GetPopularityBatch(allMBIDs)
|
||||||
if popMap == nil {
|
if batch == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build per-entity maps from the batch result.
|
// Build per-entity maps from the batch result.
|
||||||
artistPop := make(map[string]int, len(result.Artists))
|
artistPop := make(map[string]int, len(result.Artists))
|
||||||
for i, a := range result.Artists {
|
for i, a := range result.Artists {
|
||||||
if pop, ok := popMap[a.MBID]; ok {
|
if pop, ok := batch.Popularity[a.MBID]; ok {
|
||||||
artistPop[a.MBID] = pop
|
artistPop[a.MBID] = pop
|
||||||
result.Artists[i].HasPopularity = true
|
result.Artists[i].HasPopularity = true
|
||||||
result.Artists[i].Popularity = pop
|
result.Artists[i].Popularity = pop
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
rerankArtists(result.Artists, artistPop)
|
rerankArtists(result.Artists, artistPop, batch.InLibrary)
|
||||||
|
|
||||||
rgPop := make(map[string]int, len(result.ReleaseGroups))
|
rgPop := make(map[string]int, len(result.ReleaseGroups))
|
||||||
for _, rg := range result.ReleaseGroups {
|
for _, rg := range result.ReleaseGroups {
|
||||||
if pop, ok := popMap[rg.MBID]; ok {
|
if pop, ok := batch.Popularity[rg.MBID]; ok {
|
||||||
rgPop[rg.MBID] = pop
|
rgPop[rg.MBID] = pop
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1064,7 +1069,7 @@ func (e *Service) boostWithIndexPopularity(result *MBSearchResult) {
|
|||||||
|
|
||||||
recPop := make(map[string]int, len(result.Recordings))
|
recPop := make(map[string]int, len(result.Recordings))
|
||||||
for _, r := range result.Recordings {
|
for _, r := range result.Recordings {
|
||||||
if pop, ok := popMap[r.MBID]; ok {
|
if pop, ok := batch.Popularity[r.MBID]; ok {
|
||||||
recPop[r.MBID] = pop
|
recPop[r.MBID] = pop
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1144,14 +1149,15 @@ func (e *Service) boostWithPopularity(result *MBSearchResult) {
|
|||||||
|
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
|
|
||||||
// Add library bonus to artist popularity — same bonus as the
|
// Build library MBID set for the library score bonus.
|
||||||
// fast path (boostWithIndexPopularity via GetPopularityBatch).
|
libMBIDs := make(map[string]bool)
|
||||||
if artistPop != nil {
|
|
||||||
libraryMBIDs := e.libMBID.CheckMBIDs(artistMBIDs)
|
|
||||||
|
|
||||||
for mbid, entityType := range libraryMBIDs {
|
if artistPop != nil {
|
||||||
|
libraryCheck := e.libMBID.CheckMBIDs(artistMBIDs)
|
||||||
|
|
||||||
|
for mbid, entityType := range libraryCheck {
|
||||||
if entityType == "artist" {
|
if entityType == "artist" {
|
||||||
artistPop[mbid] += 10_000_000 //nolint:mnd
|
libMBIDs[mbid] = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1167,7 +1173,7 @@ func (e *Service) boostWithPopularity(result *MBSearchResult) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Rerank each entity type.
|
// Rerank each entity type.
|
||||||
rerankArtists(result.Artists, artistPop)
|
rerankArtists(result.Artists, artistPop, libMBIDs)
|
||||||
rerankRecordings(result.Recordings, recordingPop)
|
rerankRecordings(result.Recordings, recordingPop)
|
||||||
rerankReleaseGroups(result.ReleaseGroups, rgPop)
|
rerankReleaseGroups(result.ReleaseGroups, rgPop)
|
||||||
}
|
}
|
||||||
@@ -1323,7 +1329,7 @@ func rgMatchTier(query, title, artistCredit string) int {
|
|||||||
|
|
||||||
// rerankArtists sorts artists by blended score and updates their
|
// rerankArtists sorts artists by blended score and updates their
|
||||||
// Score field to the new value (0–100 scale).
|
// Score field to the new value (0–100 scale).
|
||||||
func rerankArtists(artists []MBArtist, pop map[string]int) {
|
func rerankArtists(artists []MBArtist, pop map[string]int, libraryMBIDs map[string]bool) {
|
||||||
if len(artists) == 0 {
|
if len(artists) == 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -1334,16 +1340,30 @@ func rerankArtists(artists []MBArtist, pop map[string]int) {
|
|||||||
si := blendedScore(float64(artists[i].Score)/100.0, pop[artists[i].MBID], maxPop)
|
si := blendedScore(float64(artists[i].Score)/100.0, pop[artists[i].MBID], maxPop)
|
||||||
sj := blendedScore(float64(artists[j].Score)/100.0, pop[artists[j].MBID], maxPop)
|
sj := blendedScore(float64(artists[j].Score)/100.0, pop[artists[j].MBID], maxPop)
|
||||||
|
|
||||||
|
// Library boost as tiebreaker — library artists win ties.
|
||||||
|
if si == sj {
|
||||||
|
iLib := libraryMBIDs[artists[i].MBID]
|
||||||
|
jLib := libraryMBIDs[artists[j].MBID]
|
||||||
|
|
||||||
|
if iLib != jLib {
|
||||||
|
return iLib
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return si > sj
|
return si > sj
|
||||||
})
|
})
|
||||||
|
|
||||||
// Update Score field so the frontend's top-results section can
|
// Update Score field. Library artists get a post-normalization
|
||||||
// use it directly.
|
// bonus that doesn't pollute the maxPop denominator.
|
||||||
maxPop2 := maxListenCount(pop)
|
|
||||||
|
|
||||||
for i := range artists {
|
for i := range artists {
|
||||||
s := blendedScore(float64(artists[i].Score)/100.0, pop[artists[i].MBID], maxPop2)
|
s := blendedScore(float64(artists[i].Score)/100.0, pop[artists[i].MBID], maxPop)
|
||||||
artists[i].Score = int(s * 100)
|
score := int(s * 100)
|
||||||
|
|
||||||
|
if libraryMBIDs[artists[i].MBID] {
|
||||||
|
score += libraryScoreBonus
|
||||||
|
}
|
||||||
|
|
||||||
|
artists[i].Score = score
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -316,9 +316,15 @@ func (si *SearchIndex) GetPopularity(mbid string) int {
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetPopularityBatch returns popularity (listen count) for multiple
|
// PopularityBatchResult contains popularity and library status.
|
||||||
// MBIDs in a single query. Returns a map of MBID → popularity.
|
type PopularityBatchResult struct {
|
||||||
func (si *SearchIndex) GetPopularityBatch(mbids []string) map[string]int {
|
Popularity map[string]int
|
||||||
|
InLibrary map[string]bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPopularityBatch returns popularity (listen count) and library
|
||||||
|
// status for multiple MBIDs in a single query.
|
||||||
|
func (si *SearchIndex) GetPopularityBatch(mbids []string) *PopularityBatchResult {
|
||||||
if len(mbids) == 0 {
|
if len(mbids) == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -341,7 +347,10 @@ func (si *SearchIndex) GetPopularityBatch(mbids []string) map[string]int {
|
|||||||
|
|
||||||
defer func() { _ = rows.Close() }()
|
defer func() { _ = rows.Close() }()
|
||||||
|
|
||||||
result := make(map[string]int, len(mbids))
|
result := &PopularityBatchResult{
|
||||||
|
Popularity: make(map[string]int, len(mbids)),
|
||||||
|
InLibrary: make(map[string]bool),
|
||||||
|
}
|
||||||
|
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var mbid string
|
var mbid string
|
||||||
@@ -349,13 +358,13 @@ func (si *SearchIndex) GetPopularityBatch(mbids []string) map[string]int {
|
|||||||
var inLib int
|
var inLib int
|
||||||
|
|
||||||
if err := rows.Scan(&mbid, &pop, &inLib); err == nil {
|
if err := rows.Scan(&mbid, &pop, &inLib); err == nil {
|
||||||
existing, ok := result[mbid]
|
existing, ok := result.Popularity[mbid]
|
||||||
if !ok || pop > existing {
|
if !ok || pop > existing {
|
||||||
if inLib == 1 {
|
result.Popularity[mbid] = pop
|
||||||
pop += 10_000_000 //nolint:mnd // library bonus
|
}
|
||||||
}
|
|
||||||
|
|
||||||
result[mbid] = pop
|
if inLib == 1 {
|
||||||
|
result.InLibrary[mbid] = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user