package recommendation import ( "context" "encoding/json" "time" "github.com/jackc/pgx/v5/pgtype" "git.fabledsword.com/bvandeusen/minstrel/internal/db/dbq" ) // LoadCandidates fetches the candidate pool for radio scoring. Combines // the existing track+stats query with a one-shot bulk fetch of the user's // active contextual_likes, mapping each candidate to its max similarity // against currentVector. Pass currentVector with Seed=true to short-circuit // the contextual term to 0 (cold-start path). func LoadCandidates( ctx context.Context, q *dbq.Queries, userID, seedID pgtype.UUID, recentlyPlayedHours int, currentVector SessionVector, ) ([]Candidate, error) { rows, err := q.LoadRadioCandidates(ctx, dbq.LoadRadioCandidatesParams{ UserID: userID, ID: seedID, Column3: float64(recentlyPlayedHours), }) if err != nil { return nil, err } likes, err := loadContextualLikesByTrack(ctx, q, userID) if err != nil { return nil, err } out := make([]Candidate, 0, len(rows)) for _, r := range rows { var lpt *time.Time if r.LastPlayedAt.Valid { t := r.LastPlayedAt.Time lpt = &t } ctxScore := ContextualMatchScore(currentVector, likes[r.Track.ID], DefaultSimilarityWeights) out = append(out, Candidate{ Track: r.Track, Inputs: ScoringInputs{ IsGeneralLiked: r.IsLiked, LastPlayedAt: lpt, PlayCount: int(r.PlayCount), SkipCount: int(r.SkipCount), ContextualMatchScore: ctxScore, }, }) } return out, nil } // loadContextualLikesByTrack fetches the user's active contextual_likes in // one query and groups them by track_id. Rows whose session_vector fails // to unmarshal are skipped with no error (don't poison scoring over one // bad row); the SQL query already filters NULL vectors. func loadContextualLikesByTrack( ctx context.Context, q *dbq.Queries, userID pgtype.UUID, ) (map[pgtype.UUID][]SessionVector, error) { rows, err := q.ListActiveContextualLikesForUser(ctx, userID) if err != nil { return nil, err } out := make(map[pgtype.UUID][]SessionVector, len(rows)) for _, r := range rows { var v SessionVector if err := json.Unmarshal(r.SessionVector, &v); err != nil { continue } out[r.TrackID] = append(out[r.TrackID], v) } return out, nil }