Skip to main content

Development

Episerver Commerce: Indexing product properties in a variant document

Catalog modeling and organization within a commerce site can vary widely depending on the type of products you are selling.  The way that customers will search for your products can present situations in which traditional product/variant modeling alone will not be able to be achieved.  Custom indexing can bridge the gap in these cases to provide the filtering options you want.
In this post, we will present an alternative method for indexing in which we will index product data within variant documents using the Episerver Find API.  This post is meant to serve as a companion to Episerver’s documentation using the opposite approach to store variant data within product documents.

Why Do This?

During the implementation of a recent beverage site, we were presented with a fairly common scenario.  All variants of a product contain the same intrinsic base product and properties.  They only vary in container type, number of containers, and container size.  For example, Pepsi is still Pepsi regardless of whether it is a 6-pack, case, or single can.  Therefore, it is safe to assume that every variation of a product inherits the properties of its parent product.  Here is a sample for demonstration purposes.
Episerver Find Indexing
The goal of search was to present variants so that customers could directly find the package/container combination they wanted to purchase and put them into their cart immediately.  However, facets and filtering options were related to properties present at the product level exclusively. Properties of the variants in the search results were not.
This last item is key as we cannot filter variants in our search index if they do not contain these properties within their index document.  Properties such as brand and taste do not exist on our variant items.  So, if we want to be able to filter our variants based on product data, we will need to add these properties to the index directly within the variant document.

Custom Indexing

How do we accomplish this in code?  First, we start by creating an initialization module for our custom index and declaring the additional fields we would like to index.

[InitializableModule]
[ModuleDependency(typeof(FindCommerceInitializationModule))]
public class FindInitialization : IConfigurableModule
{
    public void Initialize(InitializationEngine context)
    {
    }
    public void Uninitialize(InitializationEngine context)
    {
    }
    public void ConfigureContainer(ServiceConfigurationContext context)
    {
        context.Services.AddSingleton<CatalogContentClientConventions, SiteCatalogContentClientConventions>();
    }
}
public class SiteCatalogContentClientConventions : CatalogContentClientConventions
{
    public override void ApplyConventions(IClientConventions clientConventions)
    {
        SearchClient.Instance.Conventions.ForInstancesOf<VariationContent>().IncludeField(x => x.Brand());
        SearchClient.Instance.Conventions.ForInstancesOf<VariationContent>().IncludeField(x => x.Taste());
        SearchClient.Instance.Conventions.ForInstancesOf<VariationContent>().IncludeField(x => x.BeverageType());
    }
}

At this point, you will likely notice that the projection fields that we have declared are not showing up in autocomplete and have error tags in Visual Studio.  However, we are going to fix that now and add these extension methods.

public static class VariationContentExtensions
{
    private static readonly Lazy<IRelationRepository> RelationRepository = new Lazy<IRelationRepository>(() => ServiceLocator.Current.GetInstance<IRelationRepository>());
    private static readonly Lazy<IContentLoader> ContentLoader = new Lazy<IContentLoader>(() => ServiceLocator.Current.GetInstance<IContentLoader>());
    public static ProductContent ParentProduct(this VariationContent variationContent)
    {
        var productContents = ContentLoader.Value
            .GetItems(variationContent.GetParentProducts(RelationRepository.Value), variationContent.Language)
            .OfType<ProductContent>().ToList();
        return productContents.Any() ? productContents.First() : null;
    }
    public static string Brand(this VariationContent variationContent)
    {
        var brand = variationContent.ParentProduct().GetValue("Brand");
        return brand != null ? brand.ToString() : string.Empty;
    }
    public static string BeverageType(this VariationContent variationContent)
    {
        var beverageType = variationContent.ParentProduct().GetValue("BeverageType");
        return beverageType != null ? beverageType.ToString() : string.Empty;
    }
    public static List<string> Taste(this VariationContent variationContent)
    {
        var list = new List<string>();
        var collection = (ItemCollection<string>) variationContent.ParentProduct().GetValue("Taste");
        foreach (var c in collection)
        {
            list.Add(c);
        }
        return list;
    }
}

There are a few things going on here to note:

  1. The ParentProduct method is doing important work in our indexes.  It is fetching the product item that has all of the properties we need to index in our variants.
  2. Brand and BeverageType are declared as PropertyDictionarySingle backing types.  This means that authors can edit them dynamically as opposed to a statically declared facet.  The indexer will simply interpret these as string fields.
  3. Taste is declared with a PropertyDictionaryMultiple backing type.  This means that we will be indexing a collection and multiple facets could be selected on this single field.

That is all the coding we will need for the index!  Now, open up Find and reindex your site.  Once it is completed, you should use the Find Overview to browse one of your variants and verify that the fields are showing up with expected data in the index.
After that, you are free to use these fields in your searches to filter content as needed just like they were native fields.

var pepsiResults = SearchClient.Instance.Search<VariationContent>()
    .Filter(x => x.Brand().Match("Pepsi")).GetResult();

 

Leave a Reply

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

This site uses Akismet to reduce spam. Learn how your comment data is processed.

John Dymond

More from this Author

Follow Us