fix(recommendation): broaden You-might-like fallback to liked album/track artists (#790)
test-go / test (push) Successful in 29s
test-go / integration (push) Successful in 4m36s

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>
This commit is contained in:
2026-06-11 23:55:08 -04:00
parent 26c368c35e
commit c7adf2c87a
4 changed files with 249 additions and 7 deletions
+8 -7
View File
@@ -199,11 +199,12 @@ func HomeData(ctx context.Context, pool *pgxpool.Pool, userID pgtype.UUID) (*Hom
const youMightLikeFallbackFetch = 100
// topUpYouMightLikeArtists fills a thin You-might-like artist row from the
// user's liked artists (daily-stable sample), excluding what's already shown
// on Home (the section itself, Rediscover, Most/Last Played) so the fallback
// neither duplicates a tile nor suggests an actively-played artist. The liked
// pool dwarfs the similarity roll-up, so the same exclusions still fill it.
// Best-effort: a query error leaves the section as-is.
// artists the user has shown affinity for — explicit artist-likes PLUS the
// artists of liked albums and liked tracks (most users like albums/tracks far
// more than artists, so the broad pool is what actually fills the row).
// Excludes what's already shown on Home (the section itself, Rediscover,
// Most/Last Played) so the fallback neither duplicates a tile nor suggests an
// actively-played artist. Best-effort: a query error leaves the section as-is.
func topUpYouMightLikeArtists(
ctx context.Context, q *dbq.Queries, userID pgtype.UUID, out *HomePayload,
) []dbq.ListYouMightLikeArtistsForUserRow {
@@ -218,7 +219,7 @@ func topUpYouMightLikeArtists(
for _, r := range result {
excluded[r.Artist.ID] = struct{}{}
}
fb, err := q.ListRediscoverArtistsFallbackForUser(ctx, dbq.ListRediscoverArtistsFallbackForUserParams{
fb, err := q.ListYouMightLikeArtistFallbackForUser(ctx, dbq.ListYouMightLikeArtistFallbackForUserParams{
UserID: userID, Limit: youMightLikeFallbackFetch,
})
if err != nil {
@@ -249,7 +250,7 @@ func topUpYouMightLikeAlbums(
for _, r := range result {
excluded[r.Album.ID] = struct{}{}
}
fb, err := q.ListRediscoverAlbumsFallbackForUser(ctx, dbq.ListRediscoverAlbumsFallbackForUserParams{
fb, err := q.ListYouMightLikeAlbumFallbackForUser(ctx, dbq.ListYouMightLikeAlbumFallbackForUserParams{
UserID: userID, Limit: youMightLikeFallbackFetch,
})
if err != nil {
@@ -48,6 +48,40 @@ func TestHomeData_YouMightLike_FallbackFillsFromLikedArtists(t *testing.T) {
}
}
// TestHomeData_YouMightLike_FallbackFromLikedTrackArtists: the broad fallback
// must surface the artists of LIKED TRACKS even with no explicit artist-likes
// (most users like tracks/albums, rarely artists). 12 liked tracks by distinct
// artists, no artist-likes, no plays → the artist row fills from them.
func TestHomeData_YouMightLike_FallbackFromLikedTrackArtists(t *testing.T) {
pool := newPool(t)
user := seedUser(t, pool, "home-yml-trackartists")
fromLiked := make(map[pgtype.UUID]bool, 12)
for i := 0; i < 12; i++ {
a := seedArtist(t, pool, fmt.Sprintf("TrkArtist%02d", i), "")
al := seedAlbumForArtist(t, pool, a.ID, fmt.Sprintf("Alb%02d", i))
tr := seedTrackOnAlbum(t, pool, al.ID, a.ID, fmt.Sprintf("Trk%02d", i))
if _, err := pool.Exec(context.Background(),
`INSERT INTO general_likes (user_id, track_id) VALUES ($1, $2)`, user.ID, tr.ID); err != nil {
t.Fatalf("like track: %v", err)
}
fromLiked[a.ID] = true
}
got, err := HomeData(context.Background(), pool, user.ID)
if err != nil {
t.Fatalf("HomeData: %v", err)
}
if len(got.YouMightLikeArtists) != HomeYouMightLikeLimit {
t.Fatalf("you-might-like artists = %d, want %d (from liked-track artists)",
len(got.YouMightLikeArtists), HomeYouMightLikeLimit)
}
for _, r := range got.YouMightLikeArtists {
if !fromLiked[r.Artist.ID] {
t.Errorf("artist %s not from the liked-track pool", r.Artist.Name)
}
}
}
func TestHomeData_NewUser_AllSectionsEmpty(t *testing.T) {
pool := newPool(t)
user := seedUser(t, pool, "home-newuser")