Intro
I've been doing a few projects where we used the new facet engine in Examine 4, have seen several questions about it so figured I throw together a basic example.
This example is using the Umbraco starter kit: https://github.com/umbraco/The-Starter-Kit/tree/v13/dev
All the products have prices between 2 and 1899, the goal now is to set up facets for the price where it shows facet price range values.
To get facets running we first need to do a few things:
Enabling facets
First thing is to update the Examine NuGet package version to one that has facets (as of writing this it is 4.0.0-beta.1).
So after installing it I have this in my csproj:
<PackageReference Include="Examine" Version="4.0.0-beta.1" />
Next I need to ensure that the price field is indexed as a facet field. As this is in the external index it can be changed by adding new config:
In a composer:
public class SearchComposer : IComposer
{
public void Compose(IUmbracoBuilder builder)
{
builder.Services.ConfigureOptions<ConfigureExternalIndexOptions>();
}
}
ConfigureExternalIndexOptions:
using Examine;
using Examine.Lucene;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Core;
namespace FacetBlog.Search;
public class ConfigureExternalIndexOptions : IConfigureNamedOptions<LuceneDirectoryIndexOptions>
{
private readonly IServiceProvider _serviceProvider;
public ConfigureExternalIndexOptions(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
public void Configure(string name, LuceneDirectoryIndexOptions options)
{
if (name.Equals(Constants.UmbracoIndexes.ExternalIndexName))
{
// Index the price field as a facet of the type long (int64)
options.FieldDefinitions.AddOrUpdate(new FieldDefinition("price", FieldDefinitionTypes.FacetTaxonomyLong));
options.UseTaxonomyIndex = true;
// The standard directory factory does not work with the taxonomi index.
// If running on azure it should use the syncedTemp factory
options.DirectoryFactory = _serviceProvider.GetRequiredService<global::Examine.Lucene.Directories.TempEnvFileSystemDirectoryFactory>();
}
}
public void Configure(LuceneDirectoryIndexOptions options)
{
throw new System.NotImplementedException();
}
}
At this point if the index is rebuilt it will add the facet fields for the price.
Adding a searchservice
Can now add a bit of search logic so show the facet values on the frontend when searching on products. First we add a SearchService:
using Examine;
using Examine.Lucene;
using Examine.Search;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Cms.Web.Common;
namespace FacetBlog.Search;
public interface ISearchService
{
SearchResult SearchProducts(string query);
}
public class SearchResult
{
public IEnumerable<IPublishedContent> NodeResults { get; set; }
public IEnumerable<IFacetValue> Facets { get; set; }
}
public class SearchService : ISearchService
{
private readonly IExamineManager _examineManager;
private readonly UmbracoHelper _umbracoHelper;
public SearchService(IExamineManager examineManager, UmbracoHelper umbracoHelper)
{
_examineManager = examineManager;
_umbracoHelper = umbracoHelper;
}
public SearchResult SearchProducts(string query)
{
var res = new SearchResult();
var nodeResult = new List<IPublishedContent>();
var facetResults = new List<IFacetValue>();
if (_examineManager.TryGetIndex("ExternalIndex", out IIndex? index))
{
// Start searching product pages
var queryBuilder = index
.Searcher
.CreateQuery("content")
.NodeTypeAlias("product");
// If a query string is added, search the nodenames for that query string
if (!string.IsNullOrEmpty(query))
{
queryBuilder.And()
.Field("nodeName", query.MultipleCharacterWildcard());
}
// Add facets for the price field split up into several ranges
var results = queryBuilder
.WithFacets(f => f.FacetLongRange("price", new[]
{
new Int64Range("0-100", 0, true, 100, false),
new Int64Range("100-500", 100, true, 500, false),
new Int64Range("500-1500", 500, true, 1500, false),
new Int64Range("1500+", 1500, true, long.MaxValue, true)
}))
.Execute();
// Loop through results and add to a list of IPublishedContent
foreach (var result in results)
{
nodeResult.Add(_umbracoHelper.Content(int.Parse(result.Id)));
}
var priceFacet = results.GetFacet("price");
// Loop through facet results and add to a list
foreach (var facetValue in priceFacet)
{
facetResults.Add(facetValue);
}
}
res.NodeResults = nodeResult;
res.Facets = facetResults;
return res;
}
}
The main difference to a "normal" search is that before we execute the search we can add a list of facet fields we want to get facets for based on the result set. Because our facet in this example is a number we may not want to get every single number but instead get the ones in certain ranges - that is configured like this:
var results = queryBuilder
.WithFacets(f => f.FacetLongRange("price", new[]
{
new Int64Range("0-100", 0, true, 100, false),
new Int64Range("100-500", 100, true, 500, false),
new Int64Range("500-1500", 500, true, 1500, false),
new Int64Range("1500+", 1500, true, long.MaxValue, true)
}))
.Execute();
Then once we retrieve the result the facets can be gotten based on the facet fieldname:
var priceFacet = results.GetFacet("price");
Now in the view we can add a bit of markup with an input field and outputting a list of results and a list of facets:
<form action="@Model.Url()" method="get">
<input type="text" placeholder="Search" name="query" value="@Context.Request.Query["query"].FirstOrDefault()"/>
<button>Search</button>
</form>
<div>
@if (Model.SearchResults.NodeResults.Any())
{
<p>Content results:</p>
<ul>
@foreach (var content in Model.SearchResults.NodeResults)
{
<li>
<a href="@content.Url()">@content.Name</a>
</li>
}
</ul>
<p>Facet results:</p>
<ul>
@foreach (var facet in Model.SearchResults.Facets)
{
<li>
@facet.Label - amount: @facet.Value
</li>
}
</ul>
}
else if(Model.HasSearched)
{
<p>No results found</p>
}
</div>
And now we get something like this:
And with a query:
Outro
This is a simple example so there are likely several use cases that are not covered - if you have any specific requests for what I can cover next let me know in a comment 🙂.
Feel free to reach out to me on Mastodon and let me know if you liked the blogpost: https://umbracocommunity.social/@Jmayn


Top comments (4)
Great post. Your timing is perfect. There's not much online already about how to do this, this is a perfect start for ten.
Thanks David!
I know it's been on my list for a while, just had some trouble narrowing it down to a useable example that wasn't humongous 😊
Hi Jesper Mayntzhusen,
Is there a way to support this in Umbraco version 17?
Hey Shifra,
For Umbraco 17 I would go with the new search abstraction as that is the way Umbraco is going for search. It is also quite nice in that you can swap out the search provider with almost no work.
I would start on this repo: github.com/umbraco/Umbraco.Cms.Search
And also check out the articles on kjac.dev/ about search.
This is what will be the new standard in Umbraco in future versions.