Loading...
Area: Optimizely Commerce
Applies to versions: 10 and higher
Other versions:

Indexing variants in a product document

Recommended reading 
Note: This documentation is for the preview version of the upcoming release of CMS 12/Commerce 14/Search & Navigation 14. Features included here might not be complete, and might be changed before becoming available in the public release. This documentation is provided for evaluation purposes only.

This topic explains how to index a variant for a product document in a solution using the Optimizely Search & Navigation-Commerce integration (EPiServer.Find.Commerce). The topic also provides examples of including and excluding variants, and adding price and inventories to a product document to be indexed. 

Indexing the entire variant inside the product

By default, variants are indexed as ContentReferences inside the product document sent to the Search & Navigation index. This allows all variant references for a specific product from the index and gets the product for a specific variant.

Depending on how your site works, it might be better to index the entire variant inside of the product, as shown in the following examples.

Overriding CatalogContentClientConventions

[New in Commerce 14]

Override the CatalogContentClientConventions class, and register it in the ConfigureServices method of startup.cs.

//Startup class
public void ConfigureServices(IServiceCollection services)
{
        services.AddTransient<CatalogContentClientConventions, SiteCatalogContentClientConventions>();
}

Excluding variant reference indexing for product content

To exclude variant references, override the ApplyProductContentConventions, and exclude the field using conventions.

public class SiteCatalogContentClientConventions : CatalogContentClientConventions
  {
    protected override void ApplyProductContentConventions(TypeConventionBuilder<ProductContent> conventionBuilder)
      {
        base.ApplyProductContentConventions(conventionBuilder);
        conventionBuilder
          .ExcludeField(x => x.Variations());
      }
  }

Including related variant content items in product content

Create a new extension method for product content to be able to index variants for it.

public static class ProductContentExtensions
  {
    public static IEnumerable<VariationContent> VariationContents(this ProductContent productContent)
      {
        return VariationContents(productContent, ServiceLocator.Current.GetInstance<IContentLoader>(), ServiceLocator.Current.GetInstance<IRelationRepository>());
      }
    public static IEnumerable<VariationContent> VariationContents(this ProductContent productContent, IContentLoader contentLoader, IRelationRepository relationRepository)
      {
        return contentLoader.GetItems(productContent.GetVariants(relationRepository), productContent.Language).OfType<VariationContent>();
      }
  }

public class SiteCatalogContentClientConventions : CatalogContentClientConventions
  {
    protected override void ApplyProductContentConventions(TypeConventionBuilder<ProductContent> conventionBuilder)
      {
        base.ApplyProductContentConventions(conventionBuilder);
        conventionBuilder
          .ExcludeField(x => x.Variations())
          .IncludeField(x => x.VariationContents());
      }
  }

Adding default price and prices in product content

The following example shows how to

  • index the highest default price for product variants
  • index prices from variants in the Prices extension method
public static class ProductContentExtensions
  {
    public static Price DefaultPrice(this ProductContent productContent)
      {
        return DefaultPrice(productContent, ServiceLocator.Current.GetInstance<ReadOnlyPricingLoader>(), ServiceLocator.Current.GetInstance<IRelationRepository>());
      }

    public static Price DefaultPrice(this ProductContent productContent, ReadOnlyPricingLoader pricingLoader, IRelationRepository relationRepository)
      {
        var maxPrice = new Price();
        var variationLinks = productContent.GetVariants(relationRepository);
        foreach (var variationLink in variationLinks)
          {
            var defaultPrice = pricingLoader.GetDefaultPrice(variationLink);
            if (defaultPrice.UnitPrice.Amount > maxPrice.UnitPrice.Amount)
              {
                maxPrice = defaultPrice;
              }
          }
        return maxPrice;
      }

    public static IEnumerable<Price> Prices(this ProductContent productContent)
      {
        return Prices(productContent, ServiceLocator.Current.GetInstance<ReadOnlyPricingLoader>(), ServiceLocator.Current.GetInstance<IRelationRepository>());
      }

    public static IEnumerable<Price> Prices(this ProductContent productContent, ReadOnlyPricingLoader pricingLoader, IRelationRepository relationRepository)
      {
        var variationLinks = productContent.GetVariants(relationRepository);
        return variationLinks.SelectMany(variationLink => pricingLoader.GetPrices(variationLink, null, Enumerable.Empty<CustomerPricing>()));
      }

    public static IEnumerable<VariationContent> VariationContents(this ProductContent productContent)
      {
        return VariationContents(productContent, ServiceLocator.Current.GetInstance<IContentLoader>(), ServiceLocator.Current.GetInstance<IRelationRepository>());
      }

    public static IEnumerable<VariationContent> VariationContents(this ProductContent productContent, IContentLoader contentLoader, IRelationRepository relationRepository)
      {
        return contentLoader.GetItems(productContent.GetVariants(relationRepository), productContent.Language).OfType<VariationContent>();
      }
  }

