Personalised Product Recommendations

A recommendation is a ranking of items by the probability that a person acts on them. Most systems get there by exporting interaction logs, training a model offline and serving its output from a cache. In Aito v2 the ranking is one query over the log itself: recommend names the link to rank, goal names the outcome to maximise, and where carries whatever you know about the person and the session.

The queries below run against the live v2 sandbox. impressions records each time a product was shown in a context, and whether it was bought (purchase). Through the links, context.user is the shopper and context.basket is what was already in the basket. Probabilities in the responses are rounded.

Recommend for a user

{
  "from": "impressions",
  "where": { "context.user": "larry" },
  "recommend": "product",
  "goal": { "purchase": true },
  "select": ["$value", "name", "$p"],
  "limit": 5
}
{ "total": 42, "hits": [
  { "$value": "6410405207722", "name": "Pirkka paper towel 4 rl", "$p": 0.123 },
  { "$value": "6411401028373", "name": "Karl Fazer 200g Tyrkisk Peber chocolate", "$p": 0.113 },
  { "$value": "6415600501811", "name": "Coca-Cola 1,5l soft drink", "$p": 0.104 },
  { "$value": "6410405093677", "name": "Pirkka iceberg salad Finland 100g 1st class", "$p": 0.102 },
  { "$value": "6410405216120", "name": "Pirkka lactose-free semi-skimmed milk drink 1l", "$p": 0.092 } ] }

Every one of the 42 products is a candidate. $p is the probability that the product is bought when shown to Larry, so the ranking favours what converts for him, not only what he buys most often. select can name the product's own columns (name) because product is a link.

In SQL the same ranking is a table function:

SELECT * FROM recommend('impressions', 'product', goal => 'purchase = true',
  given => 'context.user = ''larry''', k => 5)

Leave out what is already in the basket

A condition on the recommended field restricts the candidates. To exclude items, list them under $and with $not:

{
  "from": "impressions",
  "where": {
    "context.user": "larry",
    "product": { "$and": [ { "$not": "6410405207722" }, { "$not": "6415600501811" } ] }
  },
  "recommend": "product",
  "goal": { "purchase": true },
  "select": ["$value", "name", "$p"],
  "limit": 4
}

total drops to 40, and the paper towels and the cola are gone. The remaining products keep the probabilities they had: exclusion removes candidates, it does not re-score them. An empty $and list constrains nothing, so a client can send the basket as it is, even when it is empty.

Use the basket as evidence

What is already in the basket says something about what comes next. With rye bread in Larry's basket, and the bread itself excluded:

{
  "from": "impressions",
  "where": {
    "context.user": "larry",
    "context.basket": { "$has": "6437002001454" },
    "product": { "$not": "6437002001454" }
  },
  "recommend": "product",
  "goal": { "purchase": true },
  "select": ["$value", "name", "$p"],
  "limit": 5
}
{ "total": 41, "hits": [
  { "$value": "6411401028373", "name": "Karl Fazer 200g Tyrkisk Peber chocolate", "$p": 0.162 },
  { "$value": "6415600501811", "name": "Coca-Cola 1,5l soft drink", "$p": 0.121 },
  { "$value": "6407870071224", "name": "Atria Gotler ham sausage 300g", "$p": 0.116 },
  { "$value": "6410405207722", "name": "Pirkka paper towel 4 rl", "$p": 0.096 },
  { "$value": "6410405093677", "name": "Pirkka iceberg salad Finland 100g 1st class", "$p": 0.094 } ] }

Ham sausage enters the top five and the chocolate moves up. Remove context.user and the same query serves an anonymous shopper from the basket alone: for that case the top recommendation is the weekend newspaper (0.152).

SELECT * FROM recommend('impressions', 'product', goal => 'purchase = true',
  given => 'context.user = ''larry'' AND context.basket = ''6437002001454'' AND product <> ''6437002001454''',
  k => 5)

In SQL, = against the basket array is membership, the same as $has.

Explain a recommendation

Add $why to the select:

{
  "from": "impressions",
  "where": { "context.user": "larry" },
  "recommend": "product",
  "goal": { "purchase": true },
  "select": ["$value", "name", "$p", "$why"],
  "limit": 1
}

For the paper towels the factor tree starts from the base purchase rate of an impression (0.052), multiplies in a lift of 2.01 because this product converts better than the average product, and a lift of 1.21 from context.user: "larry". The remaining factors are each close to 1. A reviewer can see which part is popularity and which part is personal.

Why Aito for this

  • No training step. A purchase written to impressions changes the next recommendation.
  • Context is a condition. The user, the basket, the weekday or the search phrase are all where conditions on the same query, and any of them can be left out.
  • Probabilities, not scores. $p is a probability of the goal, so it can be thresholded, compared across users and explained.

Related: Smart search Β· Cart autofill Β· Behavioural analytics Β· Query Reference

← All v2 use cases