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) <noreply@anthropic.com>
This commit is contained in:
2026-05-13 10:04:39 -04:00
parent 90d8aae51a
commit b4801c2dd3
2 changed files with 44 additions and 17 deletions
+31 -11
View File
@@ -308,7 +308,7 @@ SELECT p.artist_id,
FROM plays p FROM plays p
LEFT JOIN liked l ON l.artist_id = p.artist_id LEFT JOIN liked l ON l.artist_id = p.artist_id
ORDER BY score DESC, p.artist_id ORDER BY score DESC, p.artist_id
LIMIT 3 LIMIT 5
` `
type PickSeedArtistsRow struct { type PickSeedArtistsRow struct {
@@ -316,7 +316,10 @@ type PickSeedArtistsRow struct {
Score int64 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. // Score = unskipped-play count + 5 if user has liked the artist.
func (q *Queries) PickSeedArtists(ctx context.Context, userID pgtype.UUID) ([]PickSeedArtistsRow, error) { func (q *Queries) PickSeedArtists(ctx context.Context, userID pgtype.UUID) ([]PickSeedArtistsRow, error) {
rows, err := q.db.Query(ctx, pickSeedArtists, userID) rows, err := q.db.Query(ctx, pickSeedArtists, userID)
@@ -408,7 +411,7 @@ func (q *Queries) PickTopPlayedTrackForArtistByUser(ctx context.Context, arg Pic
return id, err return id, err
} }
const pickTopPlayedTrackForUser = `-- name: PickTopPlayedTrackForUser :one const pickTopPlayedTracksForUser = `-- name: PickTopPlayedTracksForUser :many
SELECT t.id SELECT t.id
FROM play_events pe FROM play_events pe
JOIN tracks t ON t.id = pe.track_id JOIN tracks t ON t.id = pe.track_id
@@ -417,16 +420,33 @@ SELECT t.id
AND pe.was_skipped = false AND pe.was_skipped = false
GROUP BY t.id GROUP BY t.id
ORDER BY COUNT(*) DESC, t.id ORDER BY COUNT(*) DESC, t.id
LIMIT 1 LIMIT 5
` `
// For-You seed selection. Returns the user's most-played non-skipped // For-You candidate seeds. Returns the user's top-5 most-played
// track in the last 7 days; tie-break by track_id for determinism. // non-skipped tracks in the last 7 days; tie-break by track_id for
func (q *Queries) PickTopPlayedTrackForUser(ctx context.Context, userID pgtype.UUID) (pgtype.UUID, error) { // determinism. The Go-side picker (pickForYouSeedForDay) chooses one
row := q.db.QueryRow(ctx, pickTopPlayedTrackForUser, userID) // of the returned rows as today's seed via userIDHash so the
var id pgtype.UUID // candidate pool rotates day-to-day while staying stable within a
err := row.Scan(&id) // day.
return id, err 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 const tryClaimSystemPlaylistRun = `-- name: TryClaimSystemPlaylistRun :one
+13 -6
View File
@@ -47,7 +47,10 @@ UPDATE system_playlist_runs
UPDATE system_playlist_runs SET in_flight = false WHERE in_flight = true; UPDATE system_playlist_runs SET in_flight = false WHERE in_flight = true;
-- name: PickSeedArtists :many -- 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. -- Score = unskipped-play count + 5 if user has liked the artist.
WITH plays AS ( WITH plays AS (
SELECT t.artist_id, SELECT t.artist_id,
@@ -67,11 +70,15 @@ SELECT p.artist_id,
FROM plays p FROM plays p
LEFT JOIN liked l ON l.artist_id = p.artist_id LEFT JOIN liked l ON l.artist_id = p.artist_id
ORDER BY score DESC, p.artist_id ORDER BY score DESC, p.artist_id
LIMIT 3; LIMIT 5;
-- name: PickTopPlayedTrackForUser :one -- name: PickTopPlayedTracksForUser :many
-- For-You seed selection. Returns the user's most-played non-skipped -- For-You candidate seeds. Returns the user's top-5 most-played
-- track in the last 7 days; tie-break by track_id for determinism. -- 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 SELECT t.id
FROM play_events pe FROM play_events pe
JOIN tracks t ON t.id = pe.track_id JOIN tracks t ON t.id = pe.track_id
@@ -80,7 +87,7 @@ SELECT t.id
AND pe.was_skipped = false AND pe.was_skipped = false
GROUP BY t.id GROUP BY t.id
ORDER BY COUNT(*) DESC, t.id ORDER BY COUNT(*) DESC, t.id
LIMIT 1; LIMIT 5;
-- name: PickTopPlayedTrackForArtistByUser :one -- name: PickTopPlayedTrackForArtistByUser :one
-- "Songs like X" seed selection. Returns the user's most-played non-skipped -- "Songs like X" seed selection. Returns the user's most-played non-skipped