Code a la Mode banner

Extending Coveo's "Numeric Slider Facet" Component to Search Over Multiple Fields - Part 1

In this post:
  1. 1. Out-of-the-box Coveo Hive
  2. 2. The challenge
  3. 3. The solution
  4. 4. Let’s code!
    1. 4.1. Investigate the out-of-the-box slider
    2. 4.2. Create custom datasource templates
    3. 4.3. Create custom model to process our datasource and settings
    4. 4.4. Add validation for custom parameters
    5. 4.5. Create the rendering definition item
    6. 4.6. Create computed fields for the minimum and maximum values
  5. 5. Wrap-up
  6. 6. Up Next

Today we will build a custom Coveo slider facet that filters based on whether the numeric range of a Sitecore item overlaps with the numeric range specified by the user. We will extend an out-of-the-box Hive rendering with extra fields and validation. This tutorial assumes you know the basics of working with the Coveo for Sitecore Hive framework.

Out-of-the-box Coveo Hive

Coveo for Sitecore is a very feature-rich framework that ships with several configurable facet components for filtering your search results. The out-of-the-box facets support most common use-cases. For example, if our items have a “Price” field, we can use the Numeric Slider Facet or Numeric Range Facet components to show only the items whose Price value falls within the user-defined range.

The challenge

But what if our search result items are associated with a range of prices? For example:

  • If we are building a search for a multi-brand site that shows different Product Families or Brands as results. Each result represents multiple products, so it correlates to a range of prices.
  • If we are building a search for a travel site that shows different flight paths as results. Each result represents a variety of plane ticket options, so it correlates to a range of prices.

The solution

To filter this type of data we need to get our hands a little dirty and create a custom facet component - one that determines whether a range of values on a Sitecore item falls within a range specified by the user. I call this component Slider Facet For Range. Our strategy will be to create 2 computed fields - 1 for the minimum value and 1 for the maximum value - and extend the slider facet to query against both fields.

Note:
My implementation was tested on the May 2018 release of Coveo for Sitecore.

Let’s code!

Investigate the out-of-the-box slider

Coveo Numeric Slider Facet rendering is a View Rendering that uses /sitecore/layout/Models/Coveo Hive/Facets/Coveo Facet Slider as the model and /sitecore/templates/Coveo Hive/Facets/Coveo Numeric Facet Slider as the datasource.

The purpose of the model is to read the user-configured parameters on the datasource and pass them to the JavaScript framework. We will need to add some custom parameters, so let’s start by creating a custom model and a custom datasource template.

Create custom datasource templates

We don’t want to re-use the settings template that the default slider template inherits from because it contains a field called FacetFIeld, which defines the single field used by the facet. Instead, we need a settings item that defines 2 fields, 1 for the minimum value and 1 for the maximum value.

Create a new template called Numeric Slider For Range Facet Settings

Mark the MinValueField and MaxValueFields fields as required.

Create a new template called Facet Slider For Range and add the following as Base Templates:

  • /sitecore/templates/Coveo Hive/Base UI Component
  • /sitecore/templates/Coveo Hive/Base UI Component
  • Numeric Slider For Range Facet Settings

This inheritance chain gives our datasource all the fields and settings from the base component except FacetField, and we gain 2 new fields - MinValueField and MaxValueField

Create custom model to process our datasource and settings

Create an item in /sitecore/layouts/Models called Facet Slider For Range. On the backend, Coveo handles models with complex inheritance by using interfaces. Following the Coveo pattern, our backend classes look like this:

FacetSliderForRangeProperties to parse our custom settings

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
namespace Project.Areas.AreaName.Models
{
using Coveo.UI.Components.ModelProperties.Facets;
using Coveo.UI.Components.SearchUiProperties;
using Coveo.UI.Components.SearchUiProperties.PropertySerializer;
using Coveo.UI.Components.SitecoreProperties;

public class FacetSliderForRangeProperties : FacetSliderProperties
{
[SitecoreProperty("MinValueField")]
[SearchUiProperty(Prebind = "fieldTranslator", PropertySerializer = typeof(FieldNameSerializer))]
public string MinField { get; set; }

[SitecoreProperty("MaxValueField")]
[SearchUiProperty(Prebind = "fieldTranslator", PropertySerializer = typeof(FieldNameSerializer))]
public string MaxField { get; set; }
}
}

FacetSliderForRangeModel to associate datasource model with settings model, to specify the Coveo CSS class, and to specify validation rules

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
namespace Project.Areas.AreaName.Models
{
using Coveo.UI.Components.Models;
using Coveo.UI.Components.ModelValidators;

public interface IFacetSliderForRangeModel : IFacetSliderForRangeModel<FacetSliderForRangeProperties>
{
}

public interface IFacetSliderForRangeModel<out T> : IModelWithProperties<T>, IModelWithRawProperties, IBaseComponentModel
where T : FacetSliderForRangeProperties, new()
{
}

/// <summary>
/// This class is based on Coveo.UI.Components.Models.Facets.FacetSliderModel
/// </summary>
public class FacetSliderForRangeModel : FacetSliderForRangeModel<FacetSliderForRangeProperties>, IFacetSliderForRangeModel
{
}

public class FacetSliderForRangeModel<TFacetSliderProperties> : BaseModelWithProperties<TFacetSliderProperties>, IFacetSliderForRangeModel<TFacetSliderProperties>
where TFacetSliderProperties : FacetSliderForRangeProperties, new()
{
public override string CssClass => "CoveoFacetSliderForRange";

protected override void OnPropertiesInitialized()
{
AddOnValidationHandler(() => new FacetSliderForRangePropertiesValidator(Labels, Properties));
AddOnValidationHandler(() => new ComponentWithDataSourceValidator(this));
}
}
}

