// Note: Feed group management is typically done server-side
// This is an example of the configuration structure
let feedGroupConfig: [String: Any] = [
"id": "myid",
"activity_processors": [["type": "topic"]], // activity processors do post processing on activities. AI topic analysis, etc.
"activity_selectors": [["type": "following"]], // control how activities end up in this feed. direct, following, search, querying etc.
"ranking": ["type": "time"], // how activities are ranked
"aggregation": ["type": "count"], // group activities together
"notification": ["type": "count"], // keep track of seen and read state on notification feeds
"custom": ["description": "My custom feed group"] // anything else to store on the feed group
]
Intro & Defaults
Creating Feed Groups & Defaults
There are several feed groups setup by default.
Group | Description |
---|---|
user | A feed setup for the content a user creates. Typically you add activities here when someone writes a post |
timeline | The timeline feed is used when you’re following. So if user Charlie is following John, timeline:charlie would follow user:john |
foryou | A version of the timeline feed that adds popular content, and priorities popularity over recency |
notification | A notification feed. Think of the bell icon you see in most apps |
You can also create your own feed group. Here’s how to create a feed group using the API:
const view = await serverClient.feeds.createFeedView({
view_id: uuidv4(),
activity_selectors: [
{
type: "following",
},
],
ranking: { type: "time" },
// Group by activity type
aggregation: { type: "count" },
});
const response = await serverClient.feeds.createFeedGroup({
feed_group_id: "myid",
default_view_id: view.feed_view.view_id,
activity_processors: [
{
type: "topic",
},
],
notification: {
track_read: true,
track_seen: true,
},
custom: {
description: "My custom feed group",
},
});
For You Feeds
Many apps want to have a “for you” or personalized feed. There are a couple benefits to a personalized feed:
- Works well even if your users don’t spend much time setting up follows
- Can be a mechanism to discover new content or things to follow
Stream offers a few built-in methods to create a for you feed and gives you the API access to do more advanced customization if needed.
Activity Ranking
When ranking activities you can specify a ranking formula. This can combine an age decay with popularity and other factors.
// Note: This is typically configured server-side
let rankingConfig = [
"type": "time",
"score": "decay_linear(time) * popularity"
]
let feedGroupConfig: [String: Any] = [
"id": "timeline",
"ranking": rankingConfig
]
const view = await serverClient.feeds.createFeedView({
view_id: uuidv4(),
ranking: { type: "time", score: "decay_linear(time) * popularity" },
activity_selectors: [
{
type: "following",
},
],
});
const response = await serverClient.feeds.createFeedGroup({
feed_group_id: "mytimeline",
default_view_id: view.feed_view.view_id,
});
Interest Based “For You” Style Feed
This next example is a bit more complicated. It uses an activity processor to add topic data to activities and ranks content you’re likely to engage with higher in the feed.
// Note: This is typically configured server-side
// Run the activity processors to analyse topics for text & images
let imageProcessor = ["type": "image_topic"]
let textProcessor = ["type": "text_topic"]
let userFeedConfig: [String: Any] = [
"id": "user",
"activity_processors": [imageProcessor, textProcessor]
]
// Activity selectors change which activities are included in the feed
// The default "following" selectors gets activities from the feeds you follow
// The "popular" activity selectors includes the popular activities
// And "interest" activities similar to activities you've engaged with in the past
// You can use multiple selectors in 1 feed
let activitySelectors = [
["type": "popular"],
["type": "following"],
["type": "interest"]
]
// Rank for a user based on interest score
// This calculates a score 0-1.0 of how well the activity matches the user's prior interest
let rankingConfig = [
"type": "interest",
"score": "decay_linear(time) * interest * decay_linear(popularity)"
]
let timelineConfig: [String: Any] = [
"id": "timeline",
"ranking": rankingConfig,
"activity_selectors": activitySelectors
]
// Read the feed
// Activities will include following, popular and similar (via interest) activities
// Sorted by time decay, interest and popularity
let forYouFeed = client.feed(group: "timeline", id: "thierry")
let response = try await forYouFeed.getOrCreate()
const view = await serverClient.feeds.createFeedView({
view_id: uuidv4(),
ranking: { type: 'expression', score: 'decay_linear(time) * popularity' },
// Activity selectors change which activities are included in the feed
// The default "following" selectors gets activities from the feeds you follow
// The "popular" activity selectors includes the popular activities
// And "interest" activities similar to activities you've engaged with in the past
// You can use multiple selectors in 1 feed
activity_selectors: [
{
type: 'popular',
},
{
type: 'following',
},
{
type: 'interest',
},
],
// Rank for a user based on interest score
// This calculates a score 0-1.0 of how well the activity matches the user's prior interest
ranking: {
type: "interest",
score: "decay_linear(time) * interest * decay_linear(popularity)"
},
});
const response = await serverClient.feeds.createFeedGroup({
feed_group_id: 'mytimeline',
default_view_id: view.feed_view.view_id,
// Run the activity processors to analyse topics for text & images
activity_processors: [
{type: 'image_topic'},
{type: 'text_topic'}
]
});
const forYouFeed = client.feeds.feed(group: "mytimeline", id: "thierry")
const response = await forYouFeed.getOrCreate({ user_id: 'thierry' })
Additional Ranking
There are a few ways that you can extend the ranking in feeds. This allows you pass in any insights you have about the user.
// Pass external ranking data when reading the feed
let query = FeedQuery(
group: "timeline",
id: "thierry",
externalRanking: [
"user_engagement_score": 0.8,
"content_preference": 0.9,
"time_of_day_bonus": 1.2
]
)
let feed = client.feed(for: query)
let feedData = try await feed.getOrCreate()
// Pass external ranking data when reading the feed
const externalRanking = {
user_engagement_score: 0.8,
content_preference: 0.9,
time_of_day_bonus: 1.2,
};
const response = await feed.getOrCreate({
external_ranking: externalRanking,
});
// Pass external ranking data when reading the feed
const externalRanking = {
user_engagement_score: 0.8,
content_preference: 0.9,
time_of_day_bonus: 1.2,
};
const response = await feed.getOrCreate({
external_ranking: externalRanking,
user_id: "<user id>",
});
You can also rank on custom data in the activity:
let activity = try await feed.addActivity(
request: .init(
custom: [
"quality_score": 0.95,
"engagement_prediction": 0.8
],
text: "Great content",
type: "post"
)
)
const activity = await feed.addActivity({
type: "post",
text: "Great content",
custom: {
quality_score: 0.95,
engagement_prediction: 0.8,
},
});
const activity = await client.feeds.addActivity({
fids: [feed.fid],
type: "post",
text: "Great content",
custom: {
quality_score: 0.95,
engagement_prediction: 0.8,
},
user_id: "<user id>",
});
Ranking
The ranking system allows you to customize how activities are ordered in your feeds. Here are some common ranking strategies:
Time-based Ranking
// Simple time-based ranking (newest first)
let timeRanking = [
"type": "time",
"direction": "desc"
]
// Simple time-based ranking (newest first)
const timeRanking = {
type: "time",
direction: "desc",
};
Popularity-based Ranking
// Rank by popularity (likes, comments, shares)
let popularityRanking = [
"type": "popularity",
"score": "likes + comments * 2 + shares * 3"
]
// Simple time-based ranking (newest first)
const popularityRanking = {
type: "popularity",
score: "likes + comments * 2 + shares * 3",
};
Hybrid Ranking
// Combine time decay with popularity
let hybridRanking = [
"type": "hybrid",
"score": "decay_linear(time, 24h) * (likes + comments + shares)"
]
// Combine time decay with popularity
const hybridRanking = {
type: "hybrid",
score: "decay_linear(time, 24h) * (likes + comments + shares)",
};
Interest-based Ranking
// Rank based on user interests and engagement history
let interestRanking = [
"type": "interest",
"score": "decay_linear(time) * interest_match * engagement_prediction"
]
// Rank based on user interests and engagement history
const interestRanking = {
type: "interest",
score: "decay_linear(time) * interest_match * engagement_prediction",
};
Aggregation & Notification Feeds
Aggregation groups similar activities together, which is useful for notification feeds and reducing noise.
Aggregation Types
// Count aggregation - shows "John and 5 others liked your post"
let countAggregation: [String: Any] = [
"type": "count",
"group_by": ["activity_id", "reaction_type"]
]
// Time-based aggregation - groups activities within a time window
let timeAggregation = [
"type": "time",
"window": "1h"
]
// User-based aggregation - groups activities by user
let userAggregation: [String: Any] = [
"type": "user",
"group_by": ["user_id"]
]
// Count aggregation - shows "John and 5 others liked your post"
const countAggregation = {
type: "count",
group_by: ["activity_id", "reaction_type"]
}
// Time-based aggregation - groups activities within a time window
const timeAggregation = {
type: "time",
window: "1h"
}
// User-based aggregation - groups activities by user
const userAggregation = [
type: "user",
group_by: ["user_id"]
]
Notification Feed Example
// Create a notification feed with aggregation
let notificationFeed = client.feed(group: "notification", id: "john")
// Configure it with aggregation
let notificationConfig = [
"aggregation": countAggregation,
"ranking": ["type": "time", "direction": "desc"]
]
// Read notifications
let notifications = try await notificationFeed.getOrCreate()
// Built in notification group
const notificationFeed = client.feed(group: "notification", id: "john")
// Read notifications
const notifications = (await notificationFeed.getOrCreate({
limit: 20,
})).aggregated_activities;
// Built in notification group
const notificationFeed = client.feed(group: "notification", id: "john")
// Or create your own config
const view = await serverClient.feeds.createFeedView({
view_id: uuidv4(),
ranking: { type: 'time', direction: 'desc' },
aggregation: {
type: "count",
group_by: ["activity_id", "reaction_type"]
}
});
const response = await serverClient.feeds.createFeedGroup({
feed_group_id: 'mynotification',
default_view_id: view.feed_view.view_id,
notification: {
track_read: true,
track_seen: true,
}
});
// Read notifications
const notifications = (await notificationFeed.getOrCreate({
limit: 20,
user_id: '<user_id>'
})).aggregated_activities;
Marking Notifications as Read
// Mark specific notifications as read
try await notificationFeed.markActivity(
request: .init(
markRead: ["notification_1", "notification_2"]
)
)
// Mark all notifications as read
try await notificationFeed.markActivity(
request: .init(
markAllRead: true
)
)
// Mark specific notifications as read
await notificationFeed.markActivity({
mark_read: ["notification_1", "notification_2"],
});
// Mark all notifications as read
await notificationFeed.markActivity({
mark_all_read: true,
});
// Mark specific notifications as read
await notificationFeed.markActivity({
mark_read: ["notification_1", "notification_2"],
user_id: "<user id>",
});
// Mark all notifications as read
await notificationFeed.markActivity({
mark_all_read: true,
user_id: "<user id>",
});