Enrich models upon indexing - Optimizely Search & Navigation

Mari Jørgensen 07.06.2021 12.11.12

By default all public properties on a model will be indexed by Optimizely Search & Navigation (formerly known as Episerver Find). Very often you need to add more data to the indexed model, these are values that you don't want as regular Optimizely properties on the model.

A typical use case scenario is a Commerce site where you have product list pages, like category page and search result, where you list product items. Normally you want to index data such as price, promotion and sometimes inventory.

Following the documentation you can add fields to the index by customizing Client conventions.

//using EPiServer.Find.ClientConventions;
client.Conventions.ForInstancesOf<WebProduct>() .IncludeField(x => x.GetBasePrice());

// the extension method on the model
public static string GetBasePrice(this WebProduct product)
{
   // logic to fetch price
}

From my experience, this works fine for simple types. But if you use GetResult() and custom projections, the field may not be deserialized and parsed directly, and then your logic inside the extension method will run for each item in the list. If your extension method contains logic like fetching price from 3rd party, that will definitely decrease performance.

A better approach?

Basically what we wanted was simple properties on the model, that we could set only when indexing, and all retrival would come from the index.
Luckily I have brilliant colleagues, and together with collegue Sven Erik we implemented what we called IndexEnrichers.

We added an interface IContentIndexEnricher with an Enrich method and then made sure that method was called whenever items were indexed.
For this we extended the default Index method of ContentIndexer.

public class SiteContentIndexer : ContentIndexer
{
private readonly IContentIndexEnricher _contentIndexEnricher;

public SiteContentIndexer(IClient searchClient, IFindConfiguration findConfiguration, IContentTypeRepository contentTypeRepository, IContentLoader contentLoader, IContentRepository contentRepository, ISiteDefinitionResolver siteDefinitionResolver, ISiteDefinitionRepository siteDefinitionRepository, IContentIndexerConventions contentIndexerConventions, IEnumerable<IReindexInformation> reindexInformation, IContentIndexEnricher contentIndexEnricher, LanguageRoutingFactory languageRoutingFactory) : base(searchClient, findConfiguration, contentTypeRepository, contentLoader, contentRepository, siteDefinitionResolver, siteDefinitionRepository, contentIndexerConventions, reindexInformation, languageRoutingFactory)
{
_contentIndexEnricher = contentIndexEnricher;
}

public override IEnumerable<ContentIndexingResult> Index(IEnumerable<IContent> contents, IndexOptions options = null)
{
_contentIndexEnricher.Enrich(contents);

return base.Index(contents, options);
}
}


// inside configure container module
c.For<IContentIndexer>().Use<SiteContentIndexer>();

// the IContentIndexEnricher interface
public interface IContentIndexEnricher
{
   void Enrich(IEnumerable<IContent> contents);
}


On the product model we added properties that we wanted to enrich. The Ignore attribute will tell Optimizely to ignore the property (it will not be backed by a PropertyData type).

[Ignore]
public decimal BasePrice { get; set; }

[Ignore]
public decimal AdjustedPrice { get; set; }

[Ignore]
public IEnumerable<string> StockStatus { get; set; }

[Ignore]
public List<PromotionModel> Promotions { get; set; }


To be able to specify enrichers based on type we created an abstract class

public abstract class IndexEnricher<TContent> : IIndexEnricher, IIndexEnricher<TContent> where TContent : IContent
{
public virtual void Enrich(IEnumerable<IContent> contents)
{
var applicableContents = GetContents(contents);

Enrich(applicableContents);
}

public abstract void Enrich(IList<TContent> contents);

protected virtual IList<TContent> GetContents(IEnumerable<IContent> contents)
{
return contents.OfType<TContent>().ToList();
}

}

Using this setup we can now easily add an enricher implemention for my type WebProduct - it consists of two parts, the registration of the interface implementation and the actual implementation. 

// inside Initializable module
c.For<IIndexEnricher>().Add<WebProductIndexEnricher>();

// the actual enrichment logic
public class WebProductIndexEnricher : IndexEnricher<WebProduct>
{
// constructor with dependecy injection and some methods removed for better readability
   public override void Enrich(IList<WebProduct> contents)
{
foreach (var content in contents)
{
SetPriceAndDiscountPercentage(content);
FetchCachedPromotionsAnDiscount(content);
}

BatchSetStockStatus(contents);
}

private void SetPriceAndDiscountPercentage(WebProduct content)
{
var cachedPrice = _priceCacheService.GetPrice(content.Code);
if (cachedPrice != null)
{
content.BasePrice = cachedPrice.BasePriceAmount;
content.AdjustedPrice = cachedPrice.AdjustedPriceAmount;
}

content.HideBasePrice = content.HideBasePrice();

content.DiscountPercentage =
_productService.GetDiscountedPercentage(content.HideBasePrice, content.BasePrice,
content.AdjustedPrice);
}
}

The enricher example above is simplified for better readability, but the approach gives you quite a lot flexibility.  And most importantly for us, it resulted in improved indexing performance and product list page load.