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
+16 -4
View File
@@ -62,6 +62,18 @@ WHERE t.id <> $2
-- the determinism they promise; radio passes a fresh value per request and
-- keeps varying, which is what a radio should do.
-- Returns same shape as LoadRadioCandidates plus similarity_score column.
--
-- sim_score BANDS keep the tiers in order (rule 131, #6177). Similarity edges
-- are stored relative to the seed's best match, 0 to 1, so the arms are placed
-- in bands rather than compared raw:
-- tier 1 lb_similar 0.5 + 0.5 × edge (0.5, 1.0]
-- tier 2 similar_artists, coplay_artists 0.5 × edge [0, 0.5]
-- tag_overlap 0.5 × share of the seed's tags
-- likes_overlap 0.3, flat
-- tier 3 taste_overlap, random_fill 0
-- Without the bands, the seed's weaker LB edges fell below a shared genre tag
-- and the flat likes score, and the one arm that measures the seed lost to
-- the ones that guess.
WITH
seed_artist AS (
@@ -87,7 +99,7 @@ excluded_ids AS (
WHERE q.user_id = $1
),
lb_similar AS (
SELECT ts.track_b_id AS track_id, ts.score AS sim_score
SELECT ts.track_b_id AS track_id, 0.5 + 0.5 * ts.score AS sim_score
FROM track_similarity ts
WHERE ts.track_a_id = $2
AND ts.source = 'listenbrainz'
@@ -117,8 +129,8 @@ similar_artists AS (
),
tag_overlap AS (
SELECT t.id AS track_id,
(count(DISTINCT trim(g))::float8
/ GREATEST((SELECT count(*) FROM seed_tags), 1)) AS sim_score
0.5 * (count(DISTINCT trim(g))::float8
/ GREATEST((SELECT count(*) FROM seed_tags), 1)) AS sim_score
FROM tracks t
LEFT JOIN LATERAL regexp_split_to_table(coalesce(t.genre, ''), '[;,]') AS g_split(g) ON true
WHERE trim(g_split.g) IN (SELECT tag FROM seed_tags)
@@ -130,7 +142,7 @@ tag_overlap AS (
LIMIT $7
),
likes_overlap AS (
SELECT gl.track_id, 0.6::float8 AS sim_score
SELECT gl.track_id, 0.3::float8 AS sim_score
FROM general_likes gl
WHERE gl.user_id = $1
AND gl.track_id NOT IN (SELECT id FROM excluded_ids)