const response = await serverClient.feeds.createFeedGroup({
id: "mytimeline",
ranking: { type: "expression", score: "decay_linear(time) * popularity" },
activity_selectors: [
{
type: "following",
},
],
});
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:
Supported types for configuration:
name | description |
---|---|
recency | 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.
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:
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_counts | How many comments the activity has |
bookmark_counts | 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 |
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. |
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:
name | example |
---|---|
Boolean | true , false |
Integer | 36 , 0b101010 |
Float | 0.2 |
String | "foo" |
Array | ["a", "b", "c"] |
Map | {foo: "bar"} |
Nil | nil |
Functions
The following functions can be used in ranking expressions:
name | description |
---|---|
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:
name | description |
---|---|
e / E | Euler’s number |
pi / PI | π |
current_time | UNIX 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
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:
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,
},
});
await timeline.getOrCreate({
interest_weights: {
travel: 1,
food: 0.5,
exercise: -1,
},
user_id: "<user id>",
});
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
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,
},
},
},
});
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
},
});
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>",
});
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);
const feed = client.feeds.feed("user", "sara");
const response = await feed.getOrCreate({ user_id: "sara" });
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.
- I'm working with the Stream Feeds Node SDK and would like to ask questions about this documentation page: https://getstream.io/activity-feeds/docs/node/custom_ranking.md
- View as markdown
- Open in ChatGPT
- Open in Claude