Add validation for custom parameters

Coveo shows validation messages in Experience Editor when a datasource is missing required fields. Following the Coveo pattern, we need to create a validator for our custom parameters (it’s used by the FacetSliderForRangeModel class in OnPropertiesInitialized)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
namespace Project.Areas.AreaName.Models
{
using Coveo.UI.Components.ModelValidators;
using Coveo.UI.Core.ErrorReports;
using Coveo.UI.Core.Strings;
using Sitecore.Diagnostics;

/// <summary>
/// This class is based on Coveo.UI.Components.ModelValidators.Facets.FacetSliderPropertiesValidator
/// </summary>
public class FacetSliderForRangePropertiesValidator : IComponentModelValidator
{
private readonly FacetSliderForRangeProperties properties;
private readonly ILocalizedStringFetcher localizedStringFetcher;

public FacetSliderForRangePropertiesValidator(ILocalizedStringFetcher localizedStringFetcher, FacetSliderForRangeProperties properties)
{
Assert.IsNotNull(localizedStringFetcher, "localizedStringFetcher");
Assert.IsNotNull(properties, "properties");
this.properties = properties;
this.localizedStringFetcher = localizedStringFetcher;
}

public void AddToReport(IErrorReport errorReport)
{
Assert.IsNotNull(errorReport, "errorReport");

if (string.IsNullOrEmpty(this.properties.MinField))
{
errorReport.AddError(this.localizedStringFetcher["The Min Value Field name must be set."]);
}

if (string.IsNullOrEmpty(this.properties.MaxField))
{
errorReport.AddError(this.localizedStringFetcher["The Max Value Field name must be set."]);
}
}
}
}

With this validator class in place, inserting the Slider Facet For Range component without filling in the min and max fields will result in errors like this:

Note:
The text in the error message comes from Dictionary entries, so this is compatible with multi-lingual sites.

Create the rendering definition item

Add a rendering definition item to /sitecore/layout/Renderings/ called Facet Slider For Range. Use the model and datasource that we created earlier.

This view is a copy of the base component view, except that it uses our custom class as the Model.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@using Coveo.UI.Components
@using Coveo.UI.Components.Extensions
@model Project.Areas.AreaName.Models.IFacetSliderForRangeModel

@*This view is based on \Website\Views\Coveo Hive\Facets\Coveo Facet Slider.cshtml*@

@Html.Coveo().RenderErrorSummary(Model.ValidateModel())

@if (Model.IsConfigured)
{
@Html.Partial(Partials.DEBUG_INFORMATION, Model)
<div id='@Model.Id'
class="@Model.CssClass"
@foreach (var property in @Model.RawProperties) {
@: data-@(property.Key)='@(property.Value)'
}>
</div>
}

Create computed fields for the minimum and maximum values

Once the new component has been added to the page, you can configure it in the same way you would configure the base component (Full list of Facet Slider properties). Additionally, you will need to specify the names of the min field and max field. Use the field names from the computed field definition.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/" xmlns:set="http://www.sitecore.net/xmlconfig/set/">
<sitecore>
<coveo>
<defaultIndexConfiguration>
<fieldMap>
<fieldNames hint="raw:AddFieldByFieldName">
<fieldType fieldName="pricemin" returnType="System.Double" type="System.Double" isMultiValue="false" isDisplayField="true" isFacet="true" includeForFreeTextSearch="false" settingType="Coveo.Framework.Configuration.FieldConfiguration, Coveo.Framework" />
<fieldType fieldName="pricemax" returnType="System.Double" type="System.Double" isMultiValue="false" isDisplayField="true" isFacet="true" includeForFreeTextSearch="false" settingType="Coveo.Framework.Configuration.FieldConfiguration, Coveo.Framework" />
</fieldNames>
</fieldMap>
<documentOptions>
<fields hint="raw:AddComputedIndexField">
<field fieldName="pricemin">Project.Search.ComputedFields.PriceMinComputedField,Project</field>
<field fieldName="pricemax">Project.Search.ComputedFields.PriceMaxComputedField,Project</field>
</fields>
</documentOptions>
</defaultIndexConfiguration>
</coveo>
</sitecore>
</configuration>

You can explicitly specify the min and max values of the facet range using the FacetSliderStart and FacetSliderEnd fields. Or you can leave these fields blank to allow the component to dynamically generate the range based on the lowest value in min field and the highest value in max field.

Wrap-up

Now that all of our Sitecore items and C# classes are set up, we can add our custom facet to the search page and use the custom parameters to define min and max fields that the facet should filter on.

Up Next

In the next post we will transition to the JavaScript side of customizing Coveo. We will go over how to create a custom TypeScript component that uses lazy loading and extends from Coveo.FacetSlider.
Go to Part 2

Bon Appétit!