feat(taste): phase 2b — taste_overlap candidate arm (#796)
test-go / test (push) Successful in 37s
test-go / integration (push) Successful in 4m41s

2a re-ranks the existing pool by TasteMatch; this ensures taste-relevant tracks
ARE in the pool. Adds a 6th arm to LoadRadioCandidatesV2: in-library tracks by
the user's top positively-weighted taste-profile artists ($10 K, weight > 0,
deterministic weight-DESC,id order so it doesn't reintroduce same-day
nondeterminism). Pool-inclusion only (sim_score 0) — TasteMatch already scores
the fit. Empty for cold-start users (no profile).

- CandidateSourceLimits.TasteOverlap; default 20 (radio), 80 for For-You via
  systemForYouSourceLimits.
- You-might-like deliberately sets TasteOverlap=0: it surfaces NOT-actively-
  engaged artists, so flooding its pool with top-taste (mostly already-played)
  artists would just feed the read-time dedup.
- Test: positive-weight artist's track enters via the arm; negative-weight one
  is excluded (weight > 0). Existing pool tests unaffected (no profile seeded).

Deferred within 2b: profile-seeded For-You — marginal given the arm + TasteMatch
already inject taste broadly (top-played seed ≈ top-taste artist).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-12 00:05:50 -04:00
parent c7adf2c87a
commit 6c26ba807e
6 changed files with 112 additions and 5 deletions
+19 -2
View File
@@ -33,10 +33,14 @@ WHERE t.id <> $2
);
-- name: LoadRadioCandidatesV2 :many
-- M4c: similarity-driven candidate pool. 5-way UNION:
-- M4c: similarity-driven candidate pool. 6-way UNION:
-- $1 user_id, $2 seed_track_id, $3 recently_played_hours,
-- $4 exclude (uuid[]), $5 lb_similar K, $6 similar_artists K,
-- $7 tag_overlap K, $8 likes_overlap K, $9 random_fill K.
-- $7 tag_overlap K, $8 likes_overlap K, $9 random_fill K,
-- $10 taste_overlap K (#796 phase 2b — tracks by the user's top
-- positively-weighted taste-profile artists, so taste-relevant tracks
-- enter the pool even when the similarity/random arms miss them; scored
-- in Go via TasteMatch, so sim_score here is 0 pool-inclusion).
-- Returns same shape as LoadRadioCandidates plus similarity_score column.
WITH
@@ -109,6 +113,17 @@ likes_overlap AS (
ORDER BY random()
LIMIT $8
),
taste_overlap AS (
SELECT t.id AS track_id, 0.0::float8 AS sim_score
FROM taste_profile_artists tpa
JOIN tracks t ON t.artist_id = tpa.artist_id
WHERE tpa.user_id = $1
AND tpa.weight > 0
AND t.id NOT IN (SELECT id FROM excluded_ids)
AND t.id <> $2
ORDER BY tpa.weight DESC, t.id
LIMIT $10
),
random_fill AS (
SELECT t.id AS track_id, 0.0::float8 AS sim_score
FROM tracks t
@@ -119,6 +134,7 @@ random_fill AS (
UNION SELECT track_id FROM similar_artists
UNION SELECT track_id FROM tag_overlap
UNION SELECT track_id FROM likes_overlap
UNION SELECT track_id FROM taste_overlap
)
ORDER BY random()
LIMIT $9
@@ -135,6 +151,7 @@ FROM (
UNION ALL SELECT track_id, sim_score FROM similar_artists
UNION ALL SELECT track_id, sim_score FROM tag_overlap
UNION ALL SELECT track_id, sim_score FROM likes_overlap
UNION ALL SELECT track_id, sim_score FROM taste_overlap
UNION ALL SELECT track_id, sim_score FROM random_fill
) u
JOIN tracks t ON t.id = u.track_id