public class SiteCatalogContentClientConventions : CatalogContentClientConventions
  {
    protected override void ApplyProductContentConventions(TypeConventionBuilder<ProductContent> conventionBuilder)
      {
        base.ApplyProductContentConventions(conventionBuilder);
        conventionBuilder
          .ExcludeField(x => x.Variations())
          .IncludeField(x => x.VariationContents())
          .IncludeField(x => x.DefaultPrice())
          .IncludeField(x => x.Prices());
      }
  }

Adding inventory in product content

The following example shows how to index the inventories from variants.

public static class ProductContentExtensions
  {
    public static IEnumerable Inventories(this ProductContent productContent)
      {
        return Inventories(
          productContent,
          ServiceLocator.Current.GetInstance<IContentLoader>(),
          ServiceLocator.Current.GetInstance<InventoryLoader>(),
          ServiceLocator.Current.GetInstance<IRelationRepository>());
      }

    public static IEnumerable<Inventory> Inventories(this ProductContent productContent, IContentLoader contentLoader, InventoryLoader inventoryLoader, IRelationRepository relationRepository)
      {
        var variations = contentLoader.GetItems(productContent.GetVariants(relationRepository), productContent.Language).OfType<VariationContent>();
        return variations.SelectMany(x => x.GetStockPlacement(inventoryLoader));
      }

    public static Price DefaultPrice(this ProductContent productContent)
      {
        return DefaultPrice(productContent, ServiceLocator.Current.GetInstance<ReadOnlyPricingLoader>(), ServiceLocator.Current.GetInstance<IRelationRepository>());
      }

    public static Price DefaultPrice(this ProductContent productContent, ReadOnlyPricingLoader pricingLoader, IRelationRepository relationRepository)
      {
        var maxPrice = new Price();
        var variationLinks = productContent.GetVariants(relationRepository);
        foreach (var variationLink in variationLinks)
          {
            var defaultPrice = pricingLoader.GetDefaultPrice(variationLink);
            if (defaultPrice.UnitPrice.Amount > maxPrice.UnitPrice.Amount)
              {
                maxPrice = defaultPrice;
              }
          }
        return maxPrice;
      }

    public static IEnumerable<Price> Prices(this ProductContent productContent)
      {
        return Prices(productContent, ServiceLocator.Current.GetInstance<ReadOnlyPricingLoader>(), ServiceLocator.Current.GetInstance<IRelationRepository>());
      }

    public static IEnumerable<Price> Prices(this ProductContent productContent, ReadOnlyPricingLoader pricingLoader, IRelationRepository relationRepository)
      {
        var variationLinks = productContent.GetVariants(relationRepository);
        return variationLinks.SelectMany(variationLink => pricingLoader.GetPrices(variationLink, null, Enumerable.Empty<CustomerPricing>()));
      }

    public static IEnumerable<VariationContent> VariationContents(this ProductContent productContent)
      {
        return VariationContents(productContent, ServiceLocator.Current.GetInstance<IContentLoader>(), ServiceLocator.Current.GetInstance<IRelationRepository>());
      }

    public static IEnumerable<VariationContent> VariationContents(this ProductContent productContent, IContentLoader contentLoader, IRelationRepository relationRepository)
      {
        return contentLoader.GetItems(productContent.GetVariants(relationRepository), productContent.Language).OfType<VariationContent>();
      }
  }

public class SiteCatalogContentClientConventions : CatalogContentClientConventions
  {
    protected override void ApplyProductContentConventions(TypeConventionBuilder<ProductContent> conventionBuilder)
      {
        base.ApplyProductContentConventions(conventionBuilder);
        conventionBuilder
          .ExcludeField(x => x.Variations())
          .IncludeField(x => x.VariationContents())
          .IncludeField(x => x.DefaultPrice())
          .IncludeField(x => x.Prices())
          .IncludeField(x => x.Inventories());
      }
  }

Excluding indexing of variant content

Exclude the variant content from being indexed by calling ShouldIndex for the type, using the conventions API.

public class SiteCatalogContentClientConventions : CatalogContentClientConventions
  {
    protected override void ApplyProductContentConventions(ClientConventions.TypeConventionBuilder<ProductContent> conventionBuilder)
      {
        base.ApplyProductContentConventions(conventionBuilder);
        conventionBuilder
          .ExcludeField(x => x.Variations())
          .IncludeField(x => x.VariationContents())
          .IncludeField(x => x.DefaultPrice())
          .IncludeField(x => x.Prices())
          .IncludeField(x => x.Inventories());
      }

   public override void ApplyConventions(IClientConventions clientConventions)
      {
        base.ApplyConventions(clientConventions);
        // Uncomment line below if we don't index VariationContent
        // ContentIndexer.Instance.Conventions.ForInstancesOf<VariationContent>().ShouldIndex(x => false);
        SearchClient.Instance.Conventions.NestedConventions.ForInstancesOf<ProductContent>().Add(x => x.VariationContents());
      }
  }

Listening for price and inventory changes

The CatalogContentEventListener class listens for price changes for classes that implement IPricing, and inventories for classes that implement IStockPlacement. VariationContent implements both of them, but ProductContent does not implement them. Therefore, override the default implementation and make some changes.

[InitializableModule]
[ModuleDependency(typeof(FindCommerceInitializationModule))]
public class InitializationModule : IConfigurableModule
  {
    public void Initialize(InitializationEngine context)
      {
        context.Locate.Advanced.GetInstance<PriceIndexing>().IsIndexingIIndexedPrices = true;
      }
    public void Uninitialize(InitializationEngine context)
      {
      }
    public void ConfigureContainer(ServiceConfigurationContext context)
      {
        context.Services.AddSingleton<CatalogContentClientConventions, 
          SiteCatalogContentClientConventions>();
      }
  }

public class SiteCatalogContentEventListener : CatalogContentEventListener
  {
    private ReferenceConverter _referenceConverter;
    private readonly IContentRepository _contentRepository;
    private readonly IRelationRepository _relationRepository;

    public SiteCatalogContentEventListener(
      ReferenceConverter referenceConverter,
      IContentRepository contentRepository,
      IClient client,
      CatalogEventIndexer indexer,
      CatalogContentClientConventions catalogContentClientConventions,
      PriceIndexing priceIndexing, 
      IRelationRepository relationRepository)
      : base(
              referenceConverter,
              contentRepository,
              client,
              indexer, 
              catalogContentClientConventions,
              priceIndexing)
      {
        _referenceConverter = referenceConverter;
        _contentRepository = contentRepository;
        _relationRepository = relationRepository;
      }
    protected override void IndexContentsIfNeeded(IEnumerable<ContentRefenrence> contentLinks, IDictionary<Type, bool> cachedReindexContentOnEventForType, Func<bool> isReindexingContentOnUpdates)
      {
        // Update parent contents
        var contents = _contentRepository.GetItems(contentLinks, CultureInfo.InvariantCulture).ToList();
        var parentContentLinks = new List<ContentReference>();
        foreach (var parents in contents.OfType().Select(content => _contentRepository.GetItems(content.GetParentProducts(_relationRepository), CultureInfo.InvariantCulture).Select(c => c.ContentLink).ToList()))
          {
            parentContentLinks.AddRange(parents);
          }
          // If index variants still needed, keep the line below
          // IndexContentsIfNeeded(contentLinks, GetIndexContentAction(), cachedReindexContentOnEventForType, isReindexingContentOnUpdates);

          IndexContentsIfNeeded(parentContentLinks, GetIndexContentAction(), cachedReindexContentOnEventForType, isReindexingContentOnUpdates);
      }
  }
Do you find this information helpful? Please log in to provide feedback.

Last updated: Jul 02, 2021

Recommended reading