11package api
22
33import (
4+ "context"
5+ "fmt"
6+
7+ "api.audius.co/api/dbv1"
48 "github.com/gofiber/fiber/v2"
59 "github.com/jackc/pgx/v5"
610)
@@ -15,29 +19,89 @@ type GetUsersRelatedParams struct {
1519Hybrid approach:
1620- For artists with < 100 followers: genre-based recommendations (not enough follower data)
1721- For artists with >= 100 followers: collaborative filtering with small genre boost
22+
23+ The candidate user-id list is cached briefly per (userId, limit, offset) — and
24+ also keyed on myId only when filter_followed is true. The list is a
25+ recommendation surface, not authoritative state, so a 10-minute TTL is fine.
26+ The follow-up GetUsers fetch (which carries the my-perspective fields like
27+ `is_followed`, etc.) still runs fresh on every request.
1828*/
1929func (app * ApiServer ) v1UsersRelated (c * fiber.Ctx ) error {
2030 params := GetUsersRelatedParams {}
2131 if err := app .ParseAndValidateQueryParams (c , & params ); err != nil {
2232 return err
2333 }
2434
35+ myId := app .getMyId (c )
36+ userId := app .getUserId (c )
37+
2538 var followerCount int64
2639 err := app .pool .QueryRow (
2740 c .Context (),
2841 `SELECT follower_count FROM aggregate_user WHERE user_id = $1` ,
29- app . getUserId ( c ) ,
42+ userId ,
3043 ).Scan (& followerCount )
3144 if err != nil {
3245 return err
3346 }
3447 lowFollowerCount := followerCount < 100
3548
36- var sql string
49+ limit := params .Limit
50+ if lowFollowerCount {
51+ // Clamp results to 0-10 because results are not as
52+ // good for low follower counts.
53+ limit = min (params .Limit , max (0 , 10 - params .Offset ))
54+ }
55+
56+ candidateIds , err := app .getRelatedUserIds (
57+ c .Context (),
58+ userId ,
59+ myId ,
60+ params .FilterFollowed ,
61+ lowFollowerCount ,
62+ limit ,
63+ params .Offset ,
64+ )
65+ if err != nil {
66+ return err
67+ }
68+
69+ users , err := app .queries .Users (c .Context (), dbv1.GetUsersParams {
70+ MyID : myId ,
71+ Ids : candidateIds ,
72+ })
73+ if err != nil {
74+ return err
75+ }
3776
38- // Use different algorithms based on follower count
77+ return v1UsersResponse (c , users )
78+ }
79+
80+ func (app * ApiServer ) getRelatedUserIds (
81+ ctx context.Context ,
82+ userId int32 ,
83+ myId int32 ,
84+ filterFollowed bool ,
85+ lowFollowerCount bool ,
86+ limit int ,
87+ offset int ,
88+ ) ([]int32 , error ) {
89+ // myId only affects the result when filter_followed is true (it's used
90+ // to exclude users the caller already follows). Folding myId into the
91+ // key only in that branch avoids splitting the cache by viewer when the
92+ // recommendations are identical across viewers.
93+ cacheMyId := int32 (0 )
94+ if filterFollowed {
95+ cacheMyId = myId
96+ }
97+ cacheKey := fmt .Sprintf ("%d:%t:%d:%d:%d:%t" ,
98+ userId , filterFollowed , cacheMyId , limit , offset , lowFollowerCount )
99+ if hit , ok := app .relatedUsersCache .Get (cacheKey ); ok {
100+ return hit , nil
101+ }
102+
103+ var sql string
39104 if lowFollowerCount {
40- // Genre-based algorithm for smaller artists
41105 sql = `
42106 WITH inp AS (
43107 SELECT genre,
@@ -49,9 +113,6 @@ func (app *ApiServer) v1UsersRelated(c *fiber.Ctx) error {
49113 AND t.is_unlisted IS false
50114 AND t.is_available IS true
51115 AND t.stem_of IS NULL
52- AND (t.access_authorities IS NULL
53- OR (COALESCE(@authed_wallet, '') <> ''
54- AND EXISTS (SELECT 1 FROM unnest(t.access_authorities) aa WHERE lower(aa) = lower(@authed_wallet))))
55116 AND owner_id = @userId
56117 GROUP BY genre
57118 ORDER BY count(*) DESC
@@ -82,15 +143,10 @@ func (app *ApiServer) v1UsersRelated(c *fiber.Ctx) error {
82143 OFFSET @offset;
83144 `
84145 } else {
85- // simple collaborative filtering
86- // - get a sample of followers. as ids are random, this is a reasonable sample
87- // - for each follower, get the top 200 artists they follow
88- // - score candidates based on how many of our sample follow them with some genre boost
89- // - return the top n
90146 sql = `
91147 WITH followers_sample AS MATERIALIZED (
92148 SELECT follower_user_id
93- FROM follows
149+ FROM follows
94150 WHERE followee_user_id = @userId
95151 ORDER BY follower_user_id DESC
96152 LIMIT 500
@@ -104,16 +160,13 @@ func (app *ApiServer) v1UsersRelated(c *fiber.Ctx) error {
104160 AND is_unlisted = false
105161 AND is_available = true
106162 AND stem_of IS NULL
107- AND (access_authorities IS NULL
108- OR (COALESCE(@authed_wallet, '') <> ''
109- AND EXISTS (SELECT 1 FROM unnest(access_authorities) aa WHERE lower(aa) = lower(@authed_wallet))))
110163 AND genre IS NOT NULL
111164 GROUP BY genre
112165 ORDER BY COUNT(*) DESC
113166 LIMIT 3
114167 ),
115168 candidate_users AS (
116- SELECT
169+ SELECT
117170 f.followee_user_id AS user_id,
118171 COUNT(*) AS shared_followers
119172 FROM followers_sample rf
@@ -128,11 +181,11 @@ func (app *ApiServer) v1UsersRelated(c *fiber.Ctx) error {
128181 GROUP BY f.followee_user_id
129182 ),
130183 scored_candidates AS (
131- SELECT
184+ SELECT
132185 cu.user_id,
133186 cu.shared_followers,
134187 au.follower_count,
135- CASE
188+ CASE
136189 WHEN au.dominant_genre IN (SELECT genre FROM top_genres) THEN 1
137190 ELSE 0
138191 END AS genre_match
@@ -159,29 +212,29 @@ func (app *ApiServer) v1UsersRelated(c *fiber.Ctx) error {
159212 SELECT user_id
160213 FROM scored_candidates
161214 WHERE shared_followers >= 3
162- ORDER BY
215+ ORDER BY
163216 -- approx jaccard similarity with small genre boost
164217 (shared_followers::float / (500 + follower_count - shared_followers)) + (genre_match * 0.05) DESC
165218 LIMIT @limit
166219 OFFSET @offset;
167220 `
168221 }
169222
170- var limit int
171- if lowFollowerCount {
172- // Clamp results to 0-10 because results are not as
173- // good for low follower counts
174- limit = min (params .Limit , max (0 , 10 - params .Offset ))
175- } else {
176- limit = params .Limit
177- }
178-
179- return app .queryUsers (c , sql , pgx.NamedArgs {
180- "myId" : app .getMyId (c ),
181- "userId" : app .getUserId (c ),
182- "filterFollowed" : params .FilterFollowed ,
223+ rows , err := app .pool .Query (ctx , sql , pgx.NamedArgs {
224+ "myId" : myId ,
225+ "userId" : userId ,
226+ "filterFollowed" : filterFollowed ,
183227 "limit" : limit ,
184- "offset" : params .Offset ,
185- "authed_wallet" : app .tryGetAuthedWallet (c ),
228+ "offset" : offset ,
186229 })
230+ if err != nil {
231+ return nil , err
232+ }
233+ ids , err := pgx .CollectRows (rows , pgx .RowTo [int32 ])
234+ if err != nil {
235+ return nil , err
236+ }
237+
238+ app .relatedUsersCache .Set (cacheKey , ids )
239+ return ids , nil
187240}
0 commit comments