// Step 1: Setup the test data
const stream = require('getstream');
const request = require('request');
const API_KEY = process.env.STREAM_API_KEY;
const API_SECRET = process.env.STREAM_API_SECRET;
if (!API_KEY || !API_SECRET) {
throw new Error('Expected STREAM_API_KEY, STREAM_API_SECRET env vars');
}
const client = stream.connect(API_KEY, API_SECRET);
// 1. Setup the test data
request.get('http://bit.ly/test-activities-gist', (error, response, body) => {
if (error) {
console.log(error);
} else {
const activities = JSON.parse(body);
client.feed('user', 'global').addActivities(activities);
}
});
Instagram Tutorial
Tutorial: Instagram Style Personalization
This tutorial will explain how to build Instagram style personalization using Stream. The underlying tech is similar to that of a Pinterest style feed, Etsy’s e-commerce recommendations, email digests (such as Quora) or YouTube’s content suggestions.
The goal of this tutorial is to build a feed similar to Instagram’s ‘Explore’ feed. The content is based on what type of images you’ve engaged with in the past as well as graph analysis. For example, if you often click on snowboarding pictures it will learn that you like snowboarding and display related posts.
Step 1: Test Data
Let’s get started. As a first step we’ll want to insert test data.
# Step 1: Setup the test data
import stream
import requests
client = stream.connect('{{ chat_api_key }}', '{{ api_secret }}')
activities = requests.get('http://bit.ly/test-activities-gist').json()
for activity in activities:
activity_response = client.feed('user', 'global').add_activity(activity)
print(activity_response)
# Step 1: Setup the test data
require 'stream'
require 'httparty'
client = Stream::Client.new('{{ chat_api_key }}', '{{ api_secret }}')
activities = JSON.parse(HTTParty.get('http://bit.ly/test-activities-gist'))
client.feed('user', 'global').add_activities(activities)
// Step 1: Setup the test data
$client = new GetStream\Stream\Client('{{ chat_api_key }}', '{{ api_secret }}');
$content = file_get_contents('http://bit.ly/test-activities-gist');
$activities = json_decode($content, true);
$response = $client->feed('user', 'global')->addActivities($activities);
echo json_encode($response) . "\n";
client, err := stream.New("{{ api_key }}", "{{ api_secret }}")
if err != nil {
panic(err)
}
// 1. Setup the test data
resp, err := http.Get("http://bit.ly/test-activities-gist")
if err != nil {
panic(err)
}
content, err := ioutil.ReadAll(resp.Body)
if err != nil {
panic(err)
}
defer resp.Body.Close()
var activities []stream.Activity
err = json.Unmarshal(content, activities)
if err != nil {
panic(err)
}
feed, err := client.FlatFeed("user", "global")
if err != nil {
panic(err)
}
_, err := feed.AddActivities(context.TODO(), activities...)
if err != nil {
panic(err)
}
If you run the above snippet you’ll insert 3 activities with the following images and tags:
Step 2: Analytics
For this tutorial we will only track clicks. If you’re building a production application, you’ll want to track any type of event that signals a user’s interest. The example below shows how to perform the analytics tracking if a user clicks on the second image. Analytics tracking is normally done client-side, and in this case we are using the JS analytics client.
// set the current user
client.setUser({id: 123, alias: 'john'});
// track a click on the 2nd picture
var engagement = {
// the label for the engagement, ie click, retweet etc.
'label': 'click',
// the ID of the content that the user clicked
'content': {
'foreign_id': 'picture:2'
},
// score between 0 and 100 indicating the importance of this event
// IE. a like is typically a more significant indicator than a click
'score': 15,
// (optional) the position in a list of activities
'position': 2
};
client.trackEngagement(engagement);
// track a click on the 2nd picture
client.analytics().trackEngagement(Engagement.builder()
// set the current user
.userData(new UserData("123", "john"))
// the ID of the content that the user clicked
.content(new Content("picture:2"))
// the label for the engagement, ie click, retweet etc.
.label("click")
// score between 0 and 100 indicating the importance of this event
// IE. a like is typically a more significant indicator than a click
.boost(15)
// (optional) the position in a list of activities
.position(3)
.build());
The example above uses a score of 15. The score is a number between 0 and 100 and indicates the importance of an event. A click is typically less important than a like or a share, so give those events a higher score.
Step 3: Understanding Features
Those clicks give us a hint that the user is interested in that activity. There are many features attached to a given activity. For this example, we’ll take a look at the features attached to the 2nd activity:
// Analyze which features activity with foreign ID picture:2 has
const params = { foreign_id: 'picture:2' };
client.personalization.get('analyze_features', params).then(
(resolution) => console.log(resolution["response"]["data"]),
(rejection) => console.log(rejection)
);
# Analyze which features activity with foreign id picture:2 has
features = client.personalization.get('analyze_features', foreign_id='picture:2')
print(features)
# Analyze which features activity with foreign id picture:2 has
features = client.personalization.get('analyze_features', foreign_id: 'picture:2')
puts features
// Analyze which features activity with foreign id picture:2 has
$features = $client->personalization()->get('analyze_features', ['foreign_id' => 'picture:2']);
print_r($features);
client.personalization().get("analyze_features",ImmutableMap.of("foreign_id", "picture:2"));
// Analyze which features activity with foreign id picture:2 has
resp, err := client.Personalization().Get(
context.TODO(),
"analyze_features",
map[string]any{
"foreign_id": "picture:2",
},
)
if err != nil {
panic(err)
}
fmt.Println(resp)
Once you’ve ran the code, ask yourself why the user is interested in this picture. Is it because the picture is about surfing? Or perhaps it indicates that he/she likes the post since it’s created by a friend. One click doesn’t give us significant data, but as the user keeps returning to your app, an interest profile starts to form.
Step 4: Follows
Personalization works best if you combine explicit follow relationships with analytics events. The example below shows how to create a follow relationship.
// John follows user conner3400
const john = client.feed('timeline', 'john');
john.follow('user', 'conner3400');
# John follows user conner3400
client.feed('timeline', 'john').follow('user', 'conner3400')
# John follows user conner3400
client.feed('timeline', 'john').follow('user', 'conner3400')
// John follows user conner3400
$client->feed('timeline', 'john')->follow('user', 'conner3400');
// John follows user conner3400
client.flatFeed("timeline", "john").follow(client.flatFeed("user", "conner3400"));
// John follows user conner3400
john, _ := client.FlatFeed("timeline", "john")
conner, _ := client.FlatFeed("user", "conner3400")
_, err := john.Follow(context.TODO(), conner)
if err != nil {
panic(err)
}
Step 5: Reading the Personalized Feed
With both follows and analytics in place, we now have the ability to read the personalized feed.
const params = {
user_id: 'john',
feed_slug: 'timeline'
};
client.personalization.get('personalized_feed', params).then(
(resolution) => console.log(resolution["response"]["data"]),
(rejection) => console.log(rejection)
);
activities = client.personalization.get('personalized_feed', feed_slug='timeline', user_id='john')
print(activities)
activities = client.personalization.get('personalized_feed', feed_slug: 'timeline', user_id: 'john')
puts activities
$activities = $client->personalization()->get('personalized_feed', ['feed_slug' => 'timeline', 'user_id' => 'john']);
print_r($activities);
client.personalization().get("personalized_feed", new ImmutableMap.Builder<string, object="">()
.put("user_id", "john")
.put("feed_slug", "timeline")
.build());</string,>
// Reading the personalized feed
resp, err := client.Personalization().Get(context.TODO(), "personalized_feed", map[string]any{
"feed_slug": "timeline",
"user_id": "john",
})
if err != nil {
panic(err)
}
Step 6: Follow Suggestions
With the follow relationships in place, we have enough data to create follow suggestions.
const params = {
user_id: 'john',
source_feed_slug: 'timeline',
target_feed_slug: 'user'
};
client.personalization.get('follow_recommendations', params).then(
(resolution) => console.log(resolution["response"]["data"]),
(rejection) => console.log(rejection)
);
suggestions = client.personalization.get('follow_recommendations', target_feed_slug='user', user_id='john', source_feed_slug='timeline')
print(suggestions)
suggestions = client.personalization.get('follow_recommendations', user_id: 'john', target_feed_slug: 'user', source_feed_slug: 'timeline')
puts suggestions
$suggestions = $client->personalization()->get('follow_recommendations', [
'user_id' => 'john',
'source_feed_slug' => 'timeline',
'target_feed_slug' => 'user',
]);
print_r($suggestions);
Map<string, object=""> suggestions = client.personalization().get("follow_recommendations", new ImmutableMap.Builder<string, object="">()
.put("user_id", "john")
.put("source_feed_slug", "timeline")
.put("target_feed_slug", "user")
.build()).get();</string,></string,>
// Follow suggestions
resp, err := client.Personalization().Get(context.TODO(), "follow_recommendations", map[string]any{
"user_id": "john",
"source_feed_slug": "timeline",
"target_feed_slug": "user",
})
if err != nil {
panic(err)
}
This tutorial explained a simplified version of personalization for your app.
These algorithms need to be customized for each enterprise customer. Contact our data science team to learn how personalization can enhance your app.