In this tutorial you will build your first Tamber recommendation engine using your own data. It should take 10-40 minutes of work, depending on whether or not you load historical data, and some extra time for seeding data and training. Feel free to use that time however you want.
Tamber learns to make recommendations by observing what users do in your app. The track method lets you record any behaviors (like, purchase, click, etc.) that your users perform. Users and items that do not yet exist will automatically be created.
To start tracking events, add the snippet below anywhere that you handle user actions in your app. Just replace the project_key with your own project's public API key (found in your project's dashboard).
var tamber = require('tamber')('project_key');
tamber.event.track({
user: "583480",
behavior: "vote",
item: {
id: "90674",
properties: {
type: "book",
title: "The Moon is a Harsh Mistress",
img: "https://img.domain.com/book/The_Moon_is_a_Harsh Mistress.jpg"
},
tags: ["sci-fi", "bestseller"]
},
context: {
page: "homepage",
section: "featured_section"
}
}).then(result => console.log(result))
.catch(err => console.log("Request failed with error:", err));
package main
import (
tamber "github.com/tamber/tamber-go"
"github.com/tamber/tamber-go/event"
"fmt"
)
func main() {
tamber.DefaultProjectKey = "project_key"
e, info, err := event.Track(&tamber.EventParams{
User: "583480",
Behavior: "vote",
Item: "90674",
})
}
@import Tamber;
[Tamber setPublishableProjectKey:@"your_project_key"];
[Tamber setUser:@"user_rlox8k927z7p"];
TMBItem *item = [TMBItem itemWithId:@"90674"
properties:@{
@"type":@"book",
@"title":@"The Moon is a Harsh Mistress",
@"img_url":@"https://img.domain.com/book/The_Moon_is_a_Harsh Mistress.jpg",
@"stock":[NSNumber numberWithInteger:34]
}
tags:@[@"sci-fi", @"bestseller"]
];
TMBEventParams *params = [TMBEventParams eventWithItem:item behavior:@"purchase" context:@{@"page": @"detail-view", @"section": @"recommended"};
[[Tamber client] trackEvent:params responseCompletion:^(TMBEventResponse *object, NSHTTPURLResponse *response, NSString *errorMessage) {
if(errorMessage){
// Handle error
} else {
object.events[0] // Event tracked
object.recs // Returns updated recommendations if params.getRecs is set - [params setGetRecs:<TMBDiscoverParams>]
}
}];
require 'tamber'
Tamber.project_key = 'project_key'
begin
e = Tamber::Event.track(
:user => '583480',
:behavior => 'vote'
:item => '90674',
)
puts e.inspect
rescue TamberError => error
puts error.message
end
$ curl https://api.tamber.com/v1/event/track \
-u project_key: \
-d user=583480 \
-d behavior=vote \
-d item=90674
Tamber tamber = new Tamber("project_key", "");
tamber.event.track(new JSONObject()
.put("user", "583480")
.put("item", "90674")
.put("behavior", "vote")
);
import tamber
tamber.project_key = 'project_key'
e = tamber.Event.track(
user='583480',
behavior='vote',
item='90674'
)
window.tamber.event.track({
user: "user_rlox8k927z7p",
behavior: "purchase",
item: {
id: "90674",
properties: {
type: "book",
title: "The Moon is a Harsh Mistress",
img: "https://img.domain.com/book/The_Moon_is_a_Harsh Mistress.jpg"
},
tags: ["sci-fi", "bestseller"]
},
context: ["homepage", "featured_section"]
});
It is important that the timestamp used to track the event matches the timestamp you store in your own Database. This is because we want to catch duplicate events when we upload our historical data.
We recommend tracking events directly from your client-side app. This makes it easy for you to:
Tracking the contexts of users' events (ex. if a user clicks on an item in a recommendation section, you might add the context tmb_recommended to the event object) allows you to make full use of Tamber's analytics tools to track how your recommendations are performing.
Read our Tamber Stats guide for more information.
The amount denotes the degree to which the behavior was performed. For binary behaviors, like vote and comment this amount is always 1. For behaviors that occur for a period of time, like hover, we recommend setting amount to the time spent consuming the item. For timed behaviors that represent consumption, like watch or listen, we recommend using the percent of the item consumed. Of course, you can also come up with your own algorithms for determining the amount of each event.
To start, click the Launch Engine button in the bottom left of your sidebar and set the desirabilities of your behaviors. As this is your first engine, we recommend using only 1-3 key behaviors, so if you have more than that toggle off the less essential behaviors. Don't worry! You can come back and refine this later.
Desirability tells Tamber how well the given behavior aligns to your business objectives (ex. Netflix would consider the watched behavior to be more desirable than the clicked behavior). These are not "weights" in a traditional sense - they're more guidelines for how Tamber will orient itself. Just make sure the proportions are set correctly, and Tamber handles the rest.
When you review your engine, you will notice an option to set the engine as the project default below the engine name at the top. The default engine is the engine used by the project when no engine-key is specified in a discover request. This allows you to easily switch between engines from the dashboard without needing to deploy new code. If you want to test with a specific engine, or use multiple engines simultaneously, you can always set an engine-key.
Once your engine is done training and is in the active state, you can begin making discover calls. We recommend loading recommendations server-side as part of normal page loading requests.
Our most powerful tool for putting machine learning personalization into your app. Add an 'up next' section to your item pages to create hyper-engaging paths of discovery that keep users clicking. discover next can be called with a user-item pair to retrieve the optimal set of items that the user should be shown next.
var tamber = require('tamber')('project_key');
const items = await tamber.discover.next({
user: "583480",
item: "90674",
number: 8
});
package main
import (
tamber "github.com/tamber/tamber-go"
"github.com/tamber/tamber-go/discover"
)
func main() {
tamber.DefaultProjectKey = "project_key"
d, info, err := discover.Next(&tamber.DiscoverNextParams{
User: tamber.IdString("583480"),
Item: tamber.IdString("90674"),
Number: tamber.Int(8)
})
}
TMBDiscoverParams *params = [TMBDiscoverParams discoverNext:@"90674" number:[NSNumber numberWithInt:8]]];
[[Tamber client] discoverNext:params responseCompletion:^(TMBDiscoverResponse *object, NSHTTPURLResponse *response, NSString *errorMessage) {
if(errorMessage){
// Handle error
} else {
object.discoveries // Array of recommendations
}
}];
require 'tamber'
Tamber.project_key = 'project_key'
Tamber::Discover.next(
:user => '583480',
:item => '90674',
:number => 8
)
$ curl https://api.tamber.com/v1/discover/next \
-u :SbWYPBNdARfIDa0IIO9L \
-d user=user_rlox8k927z7p \
-d item=item_83jx4c57r2ru \
-d number=8
Tamber tamber = new Tamber("project_key", "");
JSONObject resp = new JSONObject();
try {
resp = tamber.discover.next(new JSONObject()
.put("user", "583480")
.put("item", "90674")
.put("number", 8)
);
} catch (TamberException e) {
System.out.println(String.format("%s=%s", e.getClass().getName(), e.getMessage()));
}
import tamber
tamber.project_key = 'project_key'
tamber.Discover.next(
user='583480',
item='90674',
number=8
)
You can also use tamber to put a recommended section on your homepage. Just call discover recommended for the given user and tamber will return a set of items that the user is likely to want to engage with.
var tamber = require('tamber')('project_key');
const items = await tamber.discover.recommended({
user: "583480",
number: 8
});
package main
import (
tamber "github.com/tamber/tamber-go"
"github.com/tamber/tamber-go/discover"
)
func main() {
tamber.DefaultProjectKey = "project_key"
d, info, err := discover.recommended(&tamber.DiscoverRecommendedParams{
User: tamber.IdString("583480"),
Number: tamber.Int(8)
})
}
TMBDiscoverParams *params = [TMBDiscoverParams discoverRecommended:[NSNumber numberWithInt:8]];
[[Tamber client] discoverRecommended:params responseCompletion:^(TMBDiscoverResponse *object, NSHTTPURLResponse *response, NSString *errorMessage) {
if(errorMessage){
// Handle error
} else {
object.discoveries // Array of recommendations
}
}];
require 'tamber'
Tamber.project_key = 'project_key'
Tamber::Discover.recommended(
:user => '583480',
:number => 8
)
$ curl https://api.tamber.com/v1/discover/recommended \
-u :SbWYPBNdARfIDa0IIO9L \
-d user=user_rlox8k927z7p
Tamber tamber = new Tamber("project_key", "");
JSONObject resp = new JSONObject();
try {
resp = tamber.discover.recommended(new JSONObject()
.put("user", "583480")
.put("number", 8)
);
} catch (TamberException e) {
System.out.println(String.format("%s=%s", e.getClass().getName(), e.getMessage()));
}
import tamber
tamber.project_key = 'project_key'
tamber.Discover.recommended(
user='583480',
number=8
)
Have any lists or feeds in your app? Sort them by recommendation score by replacing your internal Database query with a call to discover recommended, and then get the items' metadata from your own DB and transform the tamber discoveries to the format your client expects.
There are a lot of things you can do to customize and improve your recommendation experiences.
Once you are streaming events as they are received by your backend, you can upload a list of past events to teach Tamber the full history of your app. To generate your historical dataset, use the sample code in your language of choice. The sample expects that you will write your own database function for reading events in batches, ideally sorting by the time the events were created.
const fs = require('fs');
const zlib = require('zlib');
const csv = require('csv');
const mydb = require('myappdatabase');
const EventsFilename = 'events.csv',
MaxTimestamp = 1592301489, // Unix Timestamp of when we began streaming real time events
BatchSize = 1000;
var gzip = zlib.createGzip();
var writer = fs.createWriteStream(EventsFilename);
var stringifier = csv.stringify({columns:["user", "item", "behavior", "amount", "created"], delimiter:','});
stringifier.on('readable', function(){
while(data = stringifier.read()){
writer.write(data);
}
});
for(var offset = 0; ; offset+=BatchSize) {
var err, events = mydb.loadEvents(BatchSize, offset, MaxTimestamp); // events should be array of events objects
if (err); // handle err
if (events.length < 1){
break;
}
events.map(function(event) {
stringifier.write(event);
});
}
var reader = fs.createReadStream(EventsFilename);
var gzWriter = fs.createWriteStream(EventsFilename + ".gz");
reader.pipe(gzip);
gzip.pipe(gzWriter);
package main
import (
tamber "github.com/tamber/tamber-go"
"github.com/tamber/tamber-go/event"
"fmt"
DB "myapp/database"
)
const (
BatchSize = 1000
EventsFilepath = "./events.csv"
MaxTimestamp = 1592301489 // Unix Timestamp of when we began streaming real time events
)
func main() {
for offset := 0; ; offset += BatchSize {
events, err := DB.LoadEvents(BatchSize, offset, MaxTimestamp) // load events created before MaxTimestamp
if err != nil {
panic(err)
}
if len(events) == 0 {
break
}
err = event.BatchEventsToCSV(events, EventsFilepath)
if err != nil {
panic(err)
}
}
tamber.Gzip(EventsFilepath) // saves to EventsFilepath + ".gz"
}
// Not supported
#!/usr/bin/env ruby
require 'csv'
require 'zlib'
EventsFilepath = "events.csv"
BatchSize = 1000
MaxTimestamp = 1592301489
CSV.open(EventsFilepath, 'w') do |csv|
i = 0
loop do
# Load events from your database in batches, returning Array of event Hashes
# ex. [{:user => 'user_zoa8jxpnez83', :item => 'item_83jx4c57r2ru', :behavior => 'purchase', :amount => 1, :created => 1454465400}
events = MyDatabase.loadEvents(BatchSize, BatchSize*i, MaxTimestamp)
if events.size < 1
break
end
events.each do |event|
csv << [event[:user], event[:item], event[:behavior], event[:amount], event[:created]]
end
i += 1
end
end
# Compress for uploading
Zlib::GzipWriter.open(EventsFilepath+'.gz') do |gz|
gz.orig_name = EventsFilepath
gz.write IO.binread(EventsFilepath)
puts "File has been archived"
end
$ curl https://api.tamber.com/v1/discover/hot \
-u :SbWYPBNdARfIDa0IIO9L
Tamber tamber = new Tamber("Mu6DUPXdDYe98cv5JIfX", "SbWYPBNdARfIDa0IIO9L");
HashMap<String, Object> discoverParams = new HashMap<String, Object>();
discoverParams.put("number", 50);
JSONObject resp = new JSONObject();
try {
resp = tamber.discover.hot(discoverParams);
} catch (TamberException e) {
System.out.println(String.format("%s=%s", e.getClass().getName(), e.getMessage()));
}
import csv, gzip, io
import tamber
import myappdatabase as db
import sys
PYTHON_VERSION = sys.version_info[0]
if PYTHON_VERSION == 2:
csv_writer = csv.writer
elif PYTHON_VERSION == 3:
csv_writer = lambda file: csv.writer(io.TextIOWrapper(file, newline="", write_through=True))
EventsFilename = "events.csv.gz"
MaxTimestamp = 1592301489 # Unix Timestamp of when we began streaming real time events
with gzip.open(EventsFilename, "w") as file:
writer = csv_writer(file)
for event in db.loadEvents(MaxTimestamp):
e = tamber.Event(event.user, event.item, event.behavior, event.amount, event.created)
writer.Write([e.user, e.item, e.behavior, e.amount, e.created])
If you use Postgres to store your events data, you can also use the convenient psql method for saving CSV:
#!/bin/bash
EVENTS_FILENAME='events.csv.gz'
MAX_TIMESTAMP=TIMESTAMP_1 # Unix Timestamp of when we began streaming real time events
psql --dbname=my_db_name --command="COPY (SELECT user, item, behavior, amount, created FROM my_events_table WHERE created < $MAX_TIMESTAMP ORDER BY created ASC) TO stdout DELIMITER ',' CSV" | gzip > $EVENTS_FILENAME
Large, historical datasets can be uploaded as either a CSV, or gzipped CSV file. The required columns are user, item, behavior, amount, and created. You may optionally include a context column (with stringified json as its value). You may include a header, or provide values in the order listed above.
user,item,behavior,amount,created
user_rlox8k927z7p,item_83jx4c57r2ru,purchase,1.0,1591331679
user,item,behavior,amount,created,context
user_rlox8k927z7p,item_83jx4c57r2ru,purchase,1.0,1591331679,"{""client"": [""iPhone"", ""ios"", ""device_phone""]}"
Item propert data is used for improving recommendations, configurable boosting, aliasing, and filtering.
Filtering your recommendations in particular can be an incredibly effective way of improving relevancy. Only show users venues nearby, items that are currently in stock, or articles that came out in the last 24 hours. All you need to do to leverage Tamber's advanced real time filtering support, and all of these other benefits, is sync your items.
While you can passively capture updates automatically by including full item objects in tracked events, as shown earlier, it is better to:
Keeping your data prefectly in sync is simple. Just call item update wherever you create and update items in your backend.
var tamber = require('tamber')('Mu6DUPXdDYe98cv5JIfX');
const item = await tamber.item.update({
id: "item_wmt4fn6o4zlk",
updates: {
add: {
properties: {
"available_large": false,
"stock": 89
}
},
remove: {
tags: ["casual"]
}
}
});
package main
import (
tamber "github.com/tamber/tamber-go"
"github.com/tamber/tamber-go/item"
)
func main() {
tamber.DefaultProjectKey = "Mu6DUPXdDYe98cv5JIfX"
i, info, err := item.Update(&tamber.ItemParams{
Id: "item_wmt4fn6o4zlk",
Updates: tamber.ItemUpdates{
Add: tamber.ItemFeatures{
Properties: map[string]interface{}{
"available_large": false,
"stock": 89,
},
},
Remove: tamber.ItemFeatures{
Tags: []string{"casual"},
},
},
})
}
require 'tamber'
Tamber.project_key = 'Mu6DUPXdDYe98cv5JIfX'
Tamber::Item.update(
:id => 'item_wmt4fn6o4zlk',
:updates => {
:add => {
:properties => {
'available_large' => false,
'stock' => 89
}
},
:remove => {
:tags => [
'casual'
]
}
}
)
$ curl https://api.tamber.com/v1/item/update \
-u Mu6DUPXdDYe98cv5JIfX: \
-d id=item_wmt4fn6o4zlk \
-d updates='
{
'add': {
'properties': {
'available_large': false,
'stock': 89
}
},
'remove': {
'tags': ['casual']
}
}' \
Tamber tamber = new Tamber("Mu6DUPXdDYe98cv5JIfX", "");
HashMap<String, Object> itemParams = new HashMap<String, Object>();
itemParams.put("id", "item_wmt4fn6o4zlk");
HashMap<String, Object> updates = new HashMap<String, Object>();
HashMap<String, Object> add = new HashMap<String, Object>();
HashMap<String, Object> addProperties = new HashMap<String, Object>();
addProperties.put("available_large", false);
addProperties.put("stock", 89);
add.put("properties", addProperties);
updates.put("add", add);
HashMap<String, Object> remove = new HashMap<String, Object>();
List<String> removeTags = new ArrayList<String>();
removeTags.add("casual");
remove.put("tags", removeTags);
updates.put("remove", remove);
itemParams.put("updates", updates);
JSONObject resp = new JSONObject();
try {
resp = tamber.item.update(itemParams);
} catch (TamberException e) {
System.out.println(String.format("%s=%s", e.getClass().getName(), e.getMessage()));
}
import tamber
tamber.project_key = 'Mu6DUPXdDYe98cv5JIfX'
tamber.Item.update(
id='item_wmt4fn6o4zlk',
updates={
'add': {
'properties': {
'available_large': False,
'stock': 89
}
},
'remove': {
'tags': ['casual']
}
)
Seed your items by uploading an items JSON file, containing an array of json encoded item objects
[
{
"id": "item_83jx4c57r2ru",
"properties": {
"type": "book",
"title": "The Moon is a Harsh Mistress",
"img": "https://img.domain.com/book/The_Moon_is_a_Harsh Mistress.jpg"
},
"tags": ["sci-fi", "bestseller"],
"created": 1591331679
},
...
]