Ranking
Stream allows you to configure your own ranking method to determine the order of activities inside a feed.
Ranked feeds are not available for free plans. Contact support after upgrading your account to enable ranked feeds for your organization.
An example ranking config is shown below:
from getstream.models import ActivitySelectorConfig, RankingConfig
client.feeds.create_feed_group(
id="mytimeline",
activity_selectors=[ActivitySelectorConfig(type="following")],
ranking=RankingConfig(
type="expression",
score="decay_linear(time) * popularity",
),
)You can also update built-in feed groups with your own ranking config:
response = client.feeds.update_feed_group(
"myid",
# Fields to update, e.g.:
# activity_processors=[...],
# activity_selectors=[...],
# ranking=RankingConfig(type="expression", score="popularity"),
# custom={"description": "Updated feed group"},
)A feed without a ranking config is ordered by recency, newest first. To rank a feed, set type to one of:
| name | description |
|---|---|
expression |
Uses a custom mathematical expression to calculate scores for ranking activities |
interest |
expression ranking extended with interest weights |
See Ranking expressions for the syntax of 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 feeds without a ranking config 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:
from getstream.models import ActivitySelectorConfig, RankingConfig
client.feeds.create_feed_group(
id="myid",
activity_selectors=[ActivitySelectorConfig(type="following")],
ranking=RankingConfig(
type="expression",
score="popularity",
),
)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. To lower the share of a popular, proximity, interest or query selector, set ranking_candidate_limit on it.
Excluding low-scoring activities
Set exclude_below on the ranking config of a feed group or feed view to remove activities whose score is too low. It works with expression and interest ranking.
A common pattern is to have the score expression return a sentinel such as -1 for activities that should be hidden, for example events that have already started, and remove them with exclude_below: 0:
{
"ranking": {
"type": "expression",
"score": "custom.starts_at > 0 && custom.starts_at < current_time ? -1 : decay_linear(time) * popularity",
"exclude_below": 0,
"defaults": {
"custom": { "starts_at": 0 }
}
}
}Here custom.starts_at is a Unix timestamp and current_time is the built-in current time. Activities without a starts_at default to 0 and are kept.
- Activities with a score strictly less than
exclude_beloware removed from the ranked feed. Activities with a score equal to the threshold are kept, and0is a valid threshold. - When
exclude_belowis not set, ranking behaves as before. - Activities are removed before pagination, so pages stay full and consistent. Unlike activities that are only sorted to the bottom, excluded activities can't be reached by paginating deeper.
- If the score expression doesn't produce a number for an activity (or produces
NaNor-Infinity), that activity is removed. - If every activity scores below the threshold, the feed is empty.
exclude_below is not supported together with aggregation. Creating or updating a feed group or feed view with both returns the error ranking.exclude_below is not supported with aggregation. A feed view without its own aggregation that is read on an aggregated feed group is accepted, but exclude_below is ignored there.
After changing the threshold, cached ranked results can take up to about 60 seconds to reflect it, or up to 10 minutes for some shared ranked feeds.
Arranging the ranked feed
To control where activities are placed without changing their scores, for example to interleave two activity types at an 80/20 ratio, add arrange to the ranking config. See Feed arrangement.
Ranking aggregated feed groups
If your feed also uses aggregation, you can rank aggregated groups by score instead of the default updated_at ordering.
Set aggregation.score_strategy together with ranking:
{
"aggregation": {
"format": "{{ type }}_{{ time.strftime('%Y-%m-%d') }}",
"score_strategy": "max"
},
"ranking": {
"type": "expression",
"score": "popularity * 2 + comment_count"
}
}Supported score_strategy values:
| value | behavior |
|---|---|
sum |
Sum of the member activity scores in the group |
max |
Highest member activity score in the group |
avg |
Average of the member activity scores in the group |
Only members that are still in the ranked feed count, so activities removed by exclude_below or filters don't contribute. A group with no ranked members sorts last.
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).
Aggregated score sorting and pagination
When score_strategy is configured, aggregated groups are sorted by:
scoredescendingupdated_atdescending (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 the cursor no longer matches the current ranking, for example because external_ranking changed between page requests, the request fails with pagination token expired, please refresh the feed and you should restart pagination from page 1.
Aggregated score in responses
When score-based aggregation ranking is enabled, aggregated_activities[].score contains the computed group score:
{
"aggregated_activities": [
{
"group": "reaction_2026-03-30",
"score": 42.5
}
]
}Ranking expression syntax
The syntax for ranking expressions, including the activity fields, functions, operators, constants and decay functions you can use, is described in Ranking expressions.
Interest weights
With interest ranking, expressions can weigh activities by how well their topics match each user's interests. See Interest ranking.
Ranking by Read/Seen Status
Feeds that track read or seen state can use is_read and is_seen in ranking expressions. See Read/seen ranking.
External data
You can pass your own data, such as per-user boosts, when reading a feed and use it in the ranking expression. See External data.
Inspecting ranking variables
When reading the feed, you can access the:
- score of each activity (this is the result of the ranking expression)
- score of each aggregated group (
aggregated_activities[].score) when usingaggregation.score_strategy - reaction count of each activity (the sum of all reactions)
- standard fields like
popularitycan also be observed when reading the feed - the values of the variables the ranking expression used (
score_vars), when you setinclude_score_vars: truein the enrichment options. They are only returned on freshly ranked pages, not on pages served from a cache
These variables can help you understand the result of the ranking expression.
response = client.feeds.feed("timeline", "john").get_or_create(user_id="john")
activity = response.data.activities[0]
print(activity.score)
print(activity.reaction_count)
print(activity.popularity)Configuring ranking can be complex. Feel free to reach out to support if you have questions!
Experimenting with ranking
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.