Skip to content

Commit

Permalink
Include viewer in feed
Browse files Browse the repository at this point in the history
  • Loading branch information
jarrel-b committed Oct 3, 2023
1 parent 9dbd899 commit 6e7d8dd
Show file tree
Hide file tree
Showing 5 changed files with 41 additions and 47 deletions.
18 changes: 4 additions & 14 deletions db/gen/coredb/recommend.sql.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 2 additions & 6 deletions db/queries/core/recommend.sql
Original file line number Diff line number Diff line change
Expand Up @@ -78,13 +78,9 @@ with refreshed as (
select sqlc.embed(feed_entity_scores), sqlc.embed(posts)
from feed_entity_scores
join posts on feed_entity_scores.id = posts.id
where feed_entity_scores.created_at > @window_end::timestamptz
and (@include_viewer::bool or feed_entity_scores.actor_id != @viewer_id)
and not posts.deleted
where feed_entity_scores.created_at > @window_end::timestamptz and not posts.deleted
union
select sqlc.embed(feed_entity_scores), sqlc.embed(posts)
from feed_entity_score_view feed_entity_scores
join posts on feed_entity_scores.id = posts.id
where feed_entity_scores.created_at > (select last_updated from refreshed limit 1)
and (@include_viewer::bool or feed_entity_scores.actor_id != @viewer_id)
and not posts.deleted;
where feed_entity_scores.created_at > (select last_updated from refreshed limit 1) and not posts.deleted;
2 changes: 1 addition & 1 deletion graphql/resolver/schema.resolvers.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

46 changes: 23 additions & 23 deletions publicapi/feed.go
Original file line number Diff line number Diff line change
Expand Up @@ -427,16 +427,8 @@ func (api FeedAPI) GlobalFeed(ctx context.Context, before *string, after *string
return paginator.paginate(before, after, first, last)
}

func fetchFeedEntityScores(ctx context.Context, queries *db.Queries, excludeUserID persist.DBID) (map[persist.DBID]db.GetFeedEntityScoresRow, error) {
params := db.GetFeedEntityScoresParams{
IncludeViewer: true,
WindowEnd: time.Now().Add(-feedLookback),
}
if excludeUserID != "" {
params.IncludeViewer = false
params.ViewerID = excludeUserID
}
scores, err := queries.GetFeedEntityScores(ctx, params)
func fetchFeedEntityScores(ctx context.Context, queries *db.Queries) (map[persist.DBID]db.GetFeedEntityScoresRow, error) {
scores, err := queries.GetFeedEntityScores(ctx, time.Now().Add(-feedLookback))
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -506,7 +498,7 @@ func (api FeedAPI) TrendingFeed(ctx context.Context, before *string, after *stri
var posts []db.Post

cacheCalcFunc := func(ctx context.Context) ([]persist.FeedEntityType, []persist.DBID, error) {
postScores, err := fetchFeedEntityScores(ctx, api.queries, "")
postScores, err := fetchFeedEntityScores(ctx, api.queries)
if err != nil {
return nil, nil, err
}
Expand Down Expand Up @@ -565,7 +557,7 @@ func (api FeedAPI) TrendingFeed(ctx context.Context, before *string, after *stri
return paginator.paginate(before, after, first, last)
}

func (api FeedAPI) CuratedFeed(ctx context.Context, before, after *string, first, last *int) ([]any, PageInfo, error) {
func (api FeedAPI) ForYouFeed(ctx context.Context, before, after *string, first, last *int) ([]any, PageInfo, error) {
// Validate
userID, _ := getAuthenticatedUserID(ctx)

Expand All @@ -592,7 +584,7 @@ func (api FeedAPI) CuratedFeed(ctx context.Context, before, after *string, first
return nil, PageInfo{}, err
}
} else {
postScores, err := fetchFeedEntityScores(ctx, api.queries, userID)
postScores, err := fetchFeedEntityScores(ctx, api.queries)
if err != nil {
return nil, PageInfo{}, err
}
Expand All @@ -603,22 +595,19 @@ func (api FeedAPI) CuratedFeed(ctx context.Context, before, after *string, first
personalizationScores := make(map[persist.DBID]float64)

for _, e := range postScores {
// Boost new events
boost := 1.0
if now.Sub(e.Post.CreatedAt) < 6*time.Hour {
boost *= 2.0
}
boost := newPostFactor(e.Post.CreatedAt, now)
timeF := timeFactor(e.Post.CreatedAt, now)
engagementScores[e.Post.ID] = boost * timeF * (1 + engagementFactor(int(e.FeedEntityScore.Interactions)))
personalizationScores[e.Post.ID] = boost * timeF * userpref.For(ctx).RelevanceTo(userID, e.FeedEntityScore)
}

// Rank by engagement first, then by personalization
topNByEngagement := api.scoreFeedEntities(ctx, 128, scores, func(e db.FeedEntityScore) float64 { return engagementScores[e.ID] })
topNByEngagement = api.scoreFeedEntities(ctx, 128, topNByEngagement, func(e db.FeedEntityScore) float64 { return personalizationScores[e.ID] })
topNByPersonalization := api.scoreFeedEntities(ctx, 128, scores, func(e db.FeedEntityScore) float64 { return engagementScores[e.ID] })
topNByPersonalization = api.scoreFeedEntities(ctx, 128, topNByPersonalization, func(e db.FeedEntityScore) float64 { return personalizationScores[e.ID] })

// Rank by personalization, then by engagement
topNByPersonalization := api.scoreFeedEntities(ctx, 128, scores, func(e db.FeedEntityScore) float64 { return personalizationScores[e.ID] })
topNByPersonalization = api.scoreFeedEntities(ctx, 128, topNByPersonalization, func(e db.FeedEntityScore) float64 { return engagementScores[e.ID] })
topNByEngagement := api.scoreFeedEntities(ctx, 128, scores, func(e db.FeedEntityScore) float64 { return personalizationScores[e.ID] })
topNByEngagement = api.scoreFeedEntities(ctx, 128, topNByEngagement, func(e db.FeedEntityScore) float64 { return engagementScores[e.ID] })

// Get ranking of both
seen := make(map[persist.DBID]bool)
Expand All @@ -644,10 +633,13 @@ func (api FeedAPI) CuratedFeed(ctx context.Context, before, after *string, first

// Score based on the average of the two rankings
interleaved := api.scoreFeedEntities(ctx, 128, combined, func(e db.FeedEntityScore) float64 {
if e.ActorID == userID {
return float64(engagementRank[e.ID])
}
return float64(engagementRank[e.ID]+personalizationRank[e.ID]) / 2.0
})

recommend.Shuffle(interleaved, 8)
recommend.Shuffle(interleaved, 4)

posts := make([]db.Post, len(interleaved))
postIDs := make([]persist.DBID, len(interleaved))
Expand Down Expand Up @@ -878,6 +870,14 @@ func timeFactor(t0, t1 time.Time) float64 {
return math.Pow(2, -(age / tHalf))
}

// newPostFactor returns a scaling factor for a post based on how recently it was made.
func newPostFactor(t1, t2 time.Time) float64 {
if t2.Sub(t1) < 6*time.Hour {
return 2.0
}
return 1.0
}

func engagementFactor(interactions int) float64 {
return math.Log1p(float64(interactions))
}
Expand Down
14 changes: 11 additions & 3 deletions service/recommend/userpref/personalize.go
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,11 @@ func (p *Personalization) RelevanceTo(userID persist.DBID, e db.FeedEntityScore)
return 0
}

// If scoring themselves, return 1 which is the max score
if userID == e.ActorID {
return 1.0
}

var relevanceScore float64

for _, contractID := range e.ContractIds {
Expand All @@ -208,7 +213,7 @@ func (p *Personalization) RelevanceTo(userID persist.DBID, e db.FeedEntityScore)
}

edgeScore, _ := p.scoreEdge(userID, e.ActorID)
return relevanceScore + edgeScore
return (relevanceScore + edgeScore) / 2.0
}

func (p *Personalization) scoreEdge(viewerID, queryID persist.DBID) (float64, error) {
Expand All @@ -219,7 +224,7 @@ func (p *Personalization) scoreEdge(viewerID, queryID persist.DBID) (float64, er
}
socialScore := calcSocialScore(p.pM.userM, vIdx, qIdx)
similarityScore := calcSimilarityScore(p.pM.simM, vIdx, qIdx)
return socialScore + similarityScore, nil
return (socialScore + similarityScore) / 2.0, nil
}

func (p *Personalization) scoreRelevance(viewerID, contractID persist.DBID) (float64, error) {
Expand Down Expand Up @@ -288,6 +293,9 @@ func (p *Personalization) readCache(ctx context.Context) {

// calcSocialScore determines if vIdx is in the same friend circle as qIdx by running a bfs on userM
func calcSocialScore(userM *sparse.CSR, vIdx, qIdx int) float64 {
if vIdx == qIdx {
return 1
}
return bfs(userM, vIdx, qIdx)
}

Expand All @@ -314,7 +322,7 @@ func calcSimilarityScore(simM *sparse.CSR, vIdx, qIdx int) float64 {
}

// idLookup uses a slice to lookup a value by its index
// Its purpose is to avoid using a map because indexing a slice is suprisingly much quicker than a map lookup.
// Its purpose is to avoid using a map because indexing a slice is surprisingly much quicker than a map lookup.
// It should be pretty space efficient: with one million users, it should take: 1 million users * 1 byte = 1MB of memory.
type idLookup struct {
l []uint8
Expand Down

0 comments on commit 6e7d8dd

Please sign in to comment.