nandi/gleanpublic Fork 0
2b8e65c
Commits
Clone
git clone https://git.rickub.com/nandi/glean.git
git clone ssh://git@rickub.com/nandi/glean.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

feat: Introduce random sampling for people recommendationsUnverified

Julien Robert committed 2026-06-08T16:52:44+02:00 Browse files
2b8e65c parent: ecbbcf7
modified internal/cluster/precompute.go +3 -6
@@ -62,21 +62,18 @@ func (e *Engine) precomputeForUser(ctx context.Context, userDID string) error {
6262 }
6363 }
6464
65- half := 3
66- inNet, err := e.computePeopleByFollowStatus(ctx, userDID, true, half)
65+ peoplePool := 10
66+ inNet, err := e.computePeopleByFollowStatus(ctx, userDID, true, peoplePool)
6767 if err != nil {
6868 return err
6969 }
70- outNet, err := e.computePeopleByFollowStatus(ctx, userDID, false, half)
70+ outNet, err := e.computePeopleByFollowStatus(ctx, userDID, false, peoplePool)
7171 if err != nil {
7272 return err
7373 }
7474 peopleRecs := append(inNet, outNet...)
7575 if len(peopleRecs) > 0 {
7676 normalizePersonScores(peopleRecs)
77- if len(peopleRecs) > 6 {
78- peopleRecs = peopleRecs[:6]
79- }
8077 }
8178
8279 e.storeRecs(ctx, userDID, "feed", feedRecs)
@@ -62,21 +62,18 @@ func (e *Engine) precomputeForUser(ctx context.Context, userDID string) error {
62 }62 }
63 }63 }
64 64
65- half := 365+ peoplePool := 10
66- inNet, err := e.computePeopleByFollowStatus(ctx, userDID, true, half)66+ inNet, err := e.computePeopleByFollowStatus(ctx, userDID, true, peoplePool)
67 if err != nil {67 if err != nil {
68 return err68 return err
69 }69 }
70- outNet, err := e.computePeopleByFollowStatus(ctx, userDID, false, half)70+ outNet, err := e.computePeopleByFollowStatus(ctx, userDID, false, peoplePool)
71 if err != nil {71 if err != nil {
72 return err72 return err
73 }73 }
74 peopleRecs := append(inNet, outNet...)74 peopleRecs := append(inNet, outNet...)
75 if len(peopleRecs) > 0 {75 if len(peopleRecs) > 0 {
76 normalizePersonScores(peopleRecs)76 normalizePersonScores(peopleRecs)
77- if len(peopleRecs) > 6 {
78- peopleRecs = peopleRecs[:6]
79- }
80 }77 }
81 78
82 e.storeRecs(ctx, userDID, "feed", feedRecs)79 e.storeRecs(ctx, userDID, "feed", feedRecs)
modified internal/cluster/scoring.go +44 -13
@@ -5,6 +5,7 @@ import (
55 "database/sql"
66 "encoding/json"
77 "fmt"
8+ "math/rand/v2"
89 "time"
910
1011 "pkg.rbrt.fr/glean/internal/db"
@@ -104,36 +105,38 @@ func (e *Engine) GetFeedRecommendations(ctx context.Context, userDID string, lim
104105 // limit come from the user's network (followed) and half from outside. When
105106 // outside-network candidates are scarce, in-network fills the remaining slots.
106107 func (e *Engine) GetPeopleRecommendations(ctx context.Context, userDID string, limit int) ([]*PersonRecommendation, error) {
108+ half := max(limit/2, 1)
109+
107110 if data, ok := e.getPrecomputed(ctx, userDID, "person"); ok {
108- var recs []*PersonRecommendation
109- if err := json.Unmarshal([]byte(data), &recs); err == nil {
110- return recs, nil
111+ var pool []*PersonRecommendation
112+ if err := json.Unmarshal([]byte(data), &pool); err == nil {
113+ return samplePeople(pool, half), nil
111114 }
112115 }
113116
114117 start := time.Now()
115118 e.logger.Info("people recommendations cache miss, computing on-demand", "did", userDID)
116119
117- half := max(limit/2, 1)
120+ peoplePool := 10
118121
119- inNet, err := e.computePeopleByFollowStatus(ctx, userDID, true, half)
122+ inNet, err := e.computePeopleByFollowStatus(ctx, userDID, true, peoplePool)
120123 if err != nil {
121124 return nil, err
122125 }
123126
124- outNet, err := e.computePeopleByFollowStatus(ctx, userDID, false, half)
127+ outNet, err := e.computePeopleByFollowStatus(ctx, userDID, false, peoplePool)
125128 if err != nil {
126129 return nil, err
127130 }
128131
129- var recs []*PersonRecommendation
130- recs = append(recs, inNet...)
131- recs = append(recs, outNet...)
132+ var pool []*PersonRecommendation
133+ pool = append(pool, inNet...)
134+ pool = append(pool, outNet...)
132135
133- normalizePersonScores(recs)
134- e.storeRecs(ctx, userDID, "person", recs)
135- e.logger.Info("people recommendations computed (on-demand)", "did", userDID, "count", len(recs), "duration", time.Since(start))
136- return recs, nil
136+ normalizePersonScores(pool)
137+ e.storeRecs(ctx, userDID, "person", pool)
138+ e.logger.Info("people recommendations computed (on-demand)", "did", userDID, "count", len(pool), "duration", time.Since(start))
139+ return samplePeople(pool, half), nil
137140 }
138141
139142 // GetArticleRecommendations returns article recommendations combining social
@@ -562,6 +565,34 @@ func (e *Engine) computePeopleByFollowStatus(ctx context.Context, userDID string
562565 return results, rows.Err()
563566 }
564567
568+// samplePeople randomly selects up to half people from each group (followed/not)
569+// to ensure rotation across page loads.
570+func samplePeople(pool []*PersonRecommendation, half int) []*PersonRecommendation {
571+ var inNet, outNet []*PersonRecommendation
572+ for _, p := range pool {
573+ if p.IsFollowed {
574+ inNet = append(inNet, p)
575+ } else {
576+ outNet = append(outNet, p)
577+ }
578+ }
579+
580+ rand.Shuffle(len(inNet), func(i, j int) { inNet[i], inNet[j] = inNet[j], inNet[i] })
581+ rand.Shuffle(len(outNet), func(i, j int) { outNet[i], outNet[j] = outNet[j], outNet[i] })
582+
583+ if len(inNet) > half {
584+ inNet = inNet[:half]
585+ }
586+ if len(outNet) > half {
587+ outNet = outNet[:half]
588+ }
589+
590+ result := make([]*PersonRecommendation, 0, len(inNet)+len(outNet))
591+ result = append(result, inNet...)
592+ result = append(result, outNet...)
593+ return result
594+}
595+
565596 func (e *Engine) ComputeSignalProfiles(ctx context.Context) error {
566597 conn, err := e.db.Conn(ctx)
567598 if err != nil {
@@ -5,6 +5,7 @@ import (
5 "database/sql"5 "database/sql"
6 "encoding/json"6 "encoding/json"
7 "fmt"7 "fmt"
8+ "math/rand/v2"
8 "time"9 "time"
9 10
10 "pkg.rbrt.fr/glean/internal/db"11 "pkg.rbrt.fr/glean/internal/db"
@@ -104,36 +105,38 @@ func (e *Engine) GetFeedRecommendations(ctx context.Context, userDID string, lim
104 // limit come from the user's network (followed) and half from outside. When105 // limit come from the user's network (followed) and half from outside. When
105 // outside-network candidates are scarce, in-network fills the remaining slots.106 // outside-network candidates are scarce, in-network fills the remaining slots.
106 func (e *Engine) GetPeopleRecommendations(ctx context.Context, userDID string, limit int) ([]*PersonRecommendation, error) {107 func (e *Engine) GetPeopleRecommendations(ctx context.Context, userDID string, limit int) ([]*PersonRecommendation, error) {
108+ half := max(limit/2, 1)
109+
107 if data, ok := e.getPrecomputed(ctx, userDID, "person"); ok {110 if data, ok := e.getPrecomputed(ctx, userDID, "person"); ok {
108- var recs []*PersonRecommendation111+ var pool []*PersonRecommendation
109- if err := json.Unmarshal([]byte(data), &recs); err == nil {112+ if err := json.Unmarshal([]byte(data), &pool); err == nil {
110- return recs, nil113+ return samplePeople(pool, half), nil
111 }114 }
112 }115 }
113 116
114 start := time.Now()117 start := time.Now()
115 e.logger.Info("people recommendations cache miss, computing on-demand", "did", userDID)118 e.logger.Info("people recommendations cache miss, computing on-demand", "did", userDID)
116 119
117- half := max(limit/2, 1)120+ peoplePool := 10
118 121
119- inNet, err := e.computePeopleByFollowStatus(ctx, userDID, true, half)122+ inNet, err := e.computePeopleByFollowStatus(ctx, userDID, true, peoplePool)
120 if err != nil {123 if err != nil {
121 return nil, err124 return nil, err
122 }125 }
123 126
124- outNet, err := e.computePeopleByFollowStatus(ctx, userDID, false, half)127+ outNet, err := e.computePeopleByFollowStatus(ctx, userDID, false, peoplePool)
125 if err != nil {128 if err != nil {
126 return nil, err129 return nil, err
127 }130 }
128 131
129- var recs []*PersonRecommendation132+ var pool []*PersonRecommendation
130- recs = append(recs, inNet...)133+ pool = append(pool, inNet...)
131- recs = append(recs, outNet...)134+ pool = append(pool, outNet...)
132 135
133- normalizePersonScores(recs)136+ normalizePersonScores(pool)
134- e.storeRecs(ctx, userDID, "person", recs)137+ e.storeRecs(ctx, userDID, "person", pool)
135- e.logger.Info("people recommendations computed (on-demand)", "did", userDID, "count", len(recs), "duration", time.Since(start))138+ e.logger.Info("people recommendations computed (on-demand)", "did", userDID, "count", len(pool), "duration", time.Since(start))
136- return recs, nil139+ return samplePeople(pool, half), nil
137 }140 }
138 141
139 // GetArticleRecommendations returns article recommendations combining social142 // GetArticleRecommendations returns article recommendations combining social
@@ -562,6 +565,34 @@ func (e *Engine) computePeopleByFollowStatus(ctx context.Context, userDID string
562 return results, rows.Err()565 return results, rows.Err()
563 }566 }
564 567
568+// samplePeople randomly selects up to half people from each group (followed/not)
569+// to ensure rotation across page loads.
570+func samplePeople(pool []*PersonRecommendation, half int) []*PersonRecommendation {
571+ var inNet, outNet []*PersonRecommendation
572+ for _, p := range pool {
573+ if p.IsFollowed {
574+ inNet = append(inNet, p)
575+ } else {
576+ outNet = append(outNet, p)
577+ }
578+ }
579+
580+ rand.Shuffle(len(inNet), func(i, j int) { inNet[i], inNet[j] = inNet[j], inNet[i] })
581+ rand.Shuffle(len(outNet), func(i, j int) { outNet[i], outNet[j] = outNet[j], outNet[i] })
582+
583+ if len(inNet) > half {
584+ inNet = inNet[:half]
585+ }
586+ if len(outNet) > half {
587+ outNet = outNet[:half]
588+ }
589+
590+ result := make([]*PersonRecommendation, 0, len(inNet)+len(outNet))
591+ result = append(result, inNet...)
592+ result = append(result, outNet...)
593+ return result
594+}
595+
565 func (e *Engine) ComputeSignalProfiles(ctx context.Context) error {596 func (e *Engine) ComputeSignalProfiles(ctx context.Context) error {
566 conn, err := e.db.Conn(ctx)597 conn, err := e.db.Conn(ctx)
567 if err != nil {598 if err != nil {