fix: ListenBrainz similarity is stored relative to the seed's best, and the candidate arms keep tier order (#6177)
release / govulncheck (push) Successful in 15s
release / web (push) Successful in 1m23s
release / go (push) Successful in 1m36s
release / integration (push) Successful in 5m4s
release / android (push) Successful in 5m5s
release / Build signed APK (releases and dev) (push) Successful in 5m18s
release / Attach APK to the Release (tag releases only) (push) Skipped
release / Build + push container image (push) Successful in 30s
release / Verify release artifacts (tag releases only) (push) Skipped

LB's similarity scores are counts (16 to 1445 for recordings, to 10,875
for artists), and they reached a scorer that reads similarity as 0 to 1.
One edge added 64 to 5,800 points under Songs-like, so likes, skips and
recency stopped mattering for any candidate with an edge.

- ResolveListenBrainzTrackEdges divides each edge by the seed's best
  score in its whole cached answer. The cache keeps the raw answer.
- The worker stores matched and unmatched artist rows over the seed's
  top artist.
- Migration 0096 rescales the rows already stored.
- The candidate arms hold sim_score bands so tier 1 stays above tier 2:
  lb_similar 0.5 + 0.5 x edge, tag_overlap halved, likes_overlap 0.3.
  Otherwise a seed's weaker LB edges fell below a shared genre tag.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-21 10:56:26 -04:00
co-authored by Claude Opus 5.5
parent 46e1dd6c9c
commit e44025435e
11 changed files with 216 additions and 45 deletions
+9 -8
View File
@@ -127,8 +127,9 @@ func DefaultCandidateSourceLimits() CandidateSourceLimits {
// tier 3 taste_overlap, random_fill
// seed-independent. The floor, and nothing more.
//
// The tiering is enforced by SCORE rather than by a fallback ladder: tier-3
// arms carry sim_score 0, so under SongsLike weights (SimilarityWeight 4.0,
// The tiering is enforced by SCORE rather than by a fallback ladder: each tier
// holds its own sim_score band (recommendation.sql; tier 1 above 0.5, tier 2
// at or below it, #6177), and tier-3 arms carry sim_score 0, so under SongsLike weights (SimilarityWeight 4.0,
// everything seed-independent demoted) they rank below any real match and
// surface only when tiers 1–2 cannot fill the mix. That is the rule's
// "fill from tier 1 first, reach down only when a tier cannot fill".
@@ -159,19 +160,19 @@ func DefaultCandidateSourceLimits() CandidateSourceLimits {
// surface only when the closer tiers cannot fill the mix.
//
// likes_overlap is cut hardest of the tier-2 arms for a specific reason: its
// SQL assigns a FLAT 0.6 sim_score (recommendation.sql) rather than measuring
// SQL assigns a FLAT 0.3 sim_score (recommendation.sql) rather than measuring
// anything. It is a collaborative signal wearing similarity's clothes, and a
// raised SimilarityWeight amplifies it — if real ListenBrainz scores commonly
// land below 0.6 it would outrank genuine matches. Halved pending the
// fill-rate measurement in #3879; the honest fix is to stop it claiming a
// similarity score it never computed.
// raised SimilarityWeight amplifies it. The tier bands keep it below every
// ListenBrainz match (#6177); halved pending the fill-rate measurement in
// #3879, and the honest fix is still to stop it claiming a similarity score
// it never computed.
func SongsLikeCandidateSourceLimits() CandidateSourceLimits {
return CandidateSourceLimits{
LBSimilar: 60, // tier 1 — doubled; the only arm that measures the seed
SimilarArtist: 40, // tier 2 — raised; growing is always safe
TagOverlap: 20, // tier 2
UserCoplay: 20, // tier 2
LikesOverlap: 10, // tier 2, halved — flat 0.6 sim_score, see above
LikesOverlap: 10, // tier 2, halved — flat 0.3 sim_score, see above
TasteOverlap: 10, // tier 3 floor — halved, not removed
RandomFill: 10, // tier 3 floor — cut hard, never to zero
}