It's now common for apps to default to a "For you" style feed instead of just a following feed.
This section of the docs will explain how to build a for you feed with Stream.
While the exact logic differs per app, fundamently the "for you" feed does a 2 different things:
After the feed combines the activities from different selectors it applies ranking.
Stream supports expression based ranking out of the box. This allows you to define a formula to rank activities.
In the example below we'll build a simple interest based for you feed, to read more about ranking, visit the "Custom ranking" guide
For the feed group we're enabling interest based ranking. We are ranking on interest_score * popularity * decay_exp(time)
For activity selectors we're also keeping it simple, showing just the activities from people you follow & popular activities.
You can also use preference_score in your ranking expression to incorporate user preferences based on activity feedback (show more/less). This allows the feed to prioritize content that users have explicitly indicated they want to see more of. See Activity feedback for more information.
We're also enabling activity processors.
The text topics activity processor uses an LLM to summarize the topics for an activity.
It writes this data to activity.interest_tags. This update happens in the background.
$updateResponse = $feedsClient->updateFeedGroup('timeline', new GeneratedModels\UpdateFeedGroupRequest( activitySelectors: [ new GeneratedModels\ActivitySelectorConfig(type: 'following'), new GeneratedModels\ActivitySelectorConfig(type: 'popular') ], ranking: new GeneratedModels\RankingConfig( type: 'interest', score: 'interest_score * (popularity > 0 ? popularity : 1) * decay_exp(time)', ), activityProcessors: [ new GeneratedModels\ActivityProcessorConfig(type: 'text_interest_tags') ]));
var updateResponse = await _feedsV3Client.UpdateFeedGroupAsync("feed_group_id", new UpdateFeedGroupRequest{ ActivityProcessors = new List<ActivityProcessorConfig> { new() { Type = "default" } }, Aggregation = new AggregationConfig { Format = "time_based" }});
var updateResponse = await _feedsV3Client.UpdateFeedGroupAsync("timeline", new UpdateFeedGroupRequest{ ActivitySelectors = new List<ActivitySelectorConfig> { new() { Type = "following" }, new() { Type = "popular" } }, Ranking = new RankingConfig { Type = "interest", Score = "interest_score * (popularity > 0 ? popularity : 1) * decay_exp(time)" }, ActivityProcessors = new List<ActivityProcessorConfig> { new() { Type = "text_interest_tags" } }});
Using preference_score: To incorporate user feedback into your ranking, you can use preference_score in your ranking expression. For example, to combine both interest-based and preference-based ranking:
const feed = client.feeds.feed("user", "jack");// since we've enabled the "text_topics" activity processor the "activity.interest_tags" will be automatically setconst response = await client.feeds.addActivity({ feeds: [feed.feed] type: "post", text: "apple stock will go up", user_id: 'jack'});// you can also set this field manuallyconst response = await client.feeds.addActivity({ feeds: ["user:1", "stock:apple"], type: "post", text: "apple stock will go up", interest_tags: ["apple", "stock"], user_id: 'jack'});
response, err := client.Feeds().AddActivity(context.Background(), &getstream.AddActivityRequest{ Feeds: []string{"user:john"}, Type: "post", Text: getstream.PtrTo("apple stock will go up"), UserID: getstream.PtrTo("john"),})if err != nil { log.Fatal("Error adding activity:", err)}// you can also set this field manuallyresponse2, err := client.Feeds().AddActivity(context.Background(), &getstream.AddActivityRequest{ Feeds: []string{"user:john"}, Type: "post", Text: getstream.PtrTo("apple stock will go up"), InterestTags: []string{"apple", "stock"}, UserID: getstream.PtrTo("john"),})if err != nil { log.Fatal("Error adding activity with manual interest tags:", err)}
AddActivityRequest activity = AddActivityRequest.builder() .type("post") .feeds(List.of(testFeedId)) .text("This is a test activity from Java SDK") .userID(testUserId) .build();AddActivityResponse response = feeds.addActivity(activity).execute().getData();
// since we've enabled the "text_interest_tags" activity processor the "interest_tags" will be automatically set$response = $feedsClient->addActivity(new GeneratedModels\AddActivityRequest( type: 'post', feeds: ['user:jack'], text: 'apple stock will go up', userID: 'jack'));// you can also set this field manually$response = $feedsClient->addActivity(new GeneratedModels\AddActivityRequest( type: 'post', feeds: ['user:1', 'stock:apple'], text: 'apple stock will go up', interestTags: ['apple', 'stock'], userID: 'jack'));
var activity = new AddActivityRequest{ Type = "post", Text = "This is a test activity from .NET SDK", UserID = _testUserId, Feeds = new List<string> { $"user:{_testFeedId}" }};var response = await _feedsV3Client.AddActivityAsync(activity);
response = self.client.feeds.add_activity( type="post", feeds=[self.test_feed.get_feed_identifier()], text="This is a test activity from Python SDK", user_id=self.test_user_id, custom={ "test_field": "test_value", "timestamp": int(datetime.now().timestamp()), },)
The important part here is that the activity has the interest tags set.
When reading an interest based feed you can either have Stream automatically calculate the interest weights, or pass them manually.
const interestWeights = { go: 1.0, // User loves Go python: 0.5, // User is ok with Python ruby: -1.0, // User dislikes Ruby};const feed = client.feeds.feed("user", "jack");const response = await feed.getOrCreate({ limit: 10, interest_weights: interestWeights, // Optionally overwrite the default interest weights for this feed user_id: "jack",});
// Define interest weightsinterestWeights := map[string]float64{ "go": 1.0, // User loves Go "python": 0.5, // User is ok with Python "ruby": -1.0, // User dislikes Ruby}// Get feed instancefeed := client.Feeds().Feed("user", "john")// Call getOrCreate with interest weightsresponse, err := feed.GetOrCreate(context.Background(), &getstream.GetOrCreateFeedRequest{ Limit: getstream.PtrTo(10), InterestWeights: interestWeights, // Optionally overwrite the default interest weights for this feed UserID: getstream.PtrTo("john"),})if err != nil { log.Fatal("Error calling GetOrCreate:", err)}
$interestWeights = [ 'go' => 1.0, // User loves Go 'python' => 0.5, // User is ok with Python 'ruby' => -1.0, // User dislikes Ruby];$response = $feedsClient->getOrCreateFeed('user', 'jack', new GeneratedModels\GetOrCreateFeedRequest( limit: 10, interestWeights: $interestWeights, // Optionally overwrite the default interest weights for this feed userID: 'jack'));
Following from For You Feed: When a user follows someone or content from the forYouFeed, this does not automatically add them to the user's timeline feed. If you want these follows to appear in the timeline feed, you need to explicitly create a follow connection between the timeline feed and the target feed when handling the follow action.
In a few lines of code you created an interest based "for you" style feed. It includes popular content as well as activities from the people you follow.
Activities from topics you engage with are shown higher in the feed.
This example kept things pretty basic but you can make it more complex.
You could include activities based on their interest_tags. So if someone engages with content about snowboarding you could show them more of that
Images can also be processed for interest tags
More ranking & activity selectors can be added. Activities close to you, activities from follow suggestions etc.
You can build infinitely complex "for you" style feeds with ranking & activity selectors.
Be sure to reach out to our support team in case anything is missing.
We're often adding selector or processors based on customer feedback.