feat(taste): time-of-day / weekday context conditioning — #1531
Milestone #160 Opt 3 (temporal half). A new additive scoring term that boosts a candidate when its artist's play history concentrates in the CURRENT daypart × weekday-type cell, in the user's local timezone. - Migration 0046: recommendation_weight_profiles.context_time_weight (per-profile scoring weight, DEFAULT 1.0). - Query ListArtistContextPlayCountsForUser: per-artist completed-play counts split by the current cell (daypart night[22,5)/morning[5,12)/ afternoon[12,17)/evening[17,22) × weekday-vs-weekend) via started_at AT TIME ZONE users.timezone; 365-day window, skips excluded. - internal/recommendation/context.go: LoadContextAffinity computes each artist's shrunk cell-share minus the user's baseline share, clamped to [-1,1]; sparse artists shrink toward baseline (pseudo-count 5), unknown artists → 0 (cold-start neutral). - Score() gains context_affinity_score · ContextTimeWeight; both candidate loaders set it per candidate. - Tuning lab: ContextTimeWeight threaded through recsettings + admin API + web card ("Time-of-day weight" row) + Go/web tests. Shipped 1.0 both profiles (uniform start, re-bakeable). Device-class axis deferred to #1551 (needs a client_id → device-class mapping that doesn't exist yet). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -173,6 +173,56 @@ GROUP BY t.id, t.title, t.album_id, t.artist_id, t.duration_ms, t.file_path,
|
||||
l.user_id, pe.last_played_at, pe.play_count, pe.skip_count,
|
||||
al.release_date;
|
||||
|
||||
-- name: ListArtistContextPlayCountsForUser :many
|
||||
-- Per-artist completed-play counts split by whether each play falls in the
|
||||
-- CURRENT daypart × weekday-type cell, in the user's local timezone (#1531).
|
||||
-- Feeds the context-affinity scoring term: an artist whose plays concentrate
|
||||
-- in the current cell (vs the user's overall baseline, computed Go-side from
|
||||
-- these rows) gets boosted right now. Skips excluded; a 365-day window bounds
|
||||
-- cost. Daypart buckets: night [22,5) morning [5,12) afternoon [12,17)
|
||||
-- evening [17,22). Weekend = ISO days 6–7 (Sat/Sun).
|
||||
WITH tz AS (
|
||||
SELECT COALESCE(NULLIF(u.timezone, ''), 'UTC') AS zone
|
||||
FROM users u WHERE u.id = $1
|
||||
),
|
||||
now_cell AS (
|
||||
SELECT
|
||||
CASE
|
||||
WHEN EXTRACT(hour FROM now() AT TIME ZONE tz.zone) < 5 THEN 3
|
||||
WHEN EXTRACT(hour FROM now() AT TIME ZONE tz.zone) < 12 THEN 0
|
||||
WHEN EXTRACT(hour FROM now() AT TIME ZONE tz.zone) < 17 THEN 1
|
||||
WHEN EXTRACT(hour FROM now() AT TIME ZONE tz.zone) < 22 THEN 2
|
||||
ELSE 3
|
||||
END AS daypart,
|
||||
(EXTRACT(isodow FROM now() AT TIME ZONE tz.zone) >= 6) AS is_weekend
|
||||
FROM tz
|
||||
),
|
||||
plays AS (
|
||||
SELECT t.artist_id,
|
||||
CASE
|
||||
WHEN EXTRACT(hour FROM pe.started_at AT TIME ZONE tz.zone) < 5 THEN 3
|
||||
WHEN EXTRACT(hour FROM pe.started_at AT TIME ZONE tz.zone) < 12 THEN 0
|
||||
WHEN EXTRACT(hour FROM pe.started_at AT TIME ZONE tz.zone) < 17 THEN 1
|
||||
WHEN EXTRACT(hour FROM pe.started_at AT TIME ZONE tz.zone) < 22 THEN 2
|
||||
ELSE 3
|
||||
END AS daypart,
|
||||
(EXTRACT(isodow FROM pe.started_at AT TIME ZONE tz.zone) >= 6) AS is_weekend
|
||||
FROM play_events pe
|
||||
JOIN tracks t ON t.id = pe.track_id
|
||||
CROSS JOIN tz
|
||||
WHERE pe.user_id = $1
|
||||
AND pe.was_skipped = false
|
||||
AND pe.started_at > now() - interval '365 days'
|
||||
)
|
||||
SELECT p.artist_id,
|
||||
count(*) AS total_plays,
|
||||
count(*) FILTER (
|
||||
WHERE p.daypart = (SELECT daypart FROM now_cell)
|
||||
AND p.is_weekend = (SELECT is_weekend FROM now_cell)
|
||||
) AS cell_plays
|
||||
FROM plays p
|
||||
GROUP BY p.artist_id;
|
||||
|
||||
-- name: SuggestArtistsForUser :many
|
||||
-- M5c: per-user artist suggestions ranked by signal x similarity. The
|
||||
-- seeds CTE collects the user's likes (x5) plus recency-decayed plays
|
||||
|
||||
Reference in New Issue
Block a user