fix(recommendation): broaden You-might-like fallback to liked album/track artists (#790)
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:
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user