From b8e3019654d40298f1f96675b87f78bcb8a9f2c3 Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Wed, 29 Apr 2026 08:43:42 -0400 Subject: [PATCH] fix(db): split seed_info CTE so similar-artist works without seed genre The combined seed_info CTE filtered the seed out when its genre was NULL/empty. Restored the spec's two-CTE design (seed_artist + seed_tags) so similar-artist matching works regardless of the seed's genre coverage. Drops the genre workaround from the SimilarArtist test. --- internal/db/dbq/recommendation.sql.go | 18 +++++++++++------- internal/db/queries/recommendation.sql | 18 +++++++++++------- internal/recommendation/candidates_v2_test.go | 5 ----- 3 files changed, 22 insertions(+), 19 deletions(-) diff --git a/internal/db/dbq/recommendation.sql.go b/internal/db/dbq/recommendation.sql.go index 146136a1..7da85f13 100644 --- a/internal/db/dbq/recommendation.sql.go +++ b/internal/db/dbq/recommendation.sql.go @@ -100,9 +100,13 @@ func (q *Queries) LoadRadioCandidates(ctx context.Context, arg LoadRadioCandidat const loadRadioCandidatesV2 = `-- name: LoadRadioCandidatesV2 :many WITH -seed_info AS ( - SELECT t.artist_id, - trim(g)::text AS tag +seed_artist AS ( + SELECT artist_id + FROM tracks + WHERE tracks.id = $2 +), +seed_tags AS ( + SELECT trim(g) AS tag FROM tracks t LEFT JOIN LATERAL regexp_split_to_table(coalesce(t.genre, ''), '[;,]') AS g(tag) ON true WHERE t.id = $2 AND trim(g) <> '' @@ -127,7 +131,7 @@ similar_artists AS ( SELECT t.id AS track_id, asim.score * 0.5 AS sim_score FROM artist_similarity asim JOIN tracks t ON t.artist_id = asim.artist_b_id - JOIN (SELECT DISTINCT artist_id FROM seed_info) sa ON asim.artist_a_id = sa.artist_id + JOIN seed_artist sa ON asim.artist_a_id = sa.artist_id WHERE asim.source = 'listenbrainz' AND t.id NOT IN (SELECT id FROM excluded_ids) ORDER BY asim.score DESC, random() @@ -136,10 +140,10 @@ similar_artists AS ( tag_overlap AS ( SELECT t.id AS track_id, (count(DISTINCT trim(g))::float8 - / GREATEST((SELECT count(DISTINCT tag) FROM seed_info), 1)) AS sim_score + / 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_info) + WHERE trim(g_split.g) IN (SELECT tag FROM seed_tags) AND t.id NOT IN (SELECT id FROM excluded_ids) AND t.id <> $2 GROUP BY t.id @@ -156,7 +160,7 @@ likes_overlap AS ( SELECT 1 FROM tracks t LEFT JOIN LATERAL regexp_split_to_table(coalesce(t.genre, ''), '[;,]') AS g_overlap(g) ON true WHERE t.id = gl.track_id - AND trim(g_overlap.g) IN (SELECT tag FROM seed_info) + AND trim(g_overlap.g) IN (SELECT tag FROM seed_tags) ) ORDER BY random() LIMIT $8 diff --git a/internal/db/queries/recommendation.sql b/internal/db/queries/recommendation.sql index 204b83cb..10e81601 100644 --- a/internal/db/queries/recommendation.sql +++ b/internal/db/queries/recommendation.sql @@ -36,9 +36,13 @@ WHERE t.id <> $2 -- Returns same shape as LoadRadioCandidates plus similarity_score column. WITH -seed_info AS ( - SELECT t.artist_id, - trim(g)::text AS tag +seed_artist AS ( + SELECT artist_id + FROM tracks + WHERE tracks.id = $2 +), +seed_tags AS ( + SELECT trim(g) AS tag FROM tracks t LEFT JOIN LATERAL regexp_split_to_table(coalesce(t.genre, ''), '[;,]') AS g(tag) ON true WHERE t.id = $2 AND trim(g) <> '' @@ -63,7 +67,7 @@ similar_artists AS ( SELECT t.id AS track_id, asim.score * 0.5 AS sim_score FROM artist_similarity asim JOIN tracks t ON t.artist_id = asim.artist_b_id - JOIN (SELECT DISTINCT artist_id FROM seed_info) sa ON asim.artist_a_id = sa.artist_id + JOIN seed_artist sa ON asim.artist_a_id = sa.artist_id WHERE asim.source = 'listenbrainz' AND t.id NOT IN (SELECT id FROM excluded_ids) ORDER BY asim.score DESC, random() @@ -72,10 +76,10 @@ similar_artists AS ( tag_overlap AS ( SELECT t.id AS track_id, (count(DISTINCT trim(g))::float8 - / GREATEST((SELECT count(DISTINCT tag) FROM seed_info), 1)) AS sim_score + / 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_info) + WHERE trim(g_split.g) IN (SELECT tag FROM seed_tags) AND t.id NOT IN (SELECT id FROM excluded_ids) AND t.id <> $2 GROUP BY t.id @@ -92,7 +96,7 @@ likes_overlap AS ( SELECT 1 FROM tracks t LEFT JOIN LATERAL regexp_split_to_table(coalesce(t.genre, ''), '[;,]') AS g_overlap(g) ON true WHERE t.id = gl.track_id - AND trim(g_overlap.g) IN (SELECT tag FROM seed_info) + AND trim(g_overlap.g) IN (SELECT tag FROM seed_tags) ) ORDER BY random() LIMIT $8 diff --git a/internal/recommendation/candidates_v2_test.go b/internal/recommendation/candidates_v2_test.go index c04b1b2e..294b0e58 100644 --- a/internal/recommendation/candidates_v2_test.go +++ b/internal/recommendation/candidates_v2_test.go @@ -73,11 +73,6 @@ func TestLoadCandidatesFromSimilarity_LBSimilarSourceContributes(t *testing.T) { func TestLoadCandidatesFromSimilarity_SimilarArtistTracksContribute(t *testing.T) { f := newFixture(t, 1) // creates 1 artist + 1 album + 1 track (the seed) seed := f.tracks[0] - // seed_info CTE only emits artist_id rows when genre is non-NULL (it - // uses a LATERAL regexp_split_to_table join filtered by trim(g) <> ''). - // Give the seed a genre so that seed_info is populated and the - // similar_artists JOIN can find artist_a_id = seed.ArtistID. - helperSetTrackGenre(t, f, seed.ID, "Rock") // Add a SECOND artist + track in that artist; relate the two artists via artist_similarity. otherArtist, _ := f.q.UpsertArtist(context.Background(), dbq.UpsertArtistParams{Name: "OtherArtist", SortName: "OtherArtist"}) otherAlbum, _ := f.q.UpsertAlbum(context.Background(), dbq.UpsertAlbumParams{Title: "OtherAlbum", SortTitle: "OtherAlbum", ArtistID: otherArtist.ID})