c7adf2c87a
The fallback pulled artists only from explicit artist-likes (general_likes_artists),
but most users like albums and tracks far more than artists — so the artists row
still came up thin (a couple of tiles) even with a rich library, while the albums
row filled fine.
Broaden both fallbacks to "entities you've shown affinity for":
- artist fallback = explicit artist-likes ∪ artists of liked albums ∪ artists of
liked tracks.
- album fallback = explicit album-likes ∪ albums of liked tracks.
New dedicated queries (ListYouMightLike{Artist,Album}FallbackForUser) replace the
narrow Rediscover-fallback reuse; same projection so the Go layer still converts
directly. (Aliased + fully-qualified the UNION arms — sqlc merges UNION scopes,
so unqualified user_id was ambiguous across the three like tables.)
Test: 12 liked TRACKS by distinct artists, no artist-likes → the artist row now
fills from their artists (was empty before).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
126 lines
4.7 KiB
SQL
126 lines
4.7 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;
|
|
|
|
-- name: ListYouMightLikeArtistFallbackForUser :many
|
|
-- Broad "artists you've shown affinity for" pool to fill a thin
|
|
-- You-might-like artists row: explicitly liked artists PLUS the artists of
|
|
-- liked albums and liked tracks. Most users like albums/tracks far more than
|
|
-- they like artists explicitly, so the union is much deeper than
|
|
-- general_likes_artists alone. Daily-stable order; same projection as
|
|
-- ListYouMightLikeArtistsForUser so the Go layer reuses it directly.
|
|
WITH liked_artist_ids AS (
|
|
SELECT lika.artist_id AS artist_id
|
|
FROM general_likes_artists lika
|
|
WHERE lika.user_id = $1
|
|
UNION
|
|
SELECT alb.artist_id AS artist_id
|
|
FROM general_likes_albums likb
|
|
JOIN albums alb ON alb.id = likb.album_id
|
|
WHERE likb.user_id = $1
|
|
UNION
|
|
SELECT trk.artist_id AS artist_id
|
|
FROM general_likes likt
|
|
JOIN tracks trk ON trk.id = likt.track_id
|
|
WHERE likt.user_id = $1
|
|
)
|
|
SELECT sqlc.embed(artists),
|
|
cov.id AS cover_album_id,
|
|
cnt.album_count::bigint AS album_count
|
|
FROM liked_artist_ids lai
|
|
JOIN artists ON artists.id = lai.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
|
|
ORDER BY md5(artists.id::text || current_date::text)
|
|
LIMIT $2;
|
|
|
|
-- name: ListYouMightLikeAlbumFallbackForUser :many
|
|
-- Broad "albums you've shown affinity for" pool: explicitly liked albums PLUS
|
|
-- the albums of liked tracks. Same projection as ListYouMightLikeAlbumsForUser.
|
|
WITH liked_album_ids AS (
|
|
SELECT likb.album_id AS album_id
|
|
FROM general_likes_albums likb
|
|
WHERE likb.user_id = $1
|
|
UNION
|
|
SELECT trk.album_id AS album_id
|
|
FROM general_likes likt
|
|
JOIN tracks trk ON trk.id = likt.track_id
|
|
WHERE likt.user_id = $1
|
|
)
|
|
SELECT sqlc.embed(albums), artists.name AS artist_name
|
|
FROM liked_album_ids lai
|
|
JOIN albums ON albums.id = lai.album_id
|
|
JOIN artists ON artists.id = albums.artist_id
|
|
ORDER BY md5(albums.id::text || current_date::text)
|
|
LIMIT $2;
|