Quick Start

Tamber's API allows you to easily stream events (user-item interactions) as they happen, and get highly accurate recommendations that are updated in real time. All requests can be sent as either GET or POST requests, and all responses, including errors, are returned in JSON.

Tracking events works just like it does for a data analytics service: whenever a user clicks, purchases, shares, thumbs, likes, hearts, favorites, saves... stream it to Tamber. Stored events are free, so just track everything – you can decide what to use in your engine later.

Overview

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.

There are 3 steps to setting up your engine:

1. Start tracking events
2. Create your engine
3. Put recommendations in your app

1. Stream events client-side

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:

A. Track clicks/taps - allowing you to personalize the app as users navigate from page to page by learning on every action.
B. Make recommendations to guest users who are not signed in.
C. Get robust analytics to assess performance and perform A/B testing.

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.

amount

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.

2. Create your engine

Once you have tracked enough events for Tamber to start learning (~1-2 weeks of tracking), head to your dashboard to create your engine.

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.

Default Engine

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.

3. Put recommendations in your app server-side

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.

Up Next

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
)

Recommended Section

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.

Feed sort

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.

More ways to use your engine

Discover weekly (like Spotify!)
Trending items
Meta entities (recommend metadata such as genres, categories, etc.)
Lower level methods for building your own features

Customize and improve recommendations

There are a lot of things you can do to customize and improve your recommendation experiences.

Upload past events optional

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

Formatting

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.

What your dataset will probably look like:

user,item,behavior,amount,created
user_rlox8k927z7p,item_83jx4c57r2ru,purchase,1.0,1591331679

A dataset with context data included:

user,item,behavior,amount,created,context
user_rlox8k927z7p,item_83jx4c57r2ru,purchase,1.0,1591331679,"{""client"": [""iPhone"", ""ios"", ""device_phone""]}"

Sync Itemsoptional

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:

Real time updates server-side

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']
        }
)

Bulk upload JSON

Seed your items by uploading an items JSON file, containing an array of json encoded item objects

What it should look like:

[
	{
		"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
	},
	...
]