From eb7dd33acd6187a01042a77aa4c321f5a6a2eb49 Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Tue, 11 Nov 2025 13:43:59 +0100 Subject: [PATCH 1/4] Update custom examine index examples --- .../reference/searching/examine/indexing.md | 205 +++++++++++++++++- .../reference/searching/examine/indexing.md | 200 ++++++++++++++++- 2 files changed, 387 insertions(+), 18 deletions(-) diff --git a/16/umbraco-cms/reference/searching/examine/indexing.md b/16/umbraco-cms/reference/searching/examine/indexing.md index 268c3babe97..df41673507f 100644 --- a/16/umbraco-cms/reference/searching/examine/indexing.md +++ b/16/umbraco-cms/reference/searching/examine/indexing.md @@ -144,10 +144,12 @@ 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_. +### A custom Umbraco content index + +The following example will show how to create an index that will only include nodes based on the **Product**. {% hint style="info" %} -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. +We 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](quick-start.md) to see some examples of how to search the ExternalIndex. {% endhint %} @@ -161,7 +163,7 @@ To create this index we need five things: 5. An `INotificationHandler` implementation that updates the index when content changes. 6. A composer that adds all these services to the runtime. -### ProductIndex +#### ProductIndex ```csharp using Examine.Lucene; @@ -190,7 +192,7 @@ public class ProductIndex : UmbracoExamineIndex } ``` -### ConfigureProductIndexOptions +#### ConfigureProductIndexOptions ```csharp using Examine; @@ -238,7 +240,7 @@ public class ConfigureProductIndexOptions : IConfigureNamedOptions } ``` -### ProductIndexPopulator +#### ProductIndexPopulator ```csharp using Examine; @@ -326,7 +328,7 @@ This is only an example of how you could do indexing. In this example, we're ind 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](https://github.com/umbraco/Umbraco-CMS/blob/c878567633a6a3354c1414ccd130c9be518b25f0/src/Umbraco.Infrastructure/Examine/ContentIndexPopulator.cs#L115). {% endhint %} -### ProductIndexingNotificationHandler +#### 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. @@ -452,7 +454,7 @@ public class ProductIndexingNotificationHandler : INotificationHandler GetBooks() => + [ + new() { Id = 1, Title = "The Great Gatsby", Author = "F. Scott Fitzgerald", PublishedYear = 1925 }, + new() { Id = 2, Title = "To Kill a Mockingbird", Author = "Harper Lee", PublishedYear = 1960 }, + new() { Id = 3, Title = "1984", Author = "George Orwell", PublishedYear = 1949 }, + new() { Id = 4, Title = "Pride and Prejudice", Author = "Jane Austen", PublishedYear = 1813 }, + new() { Id = 5, Title = "The Catcher in the Rye", Author = "J.D. Salinger", PublishedYear = 1951 } + ]; +} +``` + +As with the previous example, we define an index. As this time we aren't indexing Umbraco data, we inherit directly from `LuceneIndex`: + +```csharp +using Examine.Lucene; +using Examine.Lucene.Providers; +using Microsoft.Extensions.Options; + +namespace Umbraco.Docs.Samples.Web.CustomIndexing; + +public class BookIndex : LuceneIndex +{ + public BookIndex( + ILoggerFactory loggerFactory, + string name, + IOptionsMonitor indexOptions) + : base(loggerFactory, name, indexOptions) + { + } +} +``` + +The index is customized and fields are defined as before via `IConfigureNamedOptions`: + +```csharp +using Examine; +using Examine.Lucene; +using Microsoft.Extensions.Options; + +namespace Umbraco.Docs.Samples.Web.CustomIndexing; + +public class ConfigureBookIndexOptions : IConfigureNamedOptions +{ + public void Configure(string? name, LuceneDirectoryIndexOptions options) + { + if (name?.Equals("BookIndex") is false) + { + return; + } + + options.FieldDefinitions = new( + new("id", FieldDefinitionTypes.Integer), + new("title", FieldDefinitionTypes.FullText), + new("author", FieldDefinitionTypes.FullText), + new("publishedYear", FieldDefinitionTypes.Integer) + ); + } + + public void Configure(LuceneDirectoryIndexOptions options) + => Configure(string.Empty, options); +} +``` + +And once again, we need a composer to register the various components: + +```csharp +using Examine; +using Umbraco.Cms.Core.Composing; +using Umbraco.Cms.Infrastructure.Examine; + +namespace Umbraco.Docs.Samples.Web.CustomIndexing; + +[ComposeAfter(typeof(AddExamineComposer))] +public class ExamineComposer : IComposer +{ + public void Compose(IUmbracoBuilder builder) + { + builder.Services.AddExamineLuceneIndex("BookIndex"); + + builder.Services.ConfigureOptions(); + } +} +``` + +With this in place, the details of the index will be available under the _Settings > Examine Management > Indexes_ screen. + +To verify indexing and querying, a controller can be used: + +```csharp +using Examine; +using Microsoft.AspNetCore.Mvc; + +namespace Umbraco.Docs.Samples.Web.CustomIndexing; + +[ApiController] +[Route("/umbraco/api/books")] +public class BooksController : ControllerBase +{ + private readonly IExamineManager _examineManager; + + public BooksController(IExamineManager examineManager) => _examineManager = examineManager; + + [HttpPost("populateIndex")] + public async Task PopulateIndex() + { + if (_examineManager.TryGetIndex("BookIndex", out IIndex? index) == false) + { + throw new InvalidOperationException("Book index not found"); + } + + List books = BookData.GetBooks(); + foreach (Book book in books) + { + index.IndexItems( + [ + ValueSet.FromObject( + book.Id.ToString(), + "Book", + book), + ]); + } + + return Ok("Done"); + } + + [HttpGet("query")] + public async Task Query(string q) + { + if (_examineManager.TryGetIndex("BookIndex", out IIndex? index) == false) + { + throw new InvalidOperationException("Book index not found"); + } + + var results = index.Searcher.Search(q) + .Select(x => new BookDto(x.Values["title"], x.Values["author"], int.Parse(x.Values["publishedYear"]))) + .ToList(); + return Ok(results); + } + + private class BookDto + { + public BookDto(string title, string author, int publishedYear) + { + Title = title; + Author = author; + PublishedYear = publishedYear; + } + + public string Title { get; } + + public string Author { get; } + + public int PublishedYear { get; } + } +} +``` + + + + + diff --git a/17/umbraco-cms/reference/searching/examine/indexing.md b/17/umbraco-cms/reference/searching/examine/indexing.md index bd2eea25ed7..55901d65a1e 100644 --- a/17/umbraco-cms/reference/searching/examine/indexing.md +++ b/17/umbraco-cms/reference/searching/examine/indexing.md @@ -144,10 +144,12 @@ 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_. +### A custom Umbraco content index + +The following example will show how to create an index that will only include nodes based on the **Product**. {% hint style="info" %} -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. +We 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](quick-start.md) to see some examples of how to search the ExternalIndex. {% endhint %} @@ -161,7 +163,7 @@ To create this index we need five things: 5. An `INotificationHandler` implementation that updates the index when content changes. 6. A composer that adds all these services to the runtime. -### ProductIndex +#### ProductIndex ```csharp using Examine.Lucene; @@ -190,7 +192,7 @@ public class ProductIndex : UmbracoExamineIndex } ``` -### ConfigureProductIndexOptions +#### ConfigureProductIndexOptions ```csharp using Examine; @@ -238,7 +240,7 @@ public class ConfigureProductIndexOptions : IConfigureNamedOptions } ``` -### ProductIndexPopulator +#### ProductIndexPopulator ```csharp using Examine; @@ -326,7 +328,7 @@ This is only an example of how you could do indexing. In this example, we're ind 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](https://github.com/umbraco/Umbraco-CMS/blob/c878567633a6a3354c1414ccd130c9be518b25f0/src/Umbraco.Infrastructure/Examine/ContentIndexPopulator.cs#L115). {% endhint %} -### ProductIndexingNotificationHandler +#### 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. @@ -452,7 +454,7 @@ public class ProductIndexingNotificationHandler : INotificationHandler GetBooks() => + [ + new() { Id = 1, Title = "The Great Gatsby", Author = "F. Scott Fitzgerald", PublishedYear = 1925 }, + new() { Id = 2, Title = "To Kill a Mockingbird", Author = "Harper Lee", PublishedYear = 1960 }, + new() { Id = 3, Title = "1984", Author = "George Orwell", PublishedYear = 1949 }, + new() { Id = 4, Title = "Pride and Prejudice", Author = "Jane Austen", PublishedYear = 1813 }, + new() { Id = 5, Title = "The Catcher in the Rye", Author = "J.D. Salinger", PublishedYear = 1951 } + ]; +} +``` + +As with the previous example, we define an index. As this time we aren't indexing Umbraco data, we inherit directly from `LuceneIndex`: + +```csharp +using Examine.Lucene; +using Examine.Lucene.Providers; +using Microsoft.Extensions.Options; + +namespace Umbraco.Docs.Samples.Web.CustomIndexing; + +public class BookIndex : LuceneIndex +{ + public BookIndex( + ILoggerFactory loggerFactory, + string name, + IOptionsMonitor indexOptions) + : base(loggerFactory, name, indexOptions) + { + } +} +``` + +The index is customized and fields are defined as before via `IConfigureNamedOptions`: + +```csharp +using Examine; +using Examine.Lucene; +using Microsoft.Extensions.Options; + +namespace Umbraco.Docs.Samples.Web.CustomIndexing; + +public class ConfigureBookIndexOptions : IConfigureNamedOptions +{ + public void Configure(string? name, LuceneDirectoryIndexOptions options) + { + if (name?.Equals("BookIndex") is false) + { + return; + } + + options.FieldDefinitions = new( + new("id", FieldDefinitionTypes.Integer), + new("title", FieldDefinitionTypes.FullText), + new("author", FieldDefinitionTypes.FullText), + new("publishedYear", FieldDefinitionTypes.Integer) + ); + } + + public void Configure(LuceneDirectoryIndexOptions options) + => Configure(string.Empty, options); +} +``` + +And once again, we need a composer to register the various components: + +```csharp +using Examine; +using Umbraco.Cms.Core.Composing; +using Umbraco.Cms.Infrastructure.Examine; + +namespace Umbraco.Docs.Samples.Web.CustomIndexing; + +[ComposeAfter(typeof(AddExamineComposer))] +public class ExamineComposer : IComposer +{ + public void Compose(IUmbracoBuilder builder) + { + builder.Services.AddExamineLuceneIndex("BookIndex"); + + builder.Services.ConfigureOptions(); + } +} +``` + +With this in place, the details of the index will be available under the _Settings > Examine Management > Indexes_ screen. + +To verify indexing and querying, a controller can be used: + +```csharp +using Examine; +using Microsoft.AspNetCore.Mvc; + +namespace Umbraco.Docs.Samples.Web.CustomIndexing; + +[ApiController] +[Route("/umbraco/api/books")] +public class BooksController : ControllerBase +{ + private readonly IExamineManager _examineManager; + + public BooksController(IExamineManager examineManager) => _examineManager = examineManager; + + [HttpPost("populateIndex")] + public async Task PopulateIndex() + { + if (_examineManager.TryGetIndex("BookIndex", out IIndex? index) == false) + { + throw new InvalidOperationException("Book index not found"); + } + + List books = BookData.GetBooks(); + foreach (Book book in books) + { + index.IndexItems( + [ + ValueSet.FromObject( + book.Id.ToString(), + "Book", + book), + ]); + } + + return Ok("Done"); + } + + [HttpGet("query")] + public async Task Query(string q) + { + if (_examineManager.TryGetIndex("BookIndex", out IIndex? index) == false) + { + throw new InvalidOperationException("Book index not found"); + } + + var results = index.Searcher.Search(q) + .Select(x => new BookDto(x.Values["title"], x.Values["author"], int.Parse(x.Values["publishedYear"]))) + .ToList(); + return Ok(results); + } + + private class BookDto + { + public BookDto(string title, string author, int publishedYear) + { + Title = title; + Author = author; + PublishedYear = publishedYear; + } + + public string Title { get; } + + public string Author { get; } + + public int PublishedYear { get; } + } +} +``` From 867d210ecc78c9f5c5797223281c3c2c0fa3f227 Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Tue, 11 Nov 2025 18:47:51 +0100 Subject: [PATCH 2/4] Linting --- 16/umbraco-cms/reference/searching/examine/indexing.md | 4 ++-- 17/umbraco-cms/reference/searching/examine/indexing.md | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/16/umbraco-cms/reference/searching/examine/indexing.md b/16/umbraco-cms/reference/searching/examine/indexing.md index df41673507f..9fa0571c344 100644 --- a/16/umbraco-cms/reference/searching/examine/indexing.md +++ b/16/umbraco-cms/reference/searching/examine/indexing.md @@ -496,7 +496,7 @@ The order of these registrations matters. It is important to register your index If you have a need, you can also use an Examine index for other data, that you aren't managing as Umbraco content. -As an illustrative example, we can define a collection of books. Here we just have a hardcoded collection. In the real-world, it's more likely these will come from a database. +As an illustrative example, we can define a collection of books. Here we have a hardcoded collection. In the real-world, it's more likely these will come from a database. ```csharp namespace Umbraco.Docs.Samples.Web.CustomIndexing; @@ -577,7 +577,7 @@ public class ConfigureBookIndexOptions : IConfigureNamedOptions Date: Wed, 12 Nov 2025 09:38:06 +0100 Subject: [PATCH 3/4] Incorporated suggestions --- 16/umbraco-cms/reference/searching/examine/indexing.md | 6 +++--- 17/umbraco-cms/reference/searching/examine/indexing.md | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/16/umbraco-cms/reference/searching/examine/indexing.md b/16/umbraco-cms/reference/searching/examine/indexing.md index 9fa0571c344..8b30720e63d 100644 --- a/16/umbraco-cms/reference/searching/examine/indexing.md +++ b/16/umbraco-cms/reference/searching/examine/indexing.md @@ -149,9 +149,9 @@ Remember to register `ConfigureMemberIndexOptions` in your composer. The following example will show how to create an index that will only include nodes based on the **Product**. {% hint style="info" %} -We 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. +It is recommended to 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](quick-start.md) to see some examples of how to search the ExternalIndex. +Take a look at the [Examine Quick Start](quick-start.md) guide to see some examples of how to search the ExternalIndex. {% endhint %} To create this index we need five things: @@ -577,7 +577,7 @@ public class ConfigureBookIndexOptions : IConfigureNamedOptions Examine Management > Indexes_ screen. +With this in place, the details of the index will be available under the **Settings** > **Examine Management** > **Indexes** screen. To verify indexing and querying, a controller can be used: From ef23b5801d30475c15f323df19506ed7e1b33f86 Mon Sep 17 00:00:00 2001 From: Esha Noronha <82437098+eshanrnh@users.noreply.github.com> Date: Wed, 12 Nov 2025 09:38:45 +0100 Subject: [PATCH 4/4] Incorporated rest of the suggestions --- 16/umbraco-cms/reference/searching/examine/indexing.md | 6 +++--- 17/umbraco-cms/reference/searching/examine/indexing.md | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/16/umbraco-cms/reference/searching/examine/indexing.md b/16/umbraco-cms/reference/searching/examine/indexing.md index 8b30720e63d..cb8b4fdd099 100644 --- a/16/umbraco-cms/reference/searching/examine/indexing.md +++ b/16/umbraco-cms/reference/searching/examine/indexing.md @@ -496,7 +496,7 @@ The order of these registrations matters. It is important to register your index If you have a need, you can also use an Examine index for other data, that you aren't managing as Umbraco content. -As an illustrative example, we can define a collection of books. Here we have a hardcoded collection. In the real-world, it's more likely these will come from a database. +As an illustrative example, consider a collection of books. This example uses a hardcoded collection. In a real-world scenario, the data would more likely come from a database. ```csharp namespace Umbraco.Docs.Samples.Web.CustomIndexing; @@ -525,7 +525,7 @@ public static class BookData } ``` -As with the previous example, we define an index. As this time we aren't indexing Umbraco data, we inherit directly from `LuceneIndex`: +As with the previous example, define an index. At this time, Umbraco data isn't being indexed; the implementation inherits directly from `LuceneIndex`: ```csharp using Examine.Lucene; @@ -598,7 +598,7 @@ public class ExamineComposer : IComposer } ``` -With this in place, the details of the index will be available under the _Settings > Examine Management > Indexes_ screen. +With this in place, the details of the index will be available under the **Settings** > **Examine Management** > **Indexes** screen. To verify indexing and querying, a controller can be used: diff --git a/17/umbraco-cms/reference/searching/examine/indexing.md b/17/umbraco-cms/reference/searching/examine/indexing.md index 639747d2758..088c3013f54 100644 --- a/17/umbraco-cms/reference/searching/examine/indexing.md +++ b/17/umbraco-cms/reference/searching/examine/indexing.md @@ -496,7 +496,7 @@ The order of these registrations matters. It is important to register your index If you have a need, you can also use an Examine index for other data, that you aren't managing as Umbraco content. -As an illustrative example, we can define a collection of books. Here we have a hardcoded collection. In the real-world, it's more likely these will come from a database. +As an illustrative example, consider a collection of books. This example uses a hardcoded collection. In a real-world scenario, the data would more likely come from a database. ```csharp namespace Umbraco.Docs.Samples.Web.CustomIndexing; @@ -525,7 +525,7 @@ public static class BookData } ``` -As with the previous example, we define an index. As this time we aren't indexing Umbraco data, we inherit directly from `LuceneIndex`: +As with the previous example, define an index. At this time, Umbraco data isn't being indexed; the implementation inherits directly from `LuceneIndex`: ```csharp using Examine.Lucene;