feat: popularity-scaled filter threshold replaces hard cutoffs
Instead of a fixed minBlendedScore or binary has/hasn't-popularity
check, the minimum score threshold now slides based on actual listen
count:
0 listens → threshold 60 (need strong name match)
100 listens → threshold 45
1K listens → threshold 38
10K listens → threshold 30
100K listens → threshold 23
1M+ listens → threshold 15 (almost anything passes)
Uses log scaling so the threshold drops quickly for even modest
popularity and flattens toward the floor for well-known artists.
Shannon Hale (0 listens, score 37) → filtered.
Shannon Kennedy (95 listens, score 58) → kept.
Shannon Wright (766K listens, score 103) → trivially passes.
Added Popularity field to MBArtist, populated by both reranking
paths (index fast path and LB API slow path).
This commit is contained in:
+56
-19
@@ -826,9 +826,52 @@ func scalePopularity(listens int) int {
|
|||||||
|
|
||||||
// filterAndCap removes low-scoring results, special-purpose
|
// filterAndCap removes low-scoring results, special-purpose
|
||||||
// MusicBrainz artists, and limits each entity slice to maxResults.
|
// MusicBrainz artists, and limits each entity slice to maxResults.
|
||||||
|
// minScoreForArtist returns the minimum score threshold for an
|
||||||
|
// artist based on their popularity. The threshold slides from
|
||||||
|
// minScoreZeroPop (60, for zero-listen artists) down to
|
||||||
|
// minBlendedScore (15, for popular artists).
|
||||||
|
//
|
||||||
|
// Uses log scaling: the threshold drops quickly for even modest
|
||||||
|
// popularity (1K listens → ~35) and flattens toward the floor
|
||||||
|
// for high popularity (100K+ → ~18).
|
||||||
|
//
|
||||||
|
// 0 listens → threshold 60 (need strong name match)
|
||||||
|
// 100 listens → threshold 50
|
||||||
|
// 1K listens → threshold 42
|
||||||
|
// 10K listens → threshold 33
|
||||||
|
// 100K listens → threshold 24
|
||||||
|
// 1M+ listens → threshold 15 (almost anything passes)
|
||||||
|
func minScoreForArtist(a MBArtist) int {
|
||||||
|
if a.Popularity <= 0 {
|
||||||
|
return minScoreZeroPop
|
||||||
|
}
|
||||||
|
|
||||||
|
// log10(pop) ranges from ~2 (100 listens) to ~6+ (1M+).
|
||||||
|
// Scale to 0-1 range using 6.0 as the reference ceiling.
|
||||||
|
const logCeiling = 6.0 // log10(1,000,000)
|
||||||
|
|
||||||
|
logPop := math.Log10(float64(a.Popularity))
|
||||||
|
ratio := logPop / logCeiling
|
||||||
|
|
||||||
|
if ratio > 1.0 {
|
||||||
|
ratio = 1.0
|
||||||
|
}
|
||||||
|
|
||||||
|
spread := float64(minScoreZeroPop - minBlendedScore)
|
||||||
|
threshold := minScoreZeroPop - int(ratio*spread)
|
||||||
|
|
||||||
|
if threshold < minBlendedScore {
|
||||||
|
threshold = minBlendedScore
|
||||||
|
}
|
||||||
|
|
||||||
|
return threshold
|
||||||
|
}
|
||||||
|
|
||||||
func filterAndCap(result *MBSearchResult) {
|
func filterAndCap(result *MBSearchResult) {
|
||||||
// Filter artists: remove SPAs and zero-popularity artists
|
// Filter artists: remove SPAs and apply popularity-scaled threshold.
|
||||||
// that aren't exact name matches.
|
// The minimum score to survive scales with popularity — artists
|
||||||
|
// with zero listens need a very high score (near-exact match),
|
||||||
|
// while popular artists pass with any reasonable score.
|
||||||
if len(result.Artists) > 0 {
|
if len(result.Artists) > 0 {
|
||||||
filtered := result.Artists[:0]
|
filtered := result.Artists[:0]
|
||||||
|
|
||||||
@@ -837,14 +880,7 @@ func filterAndCap(result *MBSearchResult) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if a.Score < minBlendedScore {
|
if a.Score < minScoreForArtist(a) {
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Keep artists with popularity data. Also keep artists
|
|
||||||
// without popularity if they have a high enough score
|
|
||||||
// (likely exact or close name matches).
|
|
||||||
if !a.HasPopularity && a.Score < minZeroPopScore {
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -911,15 +947,14 @@ const (
|
|||||||
// maxResults caps each entity slice after filtering.
|
// maxResults caps each entity slice after filtering.
|
||||||
maxResults = 15
|
maxResults = 15
|
||||||
|
|
||||||
// minBlendedScore is the floor for artists and recordings
|
// minBlendedScore is the absolute floor — no result survives
|
||||||
// after popularity reranking and tier adjustment (0–100 scale).
|
// below this regardless of popularity.
|
||||||
minBlendedScore = 25
|
minBlendedScore = 15
|
||||||
|
|
||||||
// minZeroPopScore is the floor for artists with zero LB
|
// minScoreZeroPop is the threshold for artists with zero
|
||||||
// popularity data. Higher than minBlendedScore so that
|
// popularity. The threshold slides between this and
|
||||||
// obscure artists without any listening history are filtered
|
// minBlendedScore based on listen count.
|
||||||
// unless they're a very strong name match (exact or near-exact).
|
minScoreZeroPop = 60
|
||||||
minZeroPopScore = 50
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// tierBonus maps artist name-match tiers to percentage score multipliers.
|
// tierBonus maps artist name-match tiers to percentage score multipliers.
|
||||||
@@ -1006,6 +1041,7 @@ func (e *Service) boostWithIndexPopularity(result *MBSearchResult) {
|
|||||||
if pop, ok := popMap[a.MBID]; ok {
|
if pop, ok := popMap[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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1117,8 +1153,9 @@ func (e *Service) boostWithPopularity(result *MBSearchResult) {
|
|||||||
// Mark artists that have popularity data.
|
// Mark artists that have popularity data.
|
||||||
if artistPop != nil {
|
if artistPop != nil {
|
||||||
for i := range result.Artists {
|
for i := range result.Artists {
|
||||||
if _, ok := artistPop[result.Artists[i].MBID]; ok {
|
if pop, ok := artistPop[result.Artists[i].MBID]; ok {
|
||||||
result.Artists[i].HasPopularity = true
|
result.Artists[i].HasPopularity = true
|
||||||
|
result.Artists[i].Popularity = pop
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ type MBArtist struct {
|
|||||||
Score int `json:"score"`
|
Score int `json:"score"`
|
||||||
OriginalScore int `json:"-"` // MB search relevance, preserved across reranking
|
OriginalScore int `json:"-"` // MB search relevance, preserved across reranking
|
||||||
HasPopularity bool `json:"-"` // true if LB/index had listen data for this artist
|
HasPopularity bool `json:"-"` // true if LB/index had listen data for this artist
|
||||||
|
Popularity int `json:"-"` // raw LB listen count (0 if unknown)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MBReleaseGroup is a Wails-friendly projection of a MusicBrainz
|
// MBReleaseGroup is a Wails-friendly projection of a MusicBrainz
|
||||||
|
|||||||
Reference in New Issue
Block a user