diff --git a/api/v1_users_suggested_follows.go b/api/v1_users_suggested_follows.go index 57668aa3..f839bbc6 100644 --- a/api/v1_users_suggested_follows.go +++ b/api/v1_users_suggested_follows.go @@ -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 diff --git a/ddl/migrations/0239_saves_user_created_at_idx.sql b/ddl/migrations/0239_saves_user_created_at_idx.sql new file mode 100644 index 00000000..463f4368 --- /dev/null +++ b/ddl/migrations/0239_saves_user_created_at_idx.sql @@ -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;