In this sample we are altering the external index and thus we name the class ConfigureExternalIndexOptions. If you are altering multiple indexes, it is recommended to have separate classes for each index - i.e. ConfigureExternalIndexOptions for the external index, ConfigureInternalIndexOptions for the internal index and so on.
When using the ConfigureNamedOptions pattern, we have to register this in a composer for it to configure our indexes, this can be done like this:
By default, Examine will store values into the Lucene index as "Full Text" fields, meaning the values will be indexed and analyzed for a textual search. However, if a field value is numerical, date/time, or another non-textual value type, you might want to change how the value is stored in the index. This will let you take advantage of some value type-specific search features such as numerical or date range.
The easiest way to modify how a field is configured is using the ConfigureNamedOptions pattern like so:
usingExamine;usingExamine.Lucene;usingMicrosoft.Extensions.Options;usingUmbraco.Cms.Core;namespaceUmbraco.Docs.Samples.Web.CustomIndexing;publicclassConfigureExternalIndexOptions:IConfigureNamedOptions<LuceneDirectoryIndexOptions>{publicvoidConfigure(string name,LuceneDirectoryIndexOptions options) {if (name.Equals(Constants.UmbracoIndexes.ExternalIndexName)) {options.FieldDefinitions.AddOrUpdate(newFieldDefinition("price",FieldDefinitionTypes.Double)); } } // Part of the interface, but does not need to be implemented for this.publicvoidConfigure(LuceneDirectoryIndexOptions options) {thrownewSystem.NotImplementedException(); }}
This will ensure that the price field in the index is treated as a double type (if the price field does not exist in the index, it is added).
Changing IValueSetValidator
An IValueSetValidator is responsible for validating a ValueSet to see if it should be included in the index. For example, by default the validation process for the ExternalIndex checks if a ValueSet has a category type of either "media" or "content" (not member). If a ValueSet was passed to the ExternalIndex and it did not pass this requirement it would be ignored.
The IValueSetValidator is also responsible for filtering the data in the ValueSet. For example, by default the validator for the MemberIndex will validate on all the default member properties, so an extra property "phoneNumber", would not pass validation, and therefore not be included.
The IValueSetValidator implementation for the built-in indexes, can be changed like this:
usingExamine.Lucene;usingMicrosoft.Extensions.Options;usingUmbraco.Cms.Core;usingUmbraco.Cms.Infrastructure.Examine;namespaceUmbraco.Docs.Samples.Web.CustomIndexing;publicclassConfigureMemberIndexOptions:IConfigureNamedOptions<LuceneDirectoryIndexOptions>{publicvoidConfigure(string name,LuceneDirectoryIndexOptions options) {if (name.Equals(Constants.UmbracoIndexes.MembersIndexName)) {options.Validator=newMemberValueSetValidator(null,null,new[] { "id","nodeName","updateDate","loginName","email","__Key","phoneNumber" },null); } } // Part of the interface, but does not need to be implemented for this.publicvoidConfigure(LuceneDirectoryIndexOptions options) {thrownewSystem.NotImplementedException(); }}
Remember to register ConfigureMemberIndexOptions in your composer.
Creating your own index
The following example will show how to create an index that will only include nodes based on the document type product.
We always recommend that you use the existing built in ExternalIndex. You should then query based on the NodeTypeAlias instead of creating a new separate index based on that particular node type. However, should the need arise, the example below will show you how to do it.
Take a look at our Examine Quick Start to see some examples of how to search the ExternalIndex.
To create this index we need five things:
An UmbracoExamineIndex implementation that defines the index.
An IConfigureNamedOptions implementation that configures the index fields and options.
An IValueSetBuilder implementation that builds index value sets a piece of content.
An IndexPopulator implementation that populates the index with the value sets for all applicable content.
An INotificationHandler implementation that updates the index when content changes.
A composer that adds all these services to the runtime.
usingExamine;usingExamine.Lucene;usingLucene.Net.Analysis.Standard;usingLucene.Net.Index;usingLucene.Net.Util;usingMicrosoft.Extensions.Options;usingUmbraco.Cms.Core.Configuration.Models;namespaceUmbraco.Docs.Samples.Web.CustomIndexing;publicclassConfigureProductIndexOptions:IConfigureNamedOptions<LuceneDirectoryIndexOptions>{privatereadonlyIOptions<IndexCreatorSettings> _settings;publicConfigureProductIndexOptions(IOptions<IndexCreatorSettings> settings)=> _settings = settings;publicvoidConfigure(string? name,LuceneDirectoryIndexOptions options) {if (name?.Equals("ProductIndex") isfalse) {return; }options.Analyzer=newStandardAnalyzer(LuceneVersion.LUCENE_48);options.FieldDefinitions=new(new("id",FieldDefinitionTypes.Integer),new("name",FieldDefinitionTypes.FullText) );options.UnlockIndex=true;if (_settings.Value.LuceneDirectoryFactory==LuceneDirectoryFactory.SyncedTempFileSystemDirectoryFactory) { // if this directory factory is enabled then a snapshot deletion policy is requiredoptions.IndexDeletionPolicy=newSnapshotDeletionPolicy(newKeepOnlyLastCommitDeletionPolicy()); } } // not usedpublicvoidConfigure(LuceneDirectoryIndexOptions options) =>thrownewNotImplementedException();}
ProductIndexValueSetBuilder
usingExamine;usingUmbraco.Cms.Core.Models;usingUmbraco.Cms.Infrastructure.ExaminenamespaceUmbraco.Docs.Samples.Web.CustomIndexing;publicclassProductIndexValueSetBuilder:IValueSetBuilder<IContent>{publicIEnumerable<ValueSet> GetValueSets(paramsIContent[] contents) {foreach (IContent content incontents.Where(CanAddToIndex)) {var indexValues =newDictionary<string,object> { // this is a special field used to display the content name in the Examine dashboard [UmbracoExamineFieldNames.NodeNameFieldName] =content.Name!, ["name"] =content.Name!, // add the fields you want in the index ["nodeName"] =content.Name!, ["id"] =content.Id, };yieldreturnnewValueSet(content.Id.ToString(),IndexTypes.Content,content.ContentType.Alias, indexValues); } } // filter out all content types except "product"privateboolCanAddToIndex(IContent content) =>content.ContentType.Alias=="product";}
This is only an example of how you could do indexing. In this example, we're indexing all content, both published and unpublished.
In certain scenarios only published content should be added to the index. To achieve that, you will need to implement your own logic to filter out unpublished content. This can be somewhat tricky as the published state can vary throughout an entire structure of content nodes in the content tree. For inspiration on how to go about such filtering, you can look at the ContentIndexPopulator in Umbraco.
ProductIndexingNotificationHandler
The index will only update its content when you manually trigger an index rebuild in the Examine dashboard. This is not always the desired behavior for a custom index.
To update your index when content changes, you can use notification handlers.
The following handler class does not automatically update the descendant items of the modified content nodes, such as removing descendants of deleted content. If changes to the parent content item can affect its children or descendant items in your setup, please refer to the UmbracoContentIndex.PerformDeleteFromIndex() in Umbraco. Such logic should be applied when both removing and reindexing content items of type product.
usingExamine;usingUmbraco.Cms.Core;usingUmbraco.Cms.Core.Cache;usingUmbraco.Cms.Core.Events;usingUmbraco.Cms.Core.Models;usingUmbraco.Cms.Core.Notifications;usingUmbraco.Cms.Core.Services;usingUmbraco.Cms.Core.Services.Changes;usingUmbraco.Cms.Core.Sync;usingUmbraco.Cms.Infrastructure;usingUmbraco.Cms.Infrastructure.Search;namespaceUmbraco.Docs.Samples.Web.CustomIndexing;publicclassProductIndexingNotificationHandler:INotificationHandler<ContentCacheRefresherNotification>{privatereadonlyIRuntimeState _runtimeState;privatereadonlyIUmbracoIndexingHandler _umbracoIndexingHandler;privatereadonlyIExamineManager _examineManager;privatereadonlyIContentService _contentService;privatereadonlyProductIndexValueSetBuilder _productIndexValueSetBuilder;publicProductIndexingNotificationHandler(IRuntimeState runtimeState,IUmbracoIndexingHandler umbracoIndexingHandler,IExamineManager examineManager,IContentService contentService,ProductIndexValueSetBuilder productIndexValueSetBuilder) { _runtimeState = runtimeState; _umbracoIndexingHandler = umbracoIndexingHandler; _examineManager = examineManager; _contentService = contentService; _productIndexValueSetBuilder = productIndexValueSetBuilder; } /// <summary> /// Updates the index based on content changes. /// </summary>publicvoidHandle(ContentCacheRefresherNotification notification) {if (NotificationHandlingIsDisabled()) {return; }if (!_examineManager.TryGetIndex("ProductIndex",outIIndex? index)) {thrownewInvalidOperationException("Could not obtain the product index"); }ContentCacheRefresher.JsonPayload[] payloads =GetNotificationPayloads(notification);foreach (ContentCacheRefresher.JsonPayload payload in payloads) { // Removeif (payload.ChangeTypes.HasType(TreeChangeTypes.Remove)) {index.DeleteFromIndex(payload.Id.ToString()); } // Reindexelseif (payload.ChangeTypes.HasType(TreeChangeTypes.RefreshNode) ||payload.ChangeTypes.HasType(TreeChangeTypes.RefreshBranch)) {IContent? content =_contentService.GetById(payload.Id);if (content ==null||content.Trashed) {index.DeleteFromIndex(payload.Id.ToString());continue; }IEnumerable<ValueSet> valueSets =_productIndexValueSetBuilder.GetValueSets(content);index.IndexItems(valueSets); } } }privateboolNotificationHandlingIsDisabled() { // Only handle events when the site is running.if (_runtimeState.Level!=RuntimeLevel.Run) {returntrue; }if (_umbracoIndexingHandler.Enabled==false) {returntrue; }if (Suspendable.ExamineEvents.CanIndex==false) {returntrue; }returnfalse; }privateContentCacheRefresher.JsonPayload[] GetNotificationPayloads(CacheRefresherNotification notification) {if (notification.MessageType!=MessageType.RefreshByPayload||notification.MessageObjectisnotContentCacheRefresher.JsonPayload[] payloads) {thrownewNotSupportedException(); }return payloads; }}
You can find further inspiration for implementing notification handlers (for example, for media updates) in the UmbracoExamine.PDF package.