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 😊