Indexing Prices

Since Ucommerce 10.6.0, prices can be indexed in one of two ways: As part of the product index, or in its own index. What mode to use depends entirely on the use case.

Overview

Before Ucommerce 10.6.0, prices were always included as part of the product index - one price for each price group in Ucommerce:

{
  "Name": "Support",
  "PrimaryImageUrl": "ImageNotFoundImageURL",
  "DisplayName": "Support",
  "Sku": "200-000-001",
  "UnitPrices": [
    "EUR 15 pct": 100.00,
    "USD 5 pct": 110.00,
    ...
  ],
  "PricesInclTax": [
    "EUR 15 pct": 115.00,
    "USD 5 pct": 115.50,
    ...
  ],
  "Tax": [
    "EUR 15 pct": 15.00,
    "USD 5 pct": 5.50,
    ...
  ],
  ...
}

This doesn't scale well for use cases with many price groups. Therefore, the option to index prices in their own index has been added in Ucommerce 10.6.0.

By default, indexing works as it used to - having prices in the product index. To change the indexing mode, simply add the following setting in Program.cs:

...
.AddSearch(options => options.IncludePricesInProductIndex = false)
...

Or put the following in appsettings.json:

{
    "Ucommerce":
    {
        ...,
        "Search":
        {
            ...,
            "IncludePricesInProductIndex": false,
            ...
        },
        ...
    }
}

A rebuild of the indices is needed after changing the indexing mode.

Using the Price Index

When indexing prices in their own index, use IIndex<PriceSearchModel> to work with price data. The PriceSearchModel contains the following information:

{
  "ProductGuid": "a11b433e-742e-4f0f-9755-371265cf04ca",
  "MinimumQuantity": 1,
  "PriceGroupGuid": "8769e717-08d2-4313-82a9-30d4f4886663",
  "Tax": 524.25,
  "UnitPrice": 3495,
  "Id": "e57d3c48-a502-ed11-a2f4-e45f665ce0d6",
  "PriceInclTax": 4019.25
}

This means that there's no longer a need for the price group name, as in the product index. Instead, use the GUIDs to filter.

Populating Products with Price(s)

If the code base heavily depends on having prices as part of the product model, but the use case favors using the price index, mimicking the existing product model's price structure is easy. Just populate the PricesInclTax, UnitPrices and Taxes properties using the name of the price group as the key, e.g.:

foreach (var product in viewModel.Products)
{
    var price = prices.Results.FirstOrDefault(p => p.ProductGuid == product.Id);
    if (price is not null)
    {
        product.PricesInclTax[priceGroup.Name] = price.PriceInclTax;
        product.UnitPrices[priceGroup.Name] = price.UnitPrice;
        product.Taxes[priceGroup.Name] = price.Tax;
    }
}

Facets

Prices in the Product Index

When having prices as part of the product index, the properties containing price information are dictionaries. This means that they must be configured in the index definition individually for each price group, as shown below:

this.Field(p => p.UnitPrices["EUR 15 pct"])
    .Facet()
    .AutoRanges(count: 5, precision: 10);
 
this.Field(p => p.UnitPrices["USD 5 pct"])
    .Facet()
    .AutoRanges(count: 5, precision: 100);

Using the Price Index

When using the price index, faceted search works exactly like faceted search on other fields:

this.Field(p => p.UnitPrice)
    .Facet()
    .AutoRanges(count: 5, precision: 10);

See Facets for more details on faceted search.

Filtering Products Using the Price Index

When using filters (e.g. facets), a little more work is required to make sure the filtering is done correctly because some of the filtered properties might exist on the product while others exist on the price.

First, make sure to supply each index with the correct FacetDictionary if facets are used:

var productFacetDictionary = new FacetDictionary();
var priceFacetDictionary = new FacetDictionary();

if (request.SelectedCouponFacets is not null)
{
    productFacetDictionary.Add("Coupons", request.SelectedCouponFacets);
    viewModel.SelectedFacets["Coupons"] = request.SelectedCouponFacets.ToImmutableList();
}

if (request.SelectedPriceFacets is not null)
{
    priceFacetDictionary.Add("PriceInclTax", request.SelectedPriceFacets);
    viewModel.SelectedFacets["PriceInclTax"] = request.SelectedPriceFacets.ToImmutableList();
}

var productsSearchable = _productIndex.AsSearchable(cultureInfo)
    .Where(productFacetDictionary);
var pricesSearchable = _priceIndex.AsSearchable(cultureInfo)
    .Where(priceFacetDictionary);

Second, remember to combine the filters, e.g. only look up prices for products found, and remove products without a price:

var products = productsSearchable.ToResultSet(token);
var productGuids = products.Results.Select(p => p.Id)
    .ToList();
var prices = await pricesSearchable.Where(x => productGuids.Contains(x.ProductGuid))
    .ToResultSet(token);

// Make a copy using ToList() to be able to remove products from the original list
foreach (var product in products.ToList())
{
    var price = prices.Results.FirstOrDefault(p => p.ProductGuid == product.Id);
    if (price is null)
    {
        // isPriceRangeSet can be based on the value of a price-slider or similar
        if (priceFacetDictionary.Any() || isPriceRangeSet)
        {
            products.Remove(product);
        }
    }
}

What Indexing Mode to Choose?

Performance

A huge benefit of using the price index is, that you no longer have to transfer a big product model with a lot of superfluous prices over the network. Instead, filter prices using the price group(s) and product(s) GUIDs to retrieve exactly the price(s) needed. This increases performance when the product model size increases due to a high number of price groups.

On the other hand, using the price index means there are two round trips to the index: one to get the product(s) and one to get the price(s). If the number of price groups is low, the extra round trip might be more costly than transferring the superfluous prices from the product index.

Code Maintainability

As shown above, code complexity can increase if you need to combine the product and price index results. However, the price index does not use volatile (user-editable) price group names as the key to accessing price data in a dictionary.

If performance does not clearly favor one of the modes, use the added/removed code complexity as a tie-breaker.

Last updated