feat: rerank MB results with index popularity, increase popularity weight
Two changes: 1. Rerank MB results using index popularity (no API calls): When the index is ready, boostWithIndexPopularity looks up each MB result's MBID in the local index to get cached listen counts, then reranks using the same blended score formula. This was previously skipped entirely for speed, leaving MB results sorted by text relevance only — obscure exact matches beat popular partial matches. 2. Increase popularity weight across both scoring systems: - Blended score: 60% popularity / 40% relevance (was 40/60) - FTS5 index: ln(pop+1) * 1.5 factor (was 0.5) Result: 'flatbush' → Flatbush Zombies (97) beats 'Flatbush' nobody (61). Popular artists with partial name matches now reliably outrank obscure exact matches.
This commit is contained in:
@@ -331,12 +331,13 @@ func (e *Service) Search(query string) (*MBSearchResult, error) {
|
||||
"recordings", len(result.Recordings),
|
||||
)
|
||||
|
||||
// Phases 2+3 are expensive (3+ LB API calls through the rate
|
||||
// limiter). Skip them when the local index is ready — it
|
||||
// already carries popularity data and covers the cross-reference
|
||||
// use case. Only run as fallback during first launch before
|
||||
// the index is built.
|
||||
if !e.index.IsReady() {
|
||||
// Phases 2+3: when the index is ready, use cached popularity
|
||||
// from the index to rerank MB results (no API calls).
|
||||
// When the index isn't ready, fall back to live LB API calls.
|
||||
if e.index.IsReady() {
|
||||
// Phase 2 (lite): rerank MB results using index popularity.
|
||||
e.boostWithIndexPopularity(&result)
|
||||
} else {
|
||||
// Phase 2: LB popularity lookups (3 POST calls, rate-limited).
|
||||
e.boostWithPopularity(&result)
|
||||
|
||||
@@ -708,8 +709,8 @@ func filterAndCap(result *MBSearchResult) {
|
||||
|
||||
const (
|
||||
// Blending weights for final score.
|
||||
relevanceWeight = 0.6
|
||||
popularityWeight = 0.4
|
||||
relevanceWeight = 0.4
|
||||
popularityWeight = 0.6
|
||||
|
||||
// mbSearchLimit is passed to each MB search call. Slightly
|
||||
// larger than maxResults to allow headroom for filtering.
|
||||
@@ -723,6 +724,45 @@ const (
|
||||
minBlendedScore = 25
|
||||
)
|
||||
|
||||
// boostWithIndexPopularity reranks MB search results using
|
||||
// popularity data from the local search index. No API calls —
|
||||
// just SQLite lookups. This is the fast path used when the index
|
||||
// is ready.
|
||||
func (e *Service) boostWithIndexPopularity(result *MBSearchResult) {
|
||||
// Look up popularity for all artist MBIDs.
|
||||
artistPop := make(map[string]int, len(result.Artists))
|
||||
|
||||
for _, a := range result.Artists {
|
||||
if pop := e.index.GetPopularity(a.MBID); pop > 0 {
|
||||
artistPop[a.MBID] = pop
|
||||
}
|
||||
}
|
||||
|
||||
rerankArtists(result.Artists, artistPop)
|
||||
|
||||
// Look up popularity for release groups.
|
||||
rgPop := make(map[string]int, len(result.ReleaseGroups))
|
||||
|
||||
for _, rg := range result.ReleaseGroups {
|
||||
if pop := e.index.GetPopularity(rg.MBID); pop > 0 {
|
||||
rgPop[rg.MBID] = pop
|
||||
}
|
||||
}
|
||||
|
||||
rerankReleaseGroups(result.ReleaseGroups, rgPop)
|
||||
|
||||
// Look up popularity for recordings.
|
||||
recPop := make(map[string]int, len(result.Recordings))
|
||||
|
||||
for _, r := range result.Recordings {
|
||||
if pop := e.index.GetPopularity(r.MBID); pop > 0 {
|
||||
recPop[r.MBID] = pop
|
||||
}
|
||||
}
|
||||
|
||||
rerankRecordings(result.Recordings, recPop)
|
||||
}
|
||||
|
||||
// boostWithPopularity fetches ListenBrainz listen counts for all
|
||||
// entities in result and re-sorts each slice using a blended score
|
||||
// of MB text relevance + log-scaled popularity. Modifies result
|
||||
|
||||
@@ -190,6 +190,33 @@ func (si *SearchIndex) IsReady() bool {
|
||||
return si.ready
|
||||
}
|
||||
|
||||
// GetPopularity returns the cached popularity (listen count) for
|
||||
// the given MBID from the local index. Returns 0 if not found.
|
||||
func (si *SearchIndex) GetPopularity(mbid string) int {
|
||||
if mbid == "" {
|
||||
return 0
|
||||
}
|
||||
|
||||
rows, err := si.db.QueryContext(
|
||||
"SELECT popularity FROM explore_index WHERE mbid = ? LIMIT 1",
|
||||
mbid,
|
||||
)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
defer func() { _ = rows.Close() }()
|
||||
|
||||
if rows.Next() {
|
||||
var pop int
|
||||
if err := rows.Scan(&pop); err == nil {
|
||||
return pop
|
||||
}
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
// AddFromCache inserts entries from a cached discography browse
|
||||
// into the search index (Tier 5: organic growth). Called when a
|
||||
// user views an artist page and the discography is fetched.
|
||||
@@ -254,7 +281,7 @@ func (si *SearchIndex) Search(query string, limit int) []SearchIndexResult {
|
||||
FROM explore_index i
|
||||
JOIN explore_index_fts f ON f.rowid = i.id
|
||||
WHERE explore_index_fts MATCH ?
|
||||
ORDER BY bm25(explore_index_fts, 3.0, 1.0, 0.5) - (ln(i.popularity + 1) * 0.5)
|
||||
ORDER BY bm25(explore_index_fts, 3.0, 1.0, 0.5) - (ln(i.popularity + 1) * 1.5)
|
||||
LIMIT ?
|
||||
`, ftsQuery, limit)
|
||||
if err != nil {
|
||||
|
||||
Reference in New Issue
Block a user