Index custom xDB Facets to Segment Contacts in Sitecore’s List Manager

It’s a new year! And fresh from a short break from the blog, to focus on presenting at the xDB training course in New Orleans and Sitecore User Group in Cardiff, I’m back. So let’s get to it!

I talk a lot about the value of extending xDB to store data relevant to the client and how to surface that data for Content Editors to use. I now want to cover how to use that data on a grand scale. From accessing that data for all your contacts in a performant way. Then using that data to create lists of similar contacts based on their interactions. Finally allowing you to target groups of individuals with relevant email communications, via EXM, or synchronize with a CRM (more on that in a future blog).

Note : 
This functionality is only available in Sitecore 8.1 Update 3 and upwards

Indexing xDB Facet Data

We’re dealing with potentially hundereds of thousands of Contacts stored in xDB, each one having a plethora of data stored against it. Computing that quantity of data at run time each and every time isn’t going to cut it. Fortunately, this is not a new problem. We deal with this challenge when searching for Content. So we can use indexes to compute the large amount of data at a deferred time and indexing only what we need.

Fortunately, the Sitecore Analaytics Index is already indexing Contact data stored in the default xDB Facets and from Sitecore 8.1 Update 3 upwards we can now tell it to index our custom facets.

As expected, there is a pipeline for us to hijack and get it to do our bidding; contactindexable.loadfields. We’ll use this pipeline to access data in xDB Facets of the Contact currently being indexed and then return the data we care about in a number of indexable data fields. First the patch for the pipeline.


<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/"&gt;
<sitecore>
<pipelines>
<contactindexable.loadfields>
<processor type="JonathanRobbins.xTendingxDB.Index.Facets.Contacts.IndexSampleOrders, JonathanRobbins.xTendingxDB.Index" />
</contactindexable.loadfields>
</pipelines>
</sitecore>
</configuration>

Now that we’re hooked into the pipeline for getting the indexable fields lets create a class which inherits from ContactIndexableLoadFieldsProcessor to get the data. The overridable GetFields method accepts ContactIndexableLoadFieldsPipelineArgs which exposes the Contact. From there you can call GetFacet method to retrieve your custom Facet.

If you are familiar with my previous blogs the example I use is a client’s site allows customers to order samples of products. I store contextual information about that sample order in xDB.  I want to index all skus of the products that have been ordered by the Contact as well as their favourite (most ordered) product type.

In the code below, once I have calculated the skus and favourite type I create a new IndexableDataField for each, defining the field name, the value, and the type. For the favourite product type I simply want to store the type’s name as a string. Whereas for the products ordered I want to store each product’s sku in an array of strings.


public class IndexSampleOrders : ContactIndexableLoadFieldsProcessor
{
protected override IEnumerable<IIndexableDataField> GetFields(ContactIndexableLoadFieldsPipelineArgs args)
{
IContact contact = args.Contact;
var keyInteractionsFacet = contact.GetFacet<IKeyInteractionsFacet>(KeyInteractionsFacet.FacetName);
var fields = new List<IIndexableDataField>();
if (keyInteractionsFacet != null)
{
IndexableDataField<string> favouriteSampleTypeField = CreateFavouriteSampleTypeField(keyInteractionsFacet.SamplesOrdered);
IndexableDataField<string[]> samplesOrderedField = CreateSamplesOrderedField(keyInteractionsFacet.SamplesOrdered);
if (favouriteSampleTypeField != null)
{
fields.Add(favouriteSampleTypeField);
}
if (samplesOrderedField != null)
{
fields.Add(samplesOrderedField);
}
}
return fields;
}
private IndexableDataField<string> CreateFavouriteSampleTypeField(IElementCollection<ISampleOrder> samplesOrdered)
{
string favouriteSampleType = samplesOrdered.GroupBy(s => s.Type)
.OrderByDescending(gp => gp.Count())
.Select(g => g.Key)
.FirstOrDefault();
IndexableDataField<string> favouriteSampleTypeField = null;
if (favouriteSampleType != null)
{
favouriteSampleTypeField = new IndexableDataField<string>("contact.SampleOrder.FavouriteType", favouriteSampleType);
}
return favouriteSampleTypeField;
}
private IndexableDataField<string[]> CreateSamplesOrderedField(IElementCollection<ISampleOrder> samplesOrdered)
{
List<string> skus = (from s in samplesOrdered
select s.Sku).Distinct().ToList();
IndexableDataField<string[]> samplesOrderedField = null;
if (skus.Any())
{
samplesOrderedField = new IndexableDataField<string[]>("contact.SampleOrder.Skus", skus.ToArray());
}
return samplesOrderedField;
}
}

I recommend you create a processor for each type of data you want to index. In my example I’m only indexing one Element in one of my custom Facets.

Now that we have done the hard work of retrieving the values and creating indexable fields we need to define those fields on the Sitecore Analytics Index. As always I recommend using a patch. Add new fields within the Field section defining the FieldName property as the name you gave it in the processor.

Here is the field definitions for Lucene.


<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/"&gt;
<sitecore>
<contentSearch>
<configuration>
<indexes>
<index id="sitecore_analytics_index">
<configuration>
<fieldMap>
<fieldNames>
<field fieldName="contact.SampleOrder.Skus" storageType="YES" indexType="TOKENIZED" vectorType="NO" boost="1f" type="System.String[]" settingType="Sitecore.ContentSearch.LuceneProvider.LuceneSearchFieldConfiguration, Sitecore.ContentSearch.LuceneProvider" />
<field fieldName="contact.SampleOrder.FavouriteType" storageType="YES" indexType="TOKENIZED" vectorType="NO" boost="1f" type="System.String" settingType="Sitecore.ContentSearch.LuceneProvider.LuceneSearchFieldConfiguration, Sitecore.ContentSearch.LuceneProvider" />
</fieldNames>
</fieldMap>
</configuration>
</index>
</indexes>
</configuration>
</contentSearch>
</sitecore>
</configuration>

Here is the definition for your next indexable fields if you are using Solr.


<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/"&gt;
<sitecore>
<contentSearch>
<configuration>
<indexes>
<index id="sitecore_analytics_index">
<configuration>
<fieldMap>
<fieldNames>
<field fieldName="contact.SampleOrder.Skus" returnType="stringCollection" />
<field fieldName="contact.SampleOrder.FavouriteType" returnType="string" />
</fieldNames>
</fieldMap>
</configuration>
</index>
</indexes>
</configuration>
</contentSearch>
</sitecore>
</configuration>


<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/"&gt;
<sitecore>
<contentSearch>
<configuration>
<indexes>
<index id="sitecore_analytics_index">
<configuration>
<fieldMap>
<fieldNames>
<field fieldName="contact.SampleOrder.Skus" storageType="YES" indexType="TOKENIZED" vectorType="NO" boost="1f" type="System.String[]" settingType="Sitecore.ContentSearch.LuceneProvider.LuceneSearchFieldConfiguration, Sitecore.ContentSearch.LuceneProvider" />
<field fieldName="contact.SampleOrder.FavouriteType" storageType="YES" indexType="TOKENIZED" vectorType="NO" boost="1f" type="System.String" settingType="Sitecore.ContentSearch.LuceneProvider.LuceneSearchFieldConfiguration, Sitecore.ContentSearch.LuceneProvider" />
</fieldNames>
</fieldMap>
</configuration>
</index>
</indexes>
</configuration>
</contentSearch>
</sitecore>
</configuration>

Using Luke for Lucene you can see that the data of the custom Facet for this Contact has been indexed.

Index custom Sitecore xDB Facets

Quick bit of help – Once the new index patches are added to your site it will likely begin indexing the Contacts. If not, try visiting as a new contact and push the session to xDB. Reminder, you can’t rebuild the Sitecore_Analytics_Index like you can other indexes.

Now that we have an efficient method of selecting any number of contacts based on xDB data lets put it to use.

List Manager – Custom Segmentation Rules

Let’s say I want to group every contact based on their favourite type of product or I want to email promotional materials to all Contacts who have ordered samples from a particular range, then I am segmenting Contacts. Sitecore’s List Manager is made for this purpose. However the List Manager is not aware of our custom xDB data now indexed in the Analytics index. Let’s make it aware.

The List Manager lets you select Contacts based on a number of rules, this is the same Rules Engine that is used for personalisation etc. However Segmentation rules can’t be used for personalisation and vice versa.

To select the contacts in the above scenarios I will need to create a custom Rule Condition class. The class must inherit from the base class TypedQueryStringOperatorCondition of the IndexedContact type and a generic type where the generic type inherits from VisitorRuleContext. This exposes the IndexedContact allowing retrieval of the various IndexedDataFields including the ones we’ve created. As we are inheriting from TypedQueryStringOperatorCondition we take a value defined by the Content Editor and run compare expressions against the value we have indexed.


public class ProductsOrderedCondition<T> : TypedQueryableStringOperatorCondition<T, IndexedContact>
where T : VisitorRuleContext<IndexedContact>
{
protected override Expression<Func<IndexedContact, bool>> GetResultPredicate(T ruleContext)
{
string fieldName = "contact.SampleOrder.Skus";
return this.GetCompareExpression((Expression<Func<IndexedContact, string>>)(c => c[fieldName]), this.Value);
}
}

Before we can run the rule we need to create an definition Item in Sitecore. Insert a new Item at the path /sitecore/system/Settings/Rules/Definitions/Elements/Segment Builder/ using the Condition template. Enter the namespace for the type Field and then define the text of the rule.

Custom Segmentation rule

Upon selecting the rule the Content Editor is prompted to chose a comparison type and the value to compare. I recommend writing your rules such that they support the built-in operators but if your indexed data is more complex I recommend hiding the comparison types away from the users.

Sitecore Segmentation rule

Upon clicking OK the list is populated with with Contacts matching the conditions and all contacts will be visible within the List Manager.

Contacts in List Manager

And that’s it!

Segmenting millions of contacts in meaningful ways done simply, elegantly and no nasty load on the server. You’ll be free to use all that great data you are capturing in new and helpful ways.

The lists themselves are used by Sitecore’s EXM so you are facilitated tailored mailshots right off the bat. From the List Manager you can access each Contact’s Experience Profile, which makes the List Manager the most effective way to find particular Contacts right now.

The lists a Contact belongs is also stored against the Contact record in xDB so they can be easily accessed for other uses. Such as sending particular Contacts to a CRM I’ll be covering this in a future blog.

3 thoughts on “Index custom xDB Facets to Segment Contacts in Sitecore’s List Manager

  1. Great Post, very helpful!
    The indexing did not work for me with a string[]-type configured. I returned a IndexableDataField<List> in the LoadFieldsProcessor and removed the type property from the config, then everything got indexed correctly.
    Sitecore 8.2 update 2

    Like

Leave a comment