Sitecore Experience Commerce – SXA – Recommendations Component

As a followup to my previous post on recommendations, this post kicks off a series of posts on an actual implementation of a product recommendation component for Sitecore Experience Commerce. Not sure how many posts this will end up being, but in overall this is the plan:

  • Establish a first simple recommendation component for the Commerce SXA.
    Focus is not an advanced algorithm. Focus is just to get a first simple component working.
  • Implement the needed analytics and reporting to be able to validate how effective the recommendation engine works.
    If we can’t measure how’s it’s doing – it’s worthless. Like with any other personalization.
  • The last phase (one or several posts) will focus on the algorithm – the “recommendation engine”, and end up utilizing Sitecore Cortex to handle that piece.

Building the SXA component recommendation component

SXC SXA Recommendations Component Visual
SXA Recommendations Component Visual together with the Related Products component

Sitecore Experience Commerce comes with a storefront built from a set of Commerce SXA components. The SXA is, to me, an extremely important component to be able to deliver solutions faster. Just the basic principle of being able to work in parallel, and reuse of components makes so much sense.
Looking at the default components shipped with the storefront, a recommendation engine is missing. There’s a Related Products component that works as it should. It shows related products on the Product Detail Page (PDP), based on manual relations set up between products (RelatedSellableItemToSellableItem field). I choose to basically use the same UI as that component, but serve the products based on “recommendations” (more on that later). It has a separate css and view so the visual can always be customized later.

The recommendation algoritm

The assumption used for this first very basic recommendation algorithm is this:

  • If user 1 views product A, B and C, products A and B will be relevant for user 2 when she/he views product C.
Database table for persisting product pair relationships
Database table for persisting product pair relationships

So, it works by “recording” all products viewed by each user during as session on the site. When that session ends, we update “Pairs” of products and update a “count” as well as how much time users have spent on viewing the product. So, this information gets updated on each session end, and is stored in a simple database table.

The options in the Experience Editor

For this first simple version, component support to basically order the recommended products based on either view count (how many times have this product been viewed), or order by time spend (The sum of time spent for all product views). 

SXC SXA Recommendations-Component Experience Editor Options
Experience Editor Options

Obvious challenges with this simple recommendation strategy

  • Adding new products to the catalog.
    Since the view count and time spend will just keep on getting updated, will make it almost impossible for new products added to the catalog to be recommended.
    One way to mitigate that could be to run jobs (minions) that recalculate the view count and time spend – or simply reset data.
  • Deleting products from the catalog.
    When products are removed from the commerce catalog, they are not removed from our recommendations table.
    One way to handle that is to catch that when we lookup the product and then get it removed right away. Another way would be to create a minion to remove the entries in our recommendations table when products are removed from the catalog
  • Looking at all products in a session.
    This assumption is very simple, and most likely it will create some “noise” that different persons jump between different categories. But again, that is for the users to decide, so as long as this is tracked and we can measure and A/B test on it – we’re good. A way to mitigate though, could be to include category information so that recommended products would be some same main category.

There’s room for improvement, and new strategies to try out. That will most likely be in another post!

The code

To build this POC I followed the exact same structure as exists for the related Products component that’s part for the Storefront that ships with SXC. Secondly I added logic around collecting the tracked info on products viewed in a session, and finally implemented a simple feature to retrieve a list of products related to a specific product. The code will be pushed to a public Git after the next couple of blogs – so you can check out the details there, but this is the steps I followed:

The Controller

The RecommendationController implemented is initialized with a ModelProvider, RecommendationsRepository as well as various “context” references. Then a couple of simple actions:

An action that returns the view:

public ActionResult RecommendedProducts() 
{        
   RecommendedProductsRenderingModel recommendedProductsRenderingModel = this.RecommendationsRepository.GetRecommendedProductsRenderingModel();            
   return base.View(GetRenderingView("RecommendedProducts"), recommendedProductsRenderingModel); 
} 

An action that handles the ajax request that basically return recommended product data:

