Ranks activities based on their timestamp, with newer activities appearing first
expression
Uses a custom mathematical expression to calculate scores for ranking activities
interest
expression ranking extended with interest weights
Read on to learn the syntax for expression and interest based ranking.
For high-traffic feeds where every reader sees the same order, hot feed cache can serve a shared cached result. It supports recency and expression ranking (including external ranking) but not interest ranking or expressions that use per-user state such as is_read and is_seen.
The result of the ranking expression should be a number (called score). Activities in the feed will be ordered by score (highest score first).
Example of a simple ranking expression, order activities by popularity:
var createResponse = await _feedsV3Client.CreateFeedGroupAsync(new CreateFeedGroupRequest{ ID = feedGroupId, DefaultVisibility = "public", ActivityProcessors = new List<ActivityProcessorConfig> { new() { Type = "default" } }});
Custom ranking is applied after activity selectors filtered activities. Each activity selector selects a maximum of 1000 activities. If you combine selectors, 1000 is multiplied by the number of selectors you have. You can use a maximum of 3 selectors in a group/view.
score_strategy requires aggregation.format. If ranking or aggregation is not configured, groups fall back to the default aggregated ordering (updated_at descending for non-story feeds, created_at descending for stories).
When score_strategy is configured, aggregated groups are sorted by:
score descending
updated_at descending (tiebreaker, most recently active group first)
Pagination for scored aggregated groups uses ranked-feed style offset pagination (cursor includes offset, limit, and ranking version). If ranking config changes between page requests, the cursor expires and you should restart pagination from page 1.
It's not possible to access all fields of an ActivityResponse inside the ranking context. Below you'll find the list of supported fields:
name
description
time
UNIX timestamp of when the activity was created
popularity
The popularity score of an activity (formula: activity.popularity = reactions + comments * 2 + bookmarks * 3 + shares * 3)
reaction_count
The sum of all reactions
reaction_counts
Reaction counts by type, you can access specific types with dot notation (for example, reaction_counts.like)
comment_count
How many comments the activity has
bookmark_count
How many bookmarks the activity has
share_count
How many shares the activity has (Increased when an activity is set as the parent_id of another activity)
custom
The custom field of the activity, nested fields can be accessed with . notation, for example: custom.topic
selector_source
Which activity selector provided this activity (e.g., "following", "popular", "interest"). Only set when using multiple activity selectors. See Ranking by Selector Source for usage examples.
interest_score
Only available when using interest type ranking. A number between 0 and 1 reflecting how much a given activity matches the user's interests. See Interest weights to see how a user's interests are computed/configured.
preference_score
Available when using expression or interest type ranking. A number reflecting how much a given activity matches the user's preferences based on activity feedback (show more/less). Activities with interest_tags that have strong dislike are filtered out. See Activity feedback for more information.
is_read
Boolean (true/false). Only available on feeds with track_read enabled (e.g., notification feeds). true if the activity (or its aggregated group) has been marked as read by the user. Uses time-based invalidation: if a new activity arrives after the mark timestamp, it is considered unread. Enterprise plan only. See Notification feeds for details.
is_seen
Boolean (true/false). Only available on feeds with track_seen enabled (e.g., notification feeds). true if the activity (or its aggregated group) has been marked as seen by the user. Same time-based invalidation as is_read. Enterprise plan only. See Notification feeds for details.
It's possible to set default values if you expect that some data might be undefined for some activities. However it's not possible to set default values to built-in fields like popularity or share_count.
Returns a normally distributed number in the range [-inf, +inf] with standard normal distribution (stddev = 1, mean = 0)
rand_normal(a,b,σ,µ)
Returns a normally distributed number in the range [a, b] with specific normal distribution (stddev = σ, mean = µ)
rand()
Returns a random number in the range [0, 1.0)
to_unix_timestamp(t)
Converts a time value to a UNIX timestamp
dist(lat1,lng1,lat2,lng2,unit)
Returns the distance between the two points given by (lat1,lng1) and (lat2,lng2). By default the unit is in kilometers, but the unit can also be M for miles or N for nautical miles. This function can be combined with the external ranking parameters to rank activities based on a user's distance to them.
map_lookup(key, map)
Look up value from a map by key, see Arrays and Maps for more information
sum_map_lookup(keys, map)
Sum values from a map for multiple keys, see Arrays and Maps for more information
in_array(needle, haystack)
Check if a value exists in an array, see Arrays and Maps for more information
Stream supports linear, exponential, and Gaussian decay. For each decay function, we support 4 arguments: Origin, Scale, Offset, and Decay. You can pass either a timedelta string (such as 3d, 4w), or a numeric value.
Parameters
name
description
default
origin
The best possible value. If the value is equal to the origin, the decay function will return 1.
now
scale
Determines how quickly the score drops from 1.0 to the decay value. If the scale is set to "3d", the score will be equal to the decay value after 3 days.
5d
offset
Values below the offset will start to receive a lower score.
0
decay
The score that a value at scale distance from the origin should receive.
0.5
direction
left, right or both. If right is specified, only apply the decay for the right part of the graph.
both
You can use s, m, h, d and w to specify seconds, minutes, hours, days and weeks respectively.
The example below defines a simple_gauss with the following parameters:
scale: 5 days
offset: 1 day
decay: 0.3
This means that an activity younger than 1 day (the offset) will return a score of 1. An activity that is exactly 6 days old (offset + scale) will get a score of 0.3 (the decay factor). The full example:
Interest weights help customize the ranking to users' interests. If you want to create a ranking expression that relies on interest weights, use interest type ranking.
A user's interests are computed automatically by the Stream API from the interest_tags on activities the user has reacted to. When ranking with interest type, the top five computed tags are used, each with a weight of 1.0.
You can also pass interest_weights when reading the feed. These weights are merged with the computed interests rather than replacing them:
Tags that appear only in the computed profile or only in interest_weights both contribute to interest_score.
If the same tag appears in both, the value from interest_weights is used (an explicit override for that tag).
If you omit interest_weights or pass an empty map, ranking uses only the computed interests.
The range for interest weights is between -1 and 1. 1 means a user is very interested in a topic.
Preference score
preference_score reflects how much an activity matches the user's preferences based on activity feedback (show more/less). Activities with interest_tags that have strong dislike (based on user feedback) are filtered out from the feed. Other activities are ranked using preference_score which incorporates the user's activity feedback.
You can use preference_score in your ranking expressions with both expression and interest type ranking to boost activities that match user preferences:
When using multiple activity selectors, each activity is tagged with a selector_source field indicating which selector provided it. This enables you to prioritize activities from specific sources in your ranking expressions.
When track_read or track_seen is enabled on a feed group, you can use is_read and is_seen as variables in ranking expressions. This lets you prioritize unread or unseen content.
Ranking by is_read / is_seen is only available on Enterprise plans. Contact support to enable this feature for your organization.
For example, to show unread notifications first and then sort by recency:
For aggregated notification feeds, is_read and is_seen are resolved at the group level. All activities within an aggregated group share the same read/seen status. When a group is marked as read, all activities in that group are treated as read for ranking purposes.
Ranking by is_read / is_seen requires notification tracking to be enabled on the feed group (track_read / track_seen). If tracking is not enabled, these variables are not available in ranking expressions.
External data lets you add dynamic data to your ranking expressions. External data is provided when reading the feed, so you can provide user-specific data here.
Accessing external data in ranking expressions
name
description
external
The ranking configuration provided when reading the feed, nested fields can be accessed with . notation, for example: external.foo
serverClient.feeds.createFeedGroup({ id: "myid", ranking: { type: "expression", score: "popularity * external.popularity_multiplier + share_count * external.share_multiplier", defaults: { external: { // Provide default values if external data isn't provided when reading the feed popularity_multiplier: 1, share_multiplier: 1, }, }, },});
await timeline.getOrCreate({ external_ranking: { popularity_multiplier: 1, // normal weight for popularity share_multiplier: 100, // very high boost for share_count },});
await timeline.getOrCreate({ external_ranking: { popularity_multiplier: 1, // normal weight for popularity share_multiplier: 100, // very high boost for share_count },});
await timeline.getOrCreate({ external_ranking: { popularity_multiplier: 1, // normal weight for popularity share_multiplier: 100, // very high boost for share_count },});
await timeline.getOrCreate({ external_ranking: { popularity_multiplier: 1, // normal weight for popularity share_multiplier: 100, // very high boost for share_count }, user_id: "<user id>",});
Stream provides three powerful functions for working with arrays and maps in ranking expressions. These functions are particularly useful for creating dynamic, personalized ranking algorithms that can respond to external data.
map_lookup(key, map)
Looks up a single value from a map by key. If the key doesn't exist, it returns 0.0.
await timeline.getOrCreate({ external_ranking: { watchlist: ["apple", "tesla", "microsoft"], },});// activities with stocks in the watchlist get a +10 boost, others get no boost.
await timeline.getOrCreate({ external_ranking: { watchlist: ["apple", "tesla", "microsoft"], },});// activities with stocks in the watchlist get a +10 boost, others get no boost.
await timeline.getOrCreate({ external_ranking: { watchlist: ["apple", "tesla", "microsoft"], },});// activities with stocks in the watchlist get a +10 boost, others get no boost.
await timeline.getOrCreate({ external_ranking: { watchlist: ["apple", "tesla", "microsoft"], }, user_id: "<user id>",});// activities with stocks in the watchlist get a +10 boost, others get no boost.
$feedResponse = $feedsClient->feed('timeline', 'john')->getOrCreateFeed( new GeneratedModels\GetOrCreateFeedRequest( externalRanking: (object) [ 'watchlist' => ['apple', 'tesla', 'microsoft'] ], userID: "john" ));// activities with stocks in the watchlist get a +10 boost, others get no boost.
response, err := timeline.GetOrCreate(context.Background(), &getstream.GetOrCreateFeedRequest{ ExternalRanking: map[string]any{ "watchlist": []string{"apple", "tesla", "microsoft"}, }, UserID: getstream.PtrTo("john"),})if err != nil { log.Fatal("Error getting or creating timeline:", err)}// activities with stocks in the watchlist get a +10 boost, others get no boost.
Feed groups let you define what activities should be included in the feed and the ranking to sort these activities.
By default all feeds in the given group will have the same settings. However, you might want to experiment with different selectors and rankings. Feed views let you do that by overriding the group's default settings.
Note that any write operation to feed groups/views can take up to 30 seconds to propagate to all API nodes.