Activity Feeds V3 is in closed alpha — do not use it in production (just yet).

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:

const response = await serverClient.feeds.createFeedGroup({
  id: "mytimeline",
  ranking: { type: "expression", score: "decay_linear(time) * popularity" },
  activity_selectors: [
    {
      type: "following",
    },
  ],
});

Supported types for configuration:

namedescription
recencyRanks activities based on their timestamp, with newer activities appearing first
expressionUses a custom mathematical expression to calculate scores for ranking activities
interestexpression ranking extended with interest weights

Read on to learn the syntax for expression and interest based ranking.

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:

await serverClient.feeds.createFeedGroup({
  id: "myid",
  activity_selectors: [{ type: "following" }],
  ranking: {
    type: "expression",
    score: "popularity",
  },
});

Custom ranking only works on the last 1000 activities of the feed. The 1000 activities limit is applied after activities are filtered with activity selectors.

Ranking expression syntax

Activity data

It’s not possible to access all fields of an ActivityResponse inside the ranking context. Below you’ll find the list of supported fields:

namedescription
timeUNIX timestamp of when the activity was created
popularityThe popularity score of an activity (formula: activity.popularity = reactions + comments * 2 + bookmarks * 3 + shares * 3)
reaction_countThe sum of all reactions
reaction_countsReaction counts by type, you can access specific types with dot notation (for example, reaction_counts.like)
comment_countsHow many comments the activity has
bookmark_countsHow many bookmarks the activity has
share_countHow many shares the activity has (Increased when an activity is set as the parent_id of another activity)
customThe custom field of the activity, nested fields can be accessed with . notation, for example: custom.topic
interest_scoreOnly 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.

It’s possible to set default values if you expect that some data might be undefined for some activities:

await serverClient.feeds.createFeedGroup({
  id: "myid",
  activity_selectors: [{ type: "following" }],
  ranking: {
    type: "expression",
    score: "popularity * (custom.isBoosted ? 1.5 : 1)",
    defaults: {
      custom: {
        isBoosted: false,
      },
    },
  },
});

Data types

The following data types are supported in the ranking expression context:

nameexample
Booleantrue, false
Integer36, 0b101010
Float0.2
String"foo"
Array["a", "b", "c"]
Map{foo: "bar"}
Nilnil

Functions

The following functions can be used in ranking expressions:

namedescription
ln(x)Natural logarithm function
log10(x)Logarithm function base 10
sin(x)Trigonometric sine function
cos(x)Trigonometric cosine function
tan(x)Trigonometric tangent function
asin(x)Arcsine function
acos(x)Arccosine function
atan(x)Arctangent function
abs(x)Absolute value
min(a,b)Minimum value
max(a,b)Maximum value
trunc(x)Truncates to the nearest integer value
round(x)Rounds to the nearest integer value
decay_linear(t)Linear decay, see Decay & Ranking for more information
decay_exp(t)Exponential decay, see Decay & Ranking for more information
decay_gauss(t)Gaussian decay, see Decay & Ranking for more information
rand_normal()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
sum_map_lookup(keys, map)Sum values from a map for multiple keys
in_array(needle, haystack)Check if a value exists in an array

Operators

The following operators can be used in ranking expressions:

  • Arithmetic operators: +, -, *, /, % (modulus), ^, ** (exponent)

  • Logical operators: not/!, &&/ and, ||/or

  • Conditional: x ? y : z, ??, if/else

  • Comparison: ==, !=, <, >, <=, >=

  • Control order of evaluation with parentheses ()

  • Array/map operators: [] (index), ., ?., in (checks if an array/map has an item/key)

  • String operators: + (concatenation), contains, startsWith, endsWith

This lets you construct a scoring algorithm like this:

"score":"(a > 2 || (b > 4 && c > 3)) ? 1 : -1"

Constants

The following constants and helpers are also available to use in ranking expressions:

namedescription
e / EEuler’s number
pi / PIπ
current_timeUNIX timestamp of the current time

Decay & Ranking

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

namedescriptiondefault
originThe best possible value. If the value is equal to the origin, the decay function will return 1.now
scaleDetermines 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
offsetValues below the offset will start to receive a lower score.0
decayThe score that a value at scale distance from the origin should receive.0.5
directionleft, 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:

await serverClient.feeds.createFeedGroup({
  id: "myid",
  activity_selectors: [{ type: "following" }],
  ranking: {
    type: "expression",
    functions: {
      simple_gauss: {
        base: "decay_gauss",
        scale: "5d",
        offset: "1d",
        decay: "0.3",
      },
      popularity_gauss: {
        base: "decay_gauss",
        scale: "100",
        offset: "5",
        decay: "0.5",
      },
    },
    score: "simple_gauss(time)*popularity_gauss(popularity)",
  },
});

Interest weights

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.

serverClient.feeds.createFeedGroup({
  id: "myid",
  ranking: {
    type: "interest",
    score: "interest_score * popularity",
  },
});

interest_score is computed by combining activity topics and the specific user’s interests. The result is a number between 0-1.

Activity topics

The topic of an activity can be computed automatically using activity processors or by setting the interest_tags field via the Stream API.

User interests

A user’s interests can be computed automatically by the Stream API based on which activities a user interacts with. It’s also possible to provide interest weights when reading the feed (which override the automatically computed weights):

await timeline.getOrCreate({
  interest_weights: {
    travel: 1,
    food: 0.5,
    exercise: -1,
  },
});

The range for interest weights is between -1 and 1. 1 means a user is very interested in a topic.

External data

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

namedescription
externalThe 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,
      },
    },
  },
});

Providing external data when reading the feed

await timeline.getOrCreate({
  external_ranking: {
    popularity_multiplier: 1, // normal weight for popularity
    share_multiplier: 100, // very high boost for share_count
  },
});

Inspecting ranking variables

When reading the feed, you can access the:

  • score of each activity (this is the result of the ranking expression)
  • reaction count of each activity (the sum of all reactions)
  • standard fields like popularity can also be observed when reading the feed

These variables can help you understand the result of the ranking expression.

const feed = client.feed("user", "sara");
const response = await feed.getOrCreate();

console.log(response.activities[0].score);
console.log(response.activities[0].reaction_count);
console.log(response.activities[0].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.

© Getstream.io, Inc. All Rights Reserved.