public JsonResult GetRecommendedProducts([Bind(Prefix = "ps")] int? pageSize, [Bind(Prefix = "pg")] int? pageNumber, [Bind(Prefix = "rt")] string recommendationType, [Bind(Prefix = "ci")] string currentItemId, [Bind(Prefix = "cci")] string currentCatalogItemId)
{
   IVisitorContext service = ServiceLocator.ServiceProvider.GetService<IVisitorContext>();
   RecommendedProductsJsonResult promotedProductsJsonResult = this.RecommendationsRepository.GetRecommendedProductsJsonResult(service, recommendationType, currentItemId, currentCatalogItemId, pageSize, pageNumber);
   return base.Json(promotedProductsJsonResult);
}

RecommendationsRepository

This repository is in initialized with a bunch of references to that is already in the storefront code and ready to use, besides the model provider for our recommendations. So the logic in here simply handles:

  • Check if we’re in Experiences Editor, if yes the return hardcoded mock data so the component is visible in the editor.
  • If no in Experience Editor, ask the ModelProvider for some recommended products
  • Enrich the returned products with prices, ratings etc. 
public virtual RecommendedProductsJsonResult GetRecommendedProductsJsonResult(IVisitorContext visitorContext, string recommendationType, string currentItemId, string currentCatalogItemId, int? pageSize, int? pageNumber)
{
    int? nullable;
    int num;
    Assert.ArgumentNotNull(visitorContext, "visitorContext");
    RecommendedProductsJsonResult model = base.ModelProvider.GetModel<RecommendedProductsJsonResult>();
    Item scPageItem = base.Context.Database.GetItem(currentItemId);
    Item catalogProductItem = base.Context.Database.GetItem(currentCatalogItemId);
    CommerceStorefront currentStorefront = base.StorefrontContext.CurrentStorefront;
    base.SiteContext.CurrentItem = scPageItem;
    base.SiteContext.CurrentCatalogItem = catalogProductItem;
    if (base.Context.IsExperienceEditor)
    {
        model = RecommendedProductsMockData.InitializeMockData(this, model, base.ModelProvider);
    }
    else
    {
        RecommendedProducts recommendedProducts = base.ModelProvider.GetModel<RecommendedProducts>();
        Item currentCatalogItem = base.SiteContext.CurrentCatalogItem;
        string str = recommendationType;
        nullable = pageSize;
        num = (nullable.HasValue ? nullable.GetValueOrDefault() : 4);
        nullable = pageNumber;
        Dictionary<string, List<ProductEntity>> recommendedProductsLists = recommendedProducts.GetRecommendedProductsLists(currentCatalogItem, str, num, (nullable.HasValue ? nullable.GetValueOrDefault() : 0));
        List<ProductSummaryViewModel> productSummaryViewModels = new List<ProductSummaryViewModel>();
        foreach (KeyValuePair<string, List<ProductEntity>> relatedProductsList in recommendedProductsLists)
        {
            this.AdjustProductStockStatus(relatedProductsList.Value);
            base.CatalogManager.GetProductBulkPrices(base.StorefrontContext.CurrentStorefront, visitorContext, relatedProductsList.Value);
            foreach (ProductEntity value in relatedProductsList.Value)
            {
        ProductSummaryViewModel productRating = base.ModelProvider.GetModel<ProductSummaryViewModel>();
                productRating.Initialize(value, false);
        productRating.CustomerAverageRating = base.CatalogManager.GetProductRating(value.Item);
                productSummaryViewModels.Add(productRating);
            }
        }        
        model.Initialize(scPageItem["Title"], productSummaryViewModels);
    }
    return model;
}

RecommendedProduct

This class is used by the RecommendationsRepository and the logic is handled in GetRecommendedProductsLists. This is where the RecommendationsStorageProvider is initialized, and based on parameters retrieves a list of recommended product (Product IDs).

public virtual Dictionary<string, List<ProductEntity>> GetRecommendedProductsLists(Item item, string recommendationType, int pageSize, int pageNumber)
{
    Dictionary<string, List<ProductEntity>> resultList = new Dictionary<string, List<ProductEntity>>();
    ProductEntity newProductModel = null;
    List<ProductEntity> productList = new List<ProductEntity>();
    ID productId = item.ID;
    List<ID> returnedRecommededProductIDs = null;
    IRecommendationStorageProvider recommendationStorageProvider = null; 
    switch (recommendationType) {
        case RecommendationConstants.RecommendationTypes.IDs.MostViewed:
            recommendationStorageProvider = Factory.CreateObject("RecommendationStorageProvider", true) as IRecommendationStorageProvider;            
            returnedRecommededProductIDs = recommendationStorageProvider.GetRecommendations(productId, pageNumber, pageSize, 1);
            break;
        case RecommendationConstants.RecommendationTypes.IDs.MostTimeSpend:
            recommendationStorageProvider = Factory.CreateObject("RecommendationStorageProvider", true) as IRecommendationStorageProvider;
            returnedRecommededProductIDs = recommendationStorageProvider.GetRecommendations(productId, pageNumber, pageSize, 2);
            break;
    }

    if (returnedRecommededProductIDs!=null) { 
        // Get the recommended products from our repository and retrieve product model for each of them
        // foreach...
        foreach(var prodId in returnedRecommededProductIDs) {
            var recProdItem = this.Context.Database.GetItem(prodId);
            newProductModel = this.ModelProvider.GetModel<ProductEntity>();
            newProductModel.Initialize(this.StorefrontContext.CurrentStorefront, recProdItem, null);
            productList.Add(newProductModel);
        }
        resultList.Add(item.ID.ToString(), productList);
    }
    return resultList;
}

VisitEnd

When a users session ends, we loop through the events, and in case of a ProductViewed Event, we register all the product IDs, and creates product pairs. When saved we updated time spend on viewing the product as well as number of times the product was viewed (Count).

public override void Process(SubmitSessionContextArgs args)
{
    Assert.ArgumentNotNull(args, "args");
    Session session = args.Session;
    if (session != null)
    {
        CurrentInteraction currentInteraction = session.Interaction;
        if (currentInteraction != null && currentInteraction.PageCount > 0)
        {
            var vpm = new ViewedProductsManager();
            foreach (var page in currentInteraction.Pages)
            {
                if(page.PageEvents!=null)
                {
                    foreach(var pageEvent in page.PageEvents)
                    {
if(pageEvent.PageEventDefinitionId.Equals(VisitedProductDetailPage))
                        {
                            // Get the product ID from the page that was viewed:
                            ID productId = ID.Null;
  if(ID.TryParse(pageEvent.CustomValues["Product"], out productId)) {
                // Check if the product is already in the list:
                        vpm.AddViewedProduct(productId, page.Duration);
                            }
                        }
                    }
                }
           }
           vpm.SaveVisit();
        }
    }
}

Sitecore items

SXC-SXA-Recommendations-Component-Experience-Templates
Templates

On the Sitecore side a number of items has to be established. Nothing special really about that – only thing is the SXA specific things that is pretty well described here.

Templates

  • Recommended Products is pretty much the same as the related products that ships with the storefront. Used for some labels and text and settings as well.
  • Recommendation Types and Recommendation Types Folder is used to create a datasource for the dropdown so the user can select between the various  recommendation types.
  • Recommended Products Parameters is used so the user can set parameters on the rendering – especially the recommendation types.

Renderings

A standard Controller Rendering to render the base recommendation view was all that was needed. The following data is also set on the Controller Rendering item – nothing special or surprising information on the above Controller Item, if you’re used to follow best practices – SXA or not.

SXC SXA Recommendations Component Experience Controller Rendering

Data Items

Following exactly what the SXA Storefront does, we create an item for holding the basic information for the recommendations component – title, labels etc.

Recommended Products Data Item

Recommendation types

As mentioned we need to create a couple of items that defines the various recommendation types.

SXC SXA Recommendations Component Experience Data Items Recommendation Types
Recommendation Types Data Items

Themes

So all SXA components, styles etc. are stored in the media library – so we have a specific item for that as well.

SXC SXA Recommendations Component Experience Data Items Theme

That’s it!

Sorry for the long post, but this is to show all pieces that had to be done to make it working. I hope is brought some value, and give the base skeleton for adding advanced recommendation logic to SXA components. Stay tuned – the next post will focus on analytics and how to gain insight into how well the comment actually works.

You are more than welcome to leave a comment below – or to reach out to me on email, twitter or linkedin.

Sitecore Experience Commerce – SXA – Recommendations Component

Leave a Reply

Your email address will not be published. Required fields are marked *