diff --git a/internal/db/dbq/recommendation.sql.go b/internal/db/dbq/recommendation.sql.go index cf1bd09b..443fc375 100644 --- a/internal/db/dbq/recommendation.sql.go +++ b/internal/db/dbq/recommendation.sql.go @@ -1022,22 +1022,6 @@ func (q *Queries) LoadRadioCandidatesV2(ctx context.Context, arg LoadRadioCandid } const suggestArtistsForUser = `-- name: SuggestArtistsForUser :many --- Per-user artist suggestions ranked by taste signal x similarity, projected --- through artist_similarity_unmatched (out-of-library candidates only). --- --- Seeds are TIERED (rule #131) so the surface never empties: --- tier 1 - taste_profile_artists.weight: engagement-graded, time-decayed and --- SIGNED by internal/taste, so an artist the user has drifted away --- from stops contributing instead of accumulating forever. --- tier 2 - likes + completed plays, used ONLY when the profile has no rows --- (new account, or before the first daily recompute). --- --- The signal is log-damped: contribution is signal x similarity, and the old --- undamped sum let one heavily-played artist's neighbours take every slot -- --- entrenching harder the MORE the user listened (issue #2367 mechanism 2). --- --- Candidates already in the library, or already requested and not terminal, --- are excluded. $1=user_id, $2=half_life_days (tier 2 decay), $3=limit. WITH artist_plays AS ( -- Completed plays only. The previous seed query counted every play_event, -- so skipping an artist repeatedly INCREASED its signal and pushed more of @@ -1128,13 +1112,23 @@ type SuggestArtistsForUserRow struct { TopPlayCounts []int64 } -// M5c: per-user artist suggestions ranked by signal x similarity. The -// seeds CTE collects the user's likes (x5) plus recency-decayed plays -// (exp(-age_days / $2)). The contributions CTE joins those seeds against -// artist_similarity_unmatched and filters out candidates already in -// library or already in a non-terminal lidarr_request. The outer SELECT -// aggregates per candidate, returning the top-3 contributing seeds for -// attribution. $1=user_id, $2=half_life_days, $3=limit. +// Per-user artist suggestions ranked by taste signal x similarity, projected +// through artist_similarity_unmatched (out-of-library candidates only). +// +// Seeds are TIERED (rule #131) so the surface never empties: +// +// tier 1 - taste_profile_artists.weight: engagement-graded, time-decayed and +// SIGNED by internal/taste, so an artist the user has drifted away +// from stops contributing instead of accumulating forever. +// tier 2 - likes + completed plays, used ONLY when the profile has no rows +// (new account, or before the first daily recompute). +// +// The signal is log-damped: contribution is signal x similarity, and the old +// undamped sum let one heavily-played artist's neighbours take every slot -- +// entrenching harder the MORE the user listened (issue #2367 mechanism 2). +// +// Candidates already in the library, or already requested and not terminal, +// are excluded. $1=user_id, $2=half_life_days (tier 2 decay), $3=limit. func (q *Queries) SuggestArtistsForUser(ctx context.Context, arg SuggestArtistsForUserParams) ([]SuggestArtistsForUserRow, error) { rows, err := q.db.Query(ctx, suggestArtistsForUser, arg.UserID, arg.Column2, arg.Limit) if err != nil {