From 3124c80a8f19c5365bf4e0afcec3855832f233b7 Mon Sep 17 00:00:00 2001 From: Caleb Allen Date: Mon, 30 Mar 2026 03:35:41 -0400 Subject: [PATCH] feat: backfill artist popularity from LB when index lacks data MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When the fast path (index ready) returns no popularity for most artists, a targeted LB ArtistPopularity POST fires for just the missing MBIDs. This handles searches like 'shannon' where MB returns artists not covered by the index (not sitewide top 100, not in library, not similar to library artists). Only fires when >50% of artists lack index data — if the index covered most results, the backfill is skipped. Single POST call, typically 10-30 MBIDs, goes through the LB rate limiter. After backfill, rerankArtists runs again with the combined popularity data, so Shannon Wright (766K listens) correctly outranks Shannon Hale (0 listens). --- backend/explore/explore.go | 65 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/backend/explore/explore.go b/backend/explore/explore.go index 4cda340..6248d5c 100644 --- a/backend/explore/explore.go +++ b/backend/explore/explore.go @@ -479,6 +479,12 @@ func (e *Service) Search(query string) (*MBSearchResult, error) { if indexReady { // Phase 2 (lite): rerank MB results using index popularity. e.boostWithIndexPopularity(&result) + + // If most artists lack index popularity, do a targeted LB + // lookup for just the artist MBIDs. This handles the case + // where a search returns artists not covered by the index + // (not sitewide popular, not in library, not similar). + e.backfillArtistPopularity(&result) } else { // Phase 2: LB popularity lookups (3 POST calls, rate-limited). // Use a tight deadline so a slow LB/MB doesn't stall the search. @@ -1038,6 +1044,65 @@ func (e *Service) boostWithIndexPopularity(result *MBSearchResult) { // entities in result and re-sorts each slice using a blended score // of MB text relevance + log-scaled popularity. Modifies result // in place. Failures are logged and degrade to MB-only ordering. +// backfillArtistPopularity does a targeted LB API lookup for +// artists that the index fast path couldn't provide popularity for. +// Only fires when a significant fraction of artists have unknown +// popularity. Single POST call with just the missing MBIDs. +func (e *Service) backfillArtistPopularity(result *MBSearchResult) { + if len(result.Artists) == 0 { + return + } + + // Collect MBIDs that have no popularity data. + var missing []string + + for _, a := range result.Artists { + if !a.HasPopularity && a.MBID != "" { + missing = append(missing, a.MBID) + } + } + + // Only backfill if most artists lack data. + if len(missing) < len(result.Artists)/2 { + return + } + + pop, err := e.lb.ArtistPopularity(e.ctx, missing) + if err != nil || len(pop) == 0 { + return + } + + // Build library set for the re-sort. + libCheck := e.libMBID.CheckMBIDs(missing) + libMBIDs := make(map[string]bool) + + for mbid, entityType := range libCheck { + if entityType == "artist" { + libMBIDs[mbid] = true + } + } + + // Merge into a combined pop map (index + backfill). + for i := range result.Artists { + a := &result.Artists[i] + if p, ok := pop[a.MBID]; ok { + a.HasPopularity = true + a.Popularity = p + } + } + + // Re-derive scores with the new popularity data. + allPop := make(map[string]int, len(result.Artists)) + + for _, a := range result.Artists { + if a.Popularity > 0 { + allPop[a.MBID] = a.Popularity + } + } + + rerankArtists(result.Artists, allPop, libMBIDs) +} + func (e *Service) boostWithPopularity(result *MBSearchResult) { // Collect MBIDs per entity type. artistMBIDs := make([]string, len(result.Artists))