feat: score threshold + server-side result caps

Drop artists and recordings with blended score < 25 after popularity
reranking. Cap each entity slice to 15 server-side. Request 20 from
MB to allow filtering headroom. Reduces payload size and noise.
This commit is contained in:
2026-03-24 22:42:54 -04:00
parent 5f8f6d6a26
commit 1e95ddee68
+67 -6
View File
@@ -55,17 +55,17 @@ func (e *Service) SetContext(ctx context.Context) {
// SearchArtists queries MusicBrainz for artists matching the query.
func (e *Service) SearchArtists(query string) ([]MBArtist, error) {
return e.mb.SearchArtists(e.ctx, query, 0)
return e.mb.SearchArtists(e.ctx, query, mbSearchLimit)
}
// SearchReleaseGroups queries MusicBrainz for release groups matching the query.
func (e *Service) SearchReleaseGroups(query string) ([]MBReleaseGroup, error) {
return e.mb.SearchReleaseGroups(e.ctx, query, 0)
return e.mb.SearchReleaseGroups(e.ctx, query, mbSearchLimit)
}
// SearchRecordings queries MusicBrainz for recordings matching the query.
func (e *Service) SearchRecordings(query string) ([]MBRecording, error) {
return e.mb.SearchRecordings(e.ctx, query, 0)
return e.mb.SearchRecordings(e.ctx, query, mbSearchLimit)
}
// ---------------------------------------------------------------------------
@@ -155,7 +155,7 @@ func (e *Service) Search(query string) (*MBSearchResult, error) {
{
name: "artists",
fn: func() {
artists, err := e.mb.SearchArtists(e.ctx, query, 0)
artists, err := e.mb.SearchArtists(e.ctx, query, mbSearchLimit)
if err != nil {
e.logger.Warn("search sub-call failed",
"entity", "artists",
@@ -174,7 +174,7 @@ func (e *Service) Search(query string) (*MBSearchResult, error) {
{
name: "releaseGroups",
fn: func() {
rgs, err := e.mb.SearchReleaseGroups(e.ctx, query, 0)
rgs, err := e.mb.SearchReleaseGroups(e.ctx, query, mbSearchLimit)
if err != nil {
e.logger.Warn("search sub-call failed",
"entity", "releaseGroups",
@@ -193,7 +193,7 @@ func (e *Service) Search(query string) (*MBSearchResult, error) {
{
name: "recordings",
fn: func() {
recs, err := e.mb.SearchRecordings(e.ctx, query, 0)
recs, err := e.mb.SearchRecordings(e.ctx, query, mbSearchLimit)
if err != nil {
e.logger.Warn("search sub-call failed",
"entity", "recordings",
@@ -235,6 +235,9 @@ func (e *Service) Search(query string) (*MBSearchResult, error) {
// overlap on different rate-limiter tokens.
e.boostWithPopularity(&result)
// Phase 3: filter low-scoring results and cap counts.
filterAndCap(&result)
e.logger.Info("search completed",
"query", query,
"artists", len(result.Artists),
@@ -245,6 +248,53 @@ func (e *Service) Search(query string) (*MBSearchResult, error) {
return &result, nil
}
// ---------------------------------------------------------------------------
// Filtering and capping
// ---------------------------------------------------------------------------
// filterAndCap removes low-scoring results and limits each entity
// slice to maxResults entries.
func filterAndCap(result *MBSearchResult) {
// Filter artists by minimum blended score.
if len(result.Artists) > 0 {
filtered := result.Artists[:0]
for _, a := range result.Artists {
if a.Score >= minBlendedScore {
filtered = append(filtered, a)
}
}
result.Artists = filtered
}
// Filter recordings by minimum blended score.
if len(result.Recordings) > 0 {
filtered := result.Recordings[:0]
for _, r := range result.Recordings {
if r.Score >= minBlendedScore {
filtered = append(filtered, r)
}
}
result.Recordings = filtered
}
// Cap each slice.
if len(result.Artists) > maxResults {
result.Artists = result.Artists[:maxResults]
}
if len(result.ReleaseGroups) > maxResults {
result.ReleaseGroups = result.ReleaseGroups[:maxResults]
}
if len(result.Recordings) > maxResults {
result.Recordings = result.Recordings[:maxResults]
}
}
// ---------------------------------------------------------------------------
// Popularity-boosted reranking
// ---------------------------------------------------------------------------
@@ -253,6 +303,17 @@ const (
// Blending weights for final score.
relevanceWeight = 0.6
popularityWeight = 0.4
// mbSearchLimit is passed to each MB search call. Slightly
// larger than maxResults to allow headroom for filtering.
mbSearchLimit = 20
// maxResults caps each entity slice after filtering.
maxResults = 15
// minBlendedScore is the floor for artists and recordings
// after popularity reranking (0100 scale).
minBlendedScore = 25
)
// boostWithPopularity fetches ListenBrainz listen counts for all