Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 19 additions & 11 deletions api/v1_users_feed_for_you.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,19 +38,19 @@ type GetUsersFeedForYouParams struct {
// < 1500 follower & following count
// (mirrors GET /tracks/trending/underground). Capped at 50.
// - cand_in_network_playlist — playlists/albums created in the last
// 14 days by users I follow. Capped at 50.
// 14 days by users I follow. Capped at 30.
// - cand_trending_playlist — top week-trending playlists/albums
// (playlist_trending_scores). Mirrors GET /playlists/trending.
// Capped at 50.
// Capped at 30.
// - cand_underground_playlist — week-trending playlists/albums whose
// owner has < 1500 follower & following count. Capped at 25.
// owner has < 1500 follower & following count. Capped at 15.
//
// Per-source caps are smaller for playlists because they're heavier
// objects to hydrate downstream and rarer in the index — flooding the
// pool with them would crowd out tracks under the shared per-artist cap.
//
// 2. RANKING — three light signals combined linearly, identical formula
// across types:
// 2. RANKING — three light signals combined linearly, then scaled by a
// content-type weight that keeps the feed track-forward:
//
// recency_score = exp(-ln(2) * age_hours / 48)
// // 48h half-life: 48h-old → 0.5, 96h → 0.25
Expand All @@ -63,9 +63,13 @@ type GetUsersFeedForYouParams struct {
// // tracks AND playlists, plus plays of tracks.
// source_weight = {in_network: 1.20, trending: 1.00,
// underground: 0.95}
// content_type_weight = {track: 1.00, playlist/album: 0.60}
// // collections stay in the feed but rank below
// // comparably-scored tracks, targeting roughly one
// // collection per 5-6 tracks.
//
// final_score = (0.55 * recency_score + 0.45 * engagement_score)
// * social_boost * source_weight
// * social_boost * source_weight * content_type_weight
//
// 3. FILTERS — applied once after the union to keep the candidate set
// cheap:
Expand Down Expand Up @@ -303,7 +307,7 @@ func (app *ApiServer) v1FeedForYou(c *fiber.Ctx) error {
AND p.is_private = false
AND p.created_at >= NOW() - INTERVAL '14 days'
ORDER BY p.created_at DESC
LIMIT 50
LIMIT 30
),
-- Source 2b: weekly trending playlists/albums.
cand_trending_playlist AS (
Expand All @@ -319,7 +323,7 @@ func (app *ApiServer) v1FeedForYou(c *fiber.Ctx) error {
AND pts.version = 'pnagD'
AND pts.time_range = 'week'
ORDER BY pts.score DESC, pts.playlist_id DESC
LIMIT 50
LIMIT 30
),
-- Source 3b: underground trending playlists/albums.
cand_underground_playlist AS (
Expand All @@ -338,7 +342,7 @@ func (app *ApiServer) v1FeedForYou(c *fiber.Ctx) error {
AND au.follower_count < 1500
AND au.following_count < 1500
ORDER BY pts.score DESC, pts.playlist_id DESC
LIMIT 25
LIMIT 15
),
-- One row per (entity_type, entity_id). DISTINCT ON keeps the
-- strongest (lowest-prio) source so an in-network item that's also
Expand Down Expand Up @@ -455,7 +459,11 @@ func (app *ApiServer) v1FeedForYou(c *fiber.Ctx) error {
WHEN 'trending' THEN 1.00
WHEN 'underground' THEN 0.95
ELSE 1.00
END AS source_weight
END AS source_weight,
CASE f.entity_type
WHEN 'track' THEN 1.00
ELSE 0.60
END AS content_type_weight
FROM filtered f
),
final_scored AS (
Expand All @@ -465,7 +473,7 @@ func (app *ApiServer) v1FeedForYou(c *fiber.Ctx) error {
owner_id,
created_at,
(0.55 * recency_score + 0.45 * engagement_score)
* social_boost * source_weight AS score
* social_boost * source_weight * content_type_weight AS score
FROM scored
),
capped AS (
Expand Down
Loading