Custom rule condition to find Top-N Profile Key

Here we are for another simple yet useful tip from the Smart Bookshelf Demo. Although we are now improving it with some Artificial Intelligence, the first version has Sitecore Rules-based Personalization in the hearth of the recommendation mechanism.

Our desired behavior is to light-up LED strips from the top 3 Book Genres, such as seen below:

Face Detection picture

The books you see on screen are shown according to the top 3 genres for the current visitor.

It automatically scrolls, showing one genre each time. Here is how the component looks like:

Recommended Books

Our content tree is composed by Genre Pages, each of them with respective Profile Card assigned:

Mapping Genres and Profile Cards

So when your visitor visits a Genre Page, he or she will get points for that respective Profile Card, enabling Sitecore to register and track what are the preferred genres. The same thing also happens when a Book Page is visited (achieved by using the technique explained in my last post).

At this point we are already able to see preferred genres for our contacts, by opening any known contact at the Experience Profile:

Experience Profile shows preferred Genres

 

Rules Conditions

We will retrieve the top 3 genres by using Rules-based Personalization. What you see below is the personalization setting required to display the Poetry Genre if it’s “top 2″ for the current Visitor.

Custom Rule Condition

 

Custom Rule Condition: Matches Pattern Best X

Our custom condition is based on this OOTB condition:

/sitecore/system/Settings/Rules/Definitions/Elements/Visitor/Matches Pattern

However, it can only match match top 1, while our new condition accepts a number to test against.

 

Definition Item

  • Path: /sitecore/system/Settings/Rules/Definitions/Elements/Visitor/Matches Pattern Best X
  • Template:  /sitecore/templates/System/Rules/Condition
  • Text: where the current contact matches best [value,Text,,specific value] on profile key [ProfileKey,ProfileKey,,profile key]
  • Type: Your.Namespace.Rules.Conditions.ContactPatternMatchBestCondition, Your.AssemblyName
    • Make sure this points to your correct namespace

 

Source Code

Here is the source code to implement this condition. After setting up the Definition Item, as described at the previous topic, all you need to use it is to compile it.

using Sitecore.Abstractions;
using Sitecore.Analytics;
using Sitecore.Analytics.Tracking;
using Sitecore.Data;
using Sitecore.Framework.Conditions;
using Sitecore.Rules;
using Sitecore.Rules.Conditions;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web.Mvc;
using Sitecore.Diagnostics;

namespace Your.Namespace.Rules.Conditions.ContactPatternMatchBestCondition
{
   public class ContactPatternMatchBestCondition<T> : WhenCondition<T> where T : RuleContext
   {
      // ID for the item /sitecore/system/Settings/Content Testing/Personalization Suggestions/No pattern match
      private readonly ID _noPatternMatchItemId = ID.Parse("9E05C456-5CD0-405B-9D45-31DDD92AB8E4");
      private string _profileKey;
      public ContactPatternMatchBestCondition() : this(DependencyResolver.Current.GetService<BaseLog>()) { }

      internal ContactPatternMatchBestCondition(BaseLog log) {
         Condition.Requires<BaseLog>(log, nameof(log)).IsNotNull<BaseLog>();
         this.Log = log;
      }

      private string _value;
      public string Value {
         get => _value ?? string.Empty;
         set {
            Assert.ArgumentNotNull(value, nameof(value));
            _value = value;
         }
      }

      public string ProfileKey {
         get => _profileKey;
         set => _profileKey = value ?? string.Empty;
      }

      internal BaseLog Log { get; }

      protected override bool Execute(T ruleContext) {
         Condition.Requires<T>(ruleContext, nameof(ruleContext)).IsNotNull<T>();
         Condition.Ensures<ITracker>(Tracker.Current, string.Format("{0}.{1}", (object)"Tracker", 
            (object)"Current")).IsNotNull<ITracker>();
         Condition.Ensures<Session>(Tracker.Current.Session, string.Format("{0}.{1}.{2}", (object)"Tracker", 
            (object)"Current", (object)"Session")).IsNotNull<Session>();
         Condition.Ensures<Contact>(Tracker.Current.Session.Contact, string.Format("{0}.{1}.{2}.{3}", 
            (object)"Tracker", (object)"Current", (object)"Session", (object)"Contact")).IsNotNull<Contact>();
         if (string.IsNullOrEmpty(this.ProfileKey))
            return false;
         IEnumerable<IBehaviorProfileContext> profiles = Tracker.Current.Session.Contact.BehaviorProfiles.Profiles;
         List<ID> idsList = this.GetIdsList(this.ProfileKey);
         if (!profiles.Any<IBehaviorProfileContext>() && idsList.Contains(this._noPatternMatchItemId))
            return true;
         if (!idsList.Any())
            return false;

         var profileKeyId = idsList.First();

         int best;
         if (!int.TryParse(Value, out best))
            return false;

         foreach (IBehaviorProfileContext profile in profiles) {
            var profileScores = GetProfileKeyValue(profile);
            if (profileScores.Count < best)
               return false;

            var index = best - 1;
            var specifiedBest = profileScores[index].Key;
            var specifiedValue = profileScores[index].Value;
            var isBest = specifiedBest == profileKeyId && specifiedValue>0;
            if (isBest)
               return true;
         }
         return false;
      }

      private List<KeyValuePair<ID,double>> GetProfileKeyValue(IBehaviorProfileContext profile) {
         if (profile == null)
            return new List<KeyValuePair<ID,double>>();

         // ISSUE: explicit non-virtual call
         var ret = profile.Scores.ToList();
         ret.Sort((pair1,pair2) => pair2.Value.CompareTo(pair1.Value));
         return ret;
      }

      private List<ID> GetIdsList(string idList) {
         string[] strArray = idList.Split(new char[1] { '|' }, StringSplitOptions.RemoveEmptyEntries);
         var idList1 = new List<ID>();
         foreach (string str in strArray) {
            if (!string.IsNullOrEmpty(str)) {
               ID id = ID.Parse(str, ID.Null);
               if (!ID.IsNullOrEmpty(id))
                  idList1.Add(id);
            }
         }
         return idList1;
      }
   }
}

Hope you enjoy it!

Posted in Analytics, Rules, xDB

Leave a Reply

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

*

  Am Not Spammer

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>