Tig 257 stabilize explore feed ranking - #38
Conversation
…ty cap, seeded daily nudge, popularity/org-history signals, ranking docs
angelina-ji
left a comment
There was a problem hiding this comment.
Overall this looks solid and aligns well with TIG-257. I left a few comments around production-scale behavior: the fixed candidate pool, pagination consistency with the soon-event injection, and the number of per-event DB queries now that we're scoring up to 100 events.
Since we're expecting hundreds of events, I think the two correctness issues should be addressed before merge, and I'd also like to see the query pattern either batched or validated against a realistically sized dataset. Once those are updated, I can re-review and test the ranking behavior.
738e40c to
7430f6c
Compare
|
Hey, I think I addressed the issues, though I couldn't really check some things. I replaced the 100 events cap with a default dateRange filter of 14 days (so only events from the next 14 days would show up), and then put 5000 events as a safeguard in case there's too many events. For the event-ordering bug, I made it finalize the order and then paginate, so it shouldn't have the bug anymore (though I couldn't check this since there's no load more feature to move to the next page). For the benchmarking, I just counted the number of queries and latency for each method after generating 1000+ dummy events: |
| if (soonInFront < SOON_QUOTA) { | ||
| const frontIds = new Set(front.map((e) => e.id)); | ||
| const missingSoon = tail | ||
| .filter((e) => isSoon(e) && !frontIds.has(e.id)) |
There was a problem hiding this comment.
I think the soon-event injection can undo the org-diversity cap here. ranked has already deferred the 4th+ event from an org, but missingSoon pulls from tail without checking the org counts in front, so a deferred 4th event can be reinserted into the first 20 if it's imminent. That means the “max 3 per org near the top” guarantee isn't always true. Can we either preserve the cap during injection, or explicitly make/document the soon-event rule as an exception to the diversity cap?
|
Changed it so that it should only drag a soon event to the top if it's not part of an org that's already there 3 times (preserve the cap during injection) |
| deferred.push(item); | ||
| } | ||
| } | ||
| return [...primary, ...deferred]; |
There was a problem hiding this comment.
Missing a closing } for diversifyByOrg here. As written, loadFriendIds and the following declarations are still parsed inside this function, so events.ts won't compile.
| const page = finalOrder.slice(offset, offset + limit); | ||
|
|
||
| return { | ||
| events: enriched.map(({ score: _score, _rawDatetime, ...event }) => ({ |
There was a problem hiding this comment.
Should this be page.map(...) instead? We compute the final diversified + soon-injected ordering and slice it into page immediately above, but then return enriched instead. That means limit/offset are ignored and the org-diversity + soon-event transformations never make it into the response.
| * above already issues several queries per event, and adding another to that | ||
| * loop is what pushes the connection pool over on a full feed. | ||
| */ | ||
| const feedIds = enriched.map((e) => e.id); |
There was a problem hiding this comment.
The comment says this fetches attendees for the whole page, but feedIds comes from enriched, which is the entire candidate pool. Since full attendee objects aren't needed for scoring, could we do this after page is computed and query only page.map(e => e.id)? Otherwise a 20-event response can still load attendee rows for hundreds/thousands of candidate events.
angelina-ji
left a comment
There was a problem hiding this comment.
The earlier ranking/pagination concerns look addressed conceptually, and the batched candidate enrichment is a big improvement. I found a few regressions in the current head that still need to be fixed before merge: diversifyByOrg is missing its closing brace, the finalized page is computed but the response returns enriched instead, and the full attendee query is still running over the whole candidate pool rather than the final page.
Summary
Stabilizes and documents Explore feed ranking (TIG-257).
Ranking formula (see
docs/ranking.mdfor full detail)score = 3.0 × interest_relevance (tag overlap with your onboarding interests)
Highest score wins. The random nudge is a deterministic hash of
(userId, today's date, eventId), so the same user sees the same order all day, but the feed varies day to day. Two extra rules run after scoring: an org can have at most 3 events near the top of the ranking (rest pushed later), and the first page always includes a few events happening within 24h even if their score is weak, so nothing imminent gets buried behind a highly-social event further out.Changes
apps/web/src/actions/events.ts: widened candidate pool (100 events, not just page size) so personalization has more impact; replaced the bucketed time score with smooth decay; added org diversity cap, tiered org affinity, popularity signal, and the seeded random nudge.docs/ranking.md: writeup of formula, each signal, and edge cases (no interests/friends/orgs).Testing
Verified against seeded data (two users with different interests get different, explainable top events; repeated calls produce identical order; a zero-signal user gets a full feed with no errors; forcing a small page size confirms the soon-event guarantee actually kicks in under truncation). Screen recordings of both seeded users' Explore pages attached below.
https://github.com/user-attachments/assets/6a8b485a-e733-4f08-ac5a-a12c1d4ac0b9
https://github.com/user-attachments/assets/b32cdb0b-0156-4141-ab4d-845445c82bde