Adding Recommendations

The Classic Implementation vs. Using the API

One of the major features of Dynamic Yield is our Recommendations Engine for products and content. It supports a wide array of strategies and rules, with the same testing & targeting capabilities as other campaigns.

In a non-API implementation, Dynamic Yield manages the rendering of recommendations widget using our visual editor and template language - the same tools used for all other campaign types available on client script-based sites. Clicks are also automatically tracked, at the product level. Changes to the recommendations strategies used, rules and variations can be done freely via the Web UI without additional work on the part of the developer.

With the API however, rendering and tracking are now up to you the developer. To make life easier, using API Recommendations-type campaigns are very similar to Custom API Campaigns. In this step, we'll focus on the small differences and the things that sets the Recommendations campaign apart.

To make things easier, we've already integrated a dummy recommendations element at the bottom of the Petshop homepage:

2262

This widget, still in its non-Dynamic Yield-based version, is powered by the amazing logic of choosing four random products out of the catalog :wink:. However, take a look at the function getPageContent in routes/homepage.js, which we've modified before. This is where we're going to hook in the real recommendations.

Creating a Recommendations Strategy & Campaign

First, let's create a strategy. This entity has all the "meat" on which algorithm/s are used and their settings, from the basic to the advanced.

  1. Go to Assets > Strategies.

  2. Add a new strategy and set Page Type to Homepage. By default, the Popularity algorithm is selected. This may not be the most exciting strategy, but it's the quickest to set up for the sake of this tutorial.

620

  1. Head back to API Campaigns, and add a new campaign of type Recommendations.

  2. Name the campaign HP Recommendations. This will also update the API Selector Name. Unlike the display name, the selector name should remain fixed, as we've noted before.

  3. Set the Page Type to Homepage here as well. This setting only serves to limit the selection of strategies available to this campaign. In real-life scenarios where you have a multitude of campaigns and strategies, this comes in handy.

  4. Add a single variation. The JSON payload returned for Recommendations campaigns has a standard format, so there is no needed step of choosing a template and filling variables as you've done before. All you need is to choose the appropriate strategy and define a number of items to return, which should match the intended visual design. In this case, set it to 4.

  5. Save & Publish the campaign. That's it on the Web UI side of things.

686

Getting Recommendations via the API

First, let's add HP Recommendations to the list of campaign selector names in our call to choose. Let's have a look at how the response object looks like now, before we actually use it for rendering.

async function getPageContent(req) {
		// ...
    /* Add 'HP Recommendations' to the campaign selectors array: */
    const apiResponse = await DYAPI.choose(req.userId, req.sessionId, req.dyContext, 
                                      ['HP Hero Banner', 'HP Recommendations']);
		// ...

Reloading the homepage, the log output should look similar to this:

{
  "HP Recommendations": {
    "decisionId": "l7QtMzAwMDM5NTA3OTY3NTE2NzExMc4ACrJBp...",
    "slots": [
      {
        "sku": "123",
        "productData": {
          "publish_time": "2020-01-01T03:30:19",
          "name": "Scratchy Chair",
          "url": "/product/123",
          "price": 19,
          "in_stock": true,
          "image_url": "/images/Group 123.png",
          "low_on_stock": "No",
          "keywords": [],
          "dy_display_price": "19",
          "group_id": "123",
          "categories": [
            "Cats",
            "Toys"
          ],
          "description": "Let your cat scratch while ..."
        },
        "slotId": "l4SobG9jYXRpb2DovsaG9zdC+ocmVmZXJyZXKgpGRhdGGQpHR5cGWoS..."
      },
      {
        "sku": "132",
        "productData": {
          "publish_time": "2020-01-01T03:30:19",
          "name": "Charlie's Work Shirt",
          "url": "/product/132",
          "price": 19,
      		// ...Some attributes skipped
       	},
        "slotId": "l4SobG9jYXRpb26xaHR0cDovL2xvY2FsaG9zdC+ocmVmZXJyZXKgpGTIB..."
      },
      // ...Two more slots omitted here
    ]
  },
  "HP Hero Banner": {
    "decisionId": "l7QtMTU4NDA3MjI2NjQ1NDEyMzYyNc4AC6f3pzc3MjAxNDABkJHOA...",
    "image": "http://cdn.dynamicyield.com/petshop/images/pets-3715733_1920.jpg",
    "title": "We got your pet.",
    "subtitle": "Dynamic Lorem ipsum",
    "cta": "Go Shopping",
    "link": "/category/all"
  }
}

Hmmm... while we already took care within DYAPI.choose() to format the response nice and simple enough for Custom Campaigns, for recommendations we can definitely simplify it further, to a flat array of one object per each product.

Simplifying the Response Format

The response object above is already post some processing done by our DYAPI.choose() helper. Here's how the raw response looks like - note the different type and payload.type values in the sample response there.

Without much ado, here's a new version of DYAPI.flattenCampaignData() to also support flattening down recommendations, using the type to differentiate between campaign types:

function flattenCampaignData(res, choice) {
  let data = null;
  if (choice.variations.length > 0) {
    switch (choice.type) {
      case 'DECISION':
        data = { decisionId: choice.decisionId, ...choice.variations[0].payload.data };
        break;
      case 'RECS_DECISION':
        data = choice.variations[0].payload.data.slots.map(
                  slot => ({ ...slot.productData, sku: slot.sku, slotId: slot.slotId }));
        break;
      default:
        throw new Error('Unknown choice type: ' + choice.type);
    }
  }

  res[choice.name] = data;
  return res;
}

Reload the page, and our log print now looks better - and ready to be applied instead of the random recommendations:

{
  "HP Recommendations": [
    {
      "publish_time": "2020-01-01T03:30:19",
      "name": "Charlie's Work Shirt",
      "url": "/product/132",
      "price": 19,
      "in_stock": true,
      "image_url": "/images/Group 132.png",
      "low_on_stock": "Yes",
      "keywords": [],
      "dy_display_price": "19",
      "group_id": "132",
      "categories": [
        "Dogs",
        "Clothing"
      ],
      "description": "When your pug is busy doing an honest day's work...",
      "sku": "132",
      "slotId": "l4SobG9jYXRpb26xaHR0cDovL2xvY2VmZXJyZXKgpGRhdGGQpHR5cGWoSE9..."
    },
    {
      "publish_time": "2020-01-01T03:30:19",
      "name": "Scratchy Chair",
      "url": "/product/123",
      "price": 19,
      // ...Some attributes skipped
      "sku": "123",
      "slotId": "l4SobG9jYXRpb26xaHR0cDovL2xvY2FsaG9zdMc4ACrJBpzcyMDY2NTIBkJ..."
    },
    // ...Two more slots omitted here
  ],
  "HP Hero Banner": {
    "decisionId": "l7QtMTU4NDA3MjI2NjQ1NDEyMzYyNc4AC6f3pzc3MjAxNDABk...",
    "image": "http://cdn.dynamicyield.com/petshop/images/pets-3715733_1920.jpg",
    "title": "We got your pet.",
    "subtitle": "Dynamic Lorem ipsum",
    "cta": "Go Shopping",
    "link": "/category/all"
  }
}

Nice and simple. Now all we got to do is return to getPageContent() and actually put these recommendations to work:

async function getPageContent(req) {
	// ...
  const content = {
    heroBanner: apiResponse['HP Hero Banner'] || defaultHeroBanner,
    /* Modified line below: */
    recommendations: apiResponse['HP Recommendations'] || defaultRecommendations,
  };
	// ...
}

Reload the homepage, and... (drumroll) it looks basically exactly the same as before: four products.
To actually see some value out of all that we did, let's play just a bit with one of the capabilities of Recommendations in Dynamic Yield

A Bit More Fun with Recommendations

  1. Go back to edit the Popular products strategy, and click on Add Filtering Rule.

  2. Let's give it the name Only Cats in Slot #1, set the rule type to Only Include and apply it only to Slot #1 as seen below:

2048
  1. In the next step, set the filter to product category "Cats". Ensure Use as a dynamic list is enabled:
2596
  1. Skip over the next few steps of the wizard to complete the rule setup.

  2. I also went ahead and added the opposite rule for slot #2: that rule will exclude all cat products.

The strategy now looks like this:

1598

After saving the strategy and waiting a minute for changes to go live, reload the homepage multiple times. You will notice that the first slot always shows a cat product, while the second one never shows any of them.

The Last Step: Reporting Clicks at the Product Level

The last step for integrating recommendations is reporting clicks a bit differently. Look again at the JSON payload we've formatted above: instead of a single decisionId, for each slot (product) there is a slotId identifier.

When a product is clicked, you need to report the click using the unique slotId identifier of that specific product. This level of granularity allows Dynamic Yield to provide some seriously in-depth reports of recommendations performance in your site.

Let's adapt the rendering code of recommended products to store the slotId as a data attribute, and modify the click listener to support it.

  1. In views/homepage.pug add the ID as a data attribute:
each product in recommendations
        // Replace this line...
        a.ps-prodcut-listing-item(href=`/product/${product.sku}`)
        // with this one		
        a.ps-prodcut-listing-item(href=`/product/${product.sku}` data-dy-slot-id= product.slotId)
  1. In public/javascript/main.js where we attached a click listener before, let's add a similar piece of code to what we've added there before:
// This we already added
  document.querySelectorAll('[data-dy-decision-id]')
    .forEach(function(variationNode) {
      variationNode.addEventListener('mousedown', function() {
        reportClick({
          type: 'CLICK', 
          decisionId: variationNode.getAttribute('data-dy-decision-id'),
        });
      });
    });

  // Here is the new code to add:
  document.querySelectorAll('[data-dy-slot-id]')
    .forEach(function(productNode) {
      productNode.addEventListener('mousedown', function() {
        reportClick({
          type: 'SLOT_CLICK', 
          slotId: productNode.getAttribute('data-dy-slot-id')
        });
      });
    });
  1. Reload the homepage and click on a product from the recommendations. Looking at the console log, the engagement is sent to Dynamic Yield:
1840

That's it! going into the campaign report in the Web UI, you would see these clicks registered in a moment or two.

You've reached the end of this tutorial! :+1:
Let's wrap this up with some tips and bonus tasks.