From b4801c2dd3464d6709cf11678f8a4e6af97aee87 Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Wed, 13 May 2026 10:04:39 -0400 Subject: [PATCH] refactor(playlists): SQL queries return top-5 candidate seeds For-You + Songs-Like-X seed selection moves to Go-side daily rotation (next commit). The SQL change just widens the candidate pool: top-5 played tracks instead of single top played track; top-5 artists instead of top-3. Co-Authored-By: Claude Opus 4.7 (1M context) --- internal/db/dbq/system_playlists.sql.go | 42 +++++++++++++++++------- internal/db/queries/system_playlists.sql | 19 +++++++---- 2 files changed, 44 insertions(+), 17 deletions(-) diff --git a/internal/db/dbq/system_playlists.sql.go b/internal/db/dbq/system_playlists.sql.go index 97a8fde8..1c2a3d4f 100644 --- a/internal/db/dbq/system_playlists.sql.go +++ b/internal/db/dbq/system_playlists.sql.go @@ -308,7 +308,7 @@ SELECT p.artist_id, FROM plays p LEFT JOIN liked l ON l.artist_id = p.artist_id ORDER BY score DESC, p.artist_id - LIMIT 3 + LIMIT 5 ` type PickSeedArtistsRow struct { @@ -316,7 +316,10 @@ type PickSeedArtistsRow struct { Score int64 } -// Top-3 most-engaged distinct artists in the user's last 7 days. +// Top-5 most-engaged distinct artist candidates in the user's last 7 +// days. The Go-side picker (pickSeedArtistsForDay) shuffles these +// daily-deterministically and takes the first 3 so the set of +// "Songs like X" mixes rotates day-to-day. // Score = unskipped-play count + 5 if user has liked the artist. func (q *Queries) PickSeedArtists(ctx context.Context, userID pgtype.UUID) ([]PickSeedArtistsRow, error) { rows, err := q.db.Query(ctx, pickSeedArtists, userID) @@ -408,7 +411,7 @@ func (q *Queries) PickTopPlayedTrackForArtistByUser(ctx context.Context, arg Pic return id, err } -const pickTopPlayedTrackForUser = `-- name: PickTopPlayedTrackForUser :one +const pickTopPlayedTracksForUser = `-- name: PickTopPlayedTracksForUser :many SELECT t.id FROM play_events pe JOIN tracks t ON t.id = pe.track_id @@ -417,16 +420,33 @@ SELECT t.id AND pe.was_skipped = false GROUP BY t.id ORDER BY COUNT(*) DESC, t.id - LIMIT 1 + LIMIT 5 ` -// For-You seed selection. Returns the user's most-played non-skipped -// track in the last 7 days; tie-break by track_id for determinism. -func (q *Queries) PickTopPlayedTrackForUser(ctx context.Context, userID pgtype.UUID) (pgtype.UUID, error) { - row := q.db.QueryRow(ctx, pickTopPlayedTrackForUser, userID) - var id pgtype.UUID - err := row.Scan(&id) - return id, err +// For-You candidate seeds. Returns the user's top-5 most-played +// non-skipped tracks in the last 7 days; tie-break by track_id for +// determinism. The Go-side picker (pickForYouSeedForDay) chooses one +// of the returned rows as today's seed via userIDHash so the +// candidate pool rotates day-to-day while staying stable within a +// day. +func (q *Queries) PickTopPlayedTracksForUser(ctx context.Context, userID pgtype.UUID) ([]pgtype.UUID, error) { + rows, err := q.db.Query(ctx, pickTopPlayedTracksForUser, userID) + if err != nil { + return nil, err + } + defer rows.Close() + var items []pgtype.UUID + for rows.Next() { + var id pgtype.UUID + if err := rows.Scan(&id); err != nil { + return nil, err + } + items = append(items, id) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil } const tryClaimSystemPlaylistRun = `-- name: TryClaimSystemPlaylistRun :one diff --git a/internal/db/queries/system_playlists.sql b/internal/db/queries/system_playlists.sql index fa7443bd..8064dcf9 100644 --- a/internal/db/queries/system_playlists.sql +++ b/internal/db/queries/system_playlists.sql @@ -47,7 +47,10 @@ UPDATE system_playlist_runs UPDATE system_playlist_runs SET in_flight = false WHERE in_flight = true; -- name: PickSeedArtists :many --- Top-3 most-engaged distinct artists in the user's last 7 days. +-- Top-5 most-engaged distinct artist candidates in the user's last 7 +-- days. The Go-side picker (pickSeedArtistsForDay) shuffles these +-- daily-deterministically and takes the first 3 so the set of +-- "Songs like X" mixes rotates day-to-day. -- Score = unskipped-play count + 5 if user has liked the artist. WITH plays AS ( SELECT t.artist_id, @@ -67,11 +70,15 @@ SELECT p.artist_id, FROM plays p LEFT JOIN liked l ON l.artist_id = p.artist_id ORDER BY score DESC, p.artist_id - LIMIT 3; + LIMIT 5; --- name: PickTopPlayedTrackForUser :one --- For-You seed selection. Returns the user's most-played non-skipped --- track in the last 7 days; tie-break by track_id for determinism. +-- name: PickTopPlayedTracksForUser :many +-- For-You candidate seeds. Returns the user's top-5 most-played +-- non-skipped tracks in the last 7 days; tie-break by track_id for +-- determinism. The Go-side picker (pickForYouSeedForDay) chooses one +-- of the returned rows as today's seed via userIDHash so the +-- candidate pool rotates day-to-day while staying stable within a +-- day. SELECT t.id FROM play_events pe JOIN tracks t ON t.id = pe.track_id @@ -80,7 +87,7 @@ SELECT t.id AND pe.was_skipped = false GROUP BY t.id ORDER BY COUNT(*) DESC, t.id - LIMIT 1; + LIMIT 5; -- name: PickTopPlayedTrackForArtistByUser :one -- "Songs like X" seed selection. Returns the user's most-played non-skipped