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 (2)
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 ๐