Skip to content
Merged
Show file tree
Hide file tree
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
11 changes: 11 additions & 0 deletions api/v1_users_suggested_follows.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,17 @@ const (
// engagement history is unbounded, and everything downstream of it joins
// per-row, so this caps the worst case for heavy users. Recent engagement
// is also the better signal, so the cap costs little.
//
// Note this cap, not the decay below, is the effective window for anyone
// with a large library: a user who favorites 50 tracks a day is scored on
// their last ~40 days, well inside the decay's range. For everyone else the
// cap never binds and the decay does the shaping.
//
// Lowering it further is tempting for latency but trades against fill rate:
// the cap bounds the candidate pool *before* already-followed artists are
// filtered out, so a user who follows most of the artists they recently
// engaged with gets a short list. saves_user_created_at_active_idx
// (migration 0239) removes the reason to make that trade.
suggestedFollowsEngagementCap = 2000

// Engagement weight decays with e^(-age/tau). At tau = 180 days a favorite
Expand Down
21 changes: 21 additions & 0 deletions ddl/migrations/0239_saves_user_created_at_idx.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
-- Supports /v1/users/:id/suggested-follows, which takes a bounded slice of the
-- user's most recent favorites:
--
-- WHERE user_id = ?
-- AND is_delete = false
-- ORDER BY created_at DESC
-- LIMIT ?
--
-- No existing index can serve that ordering. saves_user_idx orders by save_type
-- and save_item_id before created_at, and saves_user_track_current_blocknumber_idx
-- orders by blocknumber and is partial on save_type = 'track' (the endpoint also
-- reads album/playlist saves, so it can't use a track-only index). Postgres
-- therefore reads every one of the user's saves and top-N sorts them, which for
-- a user with a large library dominates the request.
--
-- Mirrors reposts_user_created_at_active_idx (migration 0223), which already
-- solves exactly this for the reposts half of the same query.
CREATE INDEX CONCURRENTLY IF NOT EXISTS saves_user_created_at_active_idx
ON public.saves USING btree (user_id, created_at DESC)
INCLUDE (save_type, save_item_id)
WHERE is_delete = false;
Loading