Files
minstrel/internal/db/queries/you_might_like.sql
T
bvandeusen fdd14ef04c
test-go / test (push) Successful in 39s
test-go / integration (push) Failing after 4m39s
feat(server): "You might like" album/artist Home rows (#790)
Surface in-library albums/artists the listener doesn't actively spin but
is predicted to enjoy, derived from the same similarity + like-weighted
candidate engine that powers For-You — rolled up from track scores to
album/artist granularity. Built in the daily 3am BuildSystemPlaylists
pass, atomic-replaced alongside the system playlists, and read back by
/api/home (+ /api/home/index).

Cold-start gate: skips generation entirely below 20 distinct unskipped
tracks AND 5 distinct artists, so a thin profile ships empty rows rather
than near-random tiles.

- migration 0034: you_might_like_albums / you_might_like_artists (id+rank,
  CASCADE, per-user rank index).
- playlists/you_might_like.go: cold-start gate + similarity roll-up
  (sum-of-top-3 aggregation, per-artist album cap, daily-rotating via the
  same userIDHash jitter as For-You) + atomic-replace persist in the tx.
- recommendation/home.go: two new HomePayload sections with read-time
  cross-section dedup vs Most Played / Rediscover / Last Played, trimmed
  to 10 each.
- api: you_might_like_albums / you_might_like_artists on /api/home and
  /api/home/index, reusing albumRefFrom / artistRefFromCovered.
- tests: pure roll-up/aggregation/cap unit tests + DB-backed gate,
  sufficiency, and atomic-replace tests (all green vs real Postgres).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 19:33:46 -04:00

67 lines
2.5 KiB
SQL

-- "You might like" Home rows (#790). The daily build computes ranked
-- album/artist IDs in Go (similarity roll-up + cold-start gate) and
-- atomic-replaces them here; /api/home reads them back hydrated.
-- name: CountListeningSignalForUser :one
-- Cold-start gate. Returns the breadth of the user's real listening:
-- distinct unskipped tracks played and distinct artists played. The
-- build skips You-might-like entirely below a threshold (a thin history
-- yields near-random roll-ups). was_skipped=false so a user spamming
-- next can't inflate the signal.
SELECT
count(DISTINCT pe.track_id)::bigint AS distinct_tracks,
count(DISTINCT t.artist_id)::bigint AS distinct_artists
FROM play_events pe
JOIN tracks t ON t.id = pe.track_id
WHERE pe.user_id = $1 AND pe.was_skipped = false;
-- name: DeleteYouMightLikeAlbumsForUser :exec
DELETE FROM you_might_like_albums WHERE user_id = $1;
-- name: InsertYouMightLikeAlbum :exec
INSERT INTO you_might_like_albums (user_id, album_id, rank)
VALUES ($1, $2, $3);
-- name: DeleteYouMightLikeArtistsForUser :exec
DELETE FROM you_might_like_artists WHERE user_id = $1;
-- name: InsertYouMightLikeArtist :exec
INSERT INTO you_might_like_artists (user_id, artist_id, rank)
VALUES ($1, $2, $3);
-- name: ListYouMightLikeAlbumsForUser :many
-- Read path. Same projection as ListRediscoverAlbumsForUser so the API
-- layer reuses albumRefFrom(row.Album, row.ArtistName, …). Ordered by
-- the rank persisted at build time. Final cross-section dedup (vs Most
-- Played / Rediscover) and the output cap land in the Go layer
-- (internal/recommendation/home.go).
SELECT sqlc.embed(albums), artists.name AS artist_name
FROM you_might_like_albums yml
JOIN albums ON albums.id = yml.album_id
JOIN artists ON artists.id = albums.artist_id
WHERE yml.user_id = $1
ORDER BY yml.rank
LIMIT $2;
-- name: ListYouMightLikeArtistsForUser :many
-- Read path. Same projection as ListRediscoverArtistsForUser (embeds the
-- artist + a representative cover_album_id + album_count) so the API
-- layer reuses artistRefFromCovered. Ordered by persisted rank.
SELECT sqlc.embed(artists),
cov.id AS cover_album_id,
cnt.album_count::bigint AS album_count
FROM you_might_like_artists yml
JOIN artists ON artists.id = yml.artist_id
LEFT JOIN LATERAL (
SELECT id FROM albums
WHERE artist_id = artists.id AND cover_art_path IS NOT NULL
ORDER BY created_at DESC LIMIT 1
) cov ON true
LEFT JOIN LATERAL (
SELECT count(*) AS album_count
FROM albums WHERE artist_id = artists.id
) cnt ON true
WHERE yml.user_id = $1
ORDER BY yml.rank
LIMIT $2;