Ranking

Stream allows you to configure your own ranking method to determine the order of activities inside a feed.

Info:

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",
    ),
)
Info:

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_below are removed from the ranked feed. Activities with a score equal to the threshold are kept, and 0 is a valid threshold.
  • When exclude_below is 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 NaN or -Infinity), that activity is removed.
  • If every activity scores below the threshold, the feed is empty.
Info:

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.

Info:

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:

  1. score descending
  2. 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 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 using aggregation.score_strategy
  • reaction count of each activity (the sum of all reactions)
  • standard fields like popularity can also be observed when reading the feed
  • the values of the variables the ranking expression used (score_vars), when you set include_score_vars: true in 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.

Info:

Note that any write operation to feed groups/views can take up to 30 seconds to propagate to all API nodes.