diff --git a/backend/explore/explore.go b/backend/explore/explore.go index 9a91ad3..802d790 100644 --- a/backend/explore/explore.go +++ b/backend/explore/explore.go @@ -933,47 +933,56 @@ var mbSpecialPurposeArtists = map[string]bool{ // 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. - // Give a large bonus to library artists so they rank first. - artistPop := make(map[string]int, len(result.Artists)) + // Collect all MBIDs across all entity types. + allMBIDs := make([]string, 0, + len(result.Artists)+len(result.ReleaseGroups)+len(result.Recordings)) for _, a := range result.Artists { - pop := e.index.GetPopularity(a.MBID) - - // Library artists get a massive popularity bonus. - if e.index.IsInLibrary(a.MBID) { - pop += 10_000_000 //nolint:mnd + if a.MBID != "" { + allMBIDs = append(allMBIDs, a.MBID) } + } - if pop > 0 { + for _, rg := range result.ReleaseGroups { + if rg.MBID != "" { + allMBIDs = append(allMBIDs, rg.MBID) + } + } + + for _, r := range result.Recordings { + if r.MBID != "" { + allMBIDs = append(allMBIDs, r.MBID) + } + } + + // Single batch query for all popularity + in_library data. + popMap := e.index.GetPopularityBatch(allMBIDs) + if popMap == nil { + return + } + + // Build per-entity maps from the batch result. + artistPop := make(map[string]int, len(result.Artists)) + for _, a := range result.Artists { + if pop, ok := popMap[a.MBID]; ok { 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 { - pop := e.index.GetPopularity(rg.MBID) - - if e.index.IsInLibrary(rg.MBID) { - pop += 10_000_000 //nolint:mnd - } - - if pop > 0 { + if pop, ok := popMap[rg.MBID]; ok { 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 { + if pop, ok := popMap[r.MBID]; ok { recPop[r.MBID] = pop } } diff --git a/backend/explore/searchindex.go b/backend/explore/searchindex.go index e402c73..c8e7008 100644 --- a/backend/explore/searchindex.go +++ b/backend/explore/searchindex.go @@ -316,6 +316,53 @@ func (si *SearchIndex) GetPopularity(mbid string) int { return 0 } +// GetPopularityBatch returns popularity (listen count) for multiple +// MBIDs in a single query. Returns a map of MBID → popularity. +func (si *SearchIndex) GetPopularityBatch(mbids []string) map[string]int { + if len(mbids) == 0 { + return nil + } + + placeholders := make([]string, len(mbids)) + args := make([]any, len(mbids)) + + for i, m := range mbids { + placeholders[i] = "?" + args[i] = m + } + + query := "SELECT mbid, popularity, in_library FROM explore_index WHERE mbid IN (" + + strings.Join(placeholders, ",") + ")" + + rows, err := si.db.QueryContext(query, args...) + if err != nil { + return nil + } + + defer func() { _ = rows.Close() }() + + result := make(map[string]int, len(mbids)) + + for rows.Next() { + var mbid string + var pop int + var inLib int + + if err := rows.Scan(&mbid, &pop, &inLib); err == nil { + existing, ok := result[mbid] + if !ok || pop > existing { + if inLib == 1 { + pop += 10_000_000 //nolint:mnd // library bonus + } + + result[mbid] = pop + } + } + } + + return result +} + // IsInLibrary returns whether the given MBID is marked as in the // user's local library in the search index. func (si *SearchIndex) IsInLibrary(mbid string) bool {