feat(recommendations): collect demographics and advance age cohorts
ci / ts / ts (packages + web) (push) Successful in 3m19s
ci / db / db (push) Successful in 53s
ci / module boundaries / boundaries (push) Successful in 7s
ci / api / api (push) Successful in 51s
ci / contract / contract (сгенерированный код совпадает с openapi.yaml) (push) Successful in 1m3s
ci / recommender / recommender (push) Failing after 16s
ci / mobile / mobile (Metro-бандл Android) (push) Successful in 1m15s

This commit is contained in:
mrleo1nid committed 2026-10-22 14:19:52 +03:00
1 parent e158558da3
commit fff91c446b
24 files changed
+778 -324

No files matched your search

+20 -4
View File
@@ -373,7 +373,18 @@ func (h *Handler) record(ctx context.Context, s session, picks []Pick, modelVer
// без дизлайков, недавно игравших и уже выданных; один трек исполнителя на пачку.
func (h *Handler) fallback(ctx context.Context, s session, served []string) ([]Pick, error) {
rows, err := h.pool.Query(ctx, `
WITH liked AS (
WITH own AS (
SELECT onboarding FROM user_profiles_rec WHERE user_id = $1
), cohort AS (
SELECT r.track_id FROM track_reactions r
JOIN user_profiles_rec p ON p.user_id = r.user_id
JOIN users u ON u.id = p.user_id, own
WHERE r.reaction = 'like' AND p.user_id <> $1 AND NOT u.is_blocked
AND (own.onboarding->>'gender' IS NOT NULL OR rec_age_group(own.onboarding) IS NOT NULL)
AND (own.onboarding->>'gender' IS NULL OR p.onboarding->>'gender' = own.onboarding->>'gender')
AND (rec_age_group(own.onboarding) IS NULL OR rec_age_group(p.onboarding) = rec_age_group(own.onboarding))
GROUP BY r.track_id HAVING count(DISTINCT r.user_id) >= 2
), liked AS (
SELECT track_id FROM track_reactions WHERE user_id = $1 AND reaction = 'like'
), fav_artists AS (
SELECT DISTINCT ta.artist_id FROM track_artists ta JOIN liked l ON l.track_id = ta.track_id
@@ -384,7 +395,12 @@ func (h *Handler) fallback(ctx context.Context, s session, served []string) ([]P
CASE WHEN t.id IN (SELECT track_id FROM liked) THEN 'liked'
WHEN EXISTS (SELECT 1 FROM track_artists ta WHERE ta.track_id = t.id
AND ta.artist_id IN (SELECT artist_id FROM fav_artists)) THEN 'artist'
ELSE 'random' END AS source
WHEN EXISTS (SELECT 1 FROM own, track_artists ta
WHERE ta.track_id = t.id AND own.onboarding->'artist_ids' ? ta.artist_id::text) THEN 'onboarding'
WHEN EXISTS (SELECT 1 FROM own, track_genres tg JOIN genres g ON g.id = tg.genre_id
WHERE tg.track_id = t.id AND own.onboarding->'genres' ? g.name) THEN 'onboarding'
WHEN t.id IN (SELECT track_id FROM cohort) THEN 'cohort'
ELSE 'random' END AS source
FROM tracks t
JOIN albums al ON al.id = t.album_id
JOIN media_files f ON f.id = t.primary_file_id AND f.missing_since IS NULL
@@ -395,7 +411,7 @@ func (h *Handler) fallback(ctx context.Context, s session, served []string) ([]P
AND e.type = 'play_start' AND e.occurred_at > now() - interval '3 hours')
)
SELECT DISTINCT ON (artist_id) id::text, source FROM (
SELECT * FROM pool ORDER BY CASE source WHEN 'liked' THEN 0 WHEN 'artist' THEN 1 ELSE 2 END, random() LIMIT 200
SELECT * FROM pool ORDER BY CASE source WHEN 'liked' THEN 0 WHEN 'artist' THEN 1 WHEN 'onboarding' THEN 2 WHEN 'cohort' THEN 3 ELSE 4 END, random() LIMIT 200
) c
ORDER BY artist_id, random()
LIMIT 50`, s.UserID, served)
@@ -410,7 +426,7 @@ func (h *Handler) fallback(ctx context.Context, s session, served []string) ([]P
return nil, fmt.Errorf("wave: fallback: %w", err)
}
// DISTINCT ON упорядочил по исполнителю — перемешиваем, предпочитая лайки и любимых исполнителей.
rank := map[string]int{"liked": 0, "artist": 1, "random": 2}
rank := map[string]int{"liked": 0, "artist": 1, "onboarding": 2, "cohort": 3, "random": 4}
shuffled := shuffle(picks)
stableSort(shuffled, func(a, b Pick) bool { return rank[a.Source] < rank[b.Source] })
// Половина пачки — любимое, половина — случайное: запасная волна не должна быть одними лайками.