Umbraco.Forms.Search
18.0.0-beta.1
Prefix Reserved
dotnet add package Umbraco.Forms.Search --version 18.0.0-beta.1
NuGet\Install-Package Umbraco.Forms.Search -Version 18.0.0-beta.1
<PackageReference Include="Umbraco.Forms.Search" Version="18.0.0-beta.1" />
<PackageVersion Include="Umbraco.Forms.Search" Version="18.0.0-beta.1" />
<PackageReference Include="Umbraco.Forms.Search" />
paket add Umbraco.Forms.Search --version 18.0.0-beta.1
#r "nuget: Umbraco.Forms.Search, 18.0.0-beta.1"
#:package Umbraco.Forms.Search@18.0.0-beta.1
#addin nuget:?package=Umbraco.Forms.Search&version=18.0.0-beta.1&prerelease
#tool nuget:?package=Umbraco.Forms.Search&version=18.0.0-beta.1&prerelease
Umbraco.Forms.Search
Provides record indexing and querying for Umbraco Forms via the Umbraco.Cms.Search abstractions.
Compatible with Umbraco CMS and Forms 18+.
Installation
Umbraco.Forms.Search builds on Umbraco.Cms.Search.Core and, for the physical index and field registration, the official Umbraco.Cms.Search.Provider.Examine provider. Register the search core, the Examine provider, and then this package:
builder.CreateUmbracoBuilder()
.AddBackOffice()
.AddWebsite()
.AddDeliveryApi()
.AddSearchCore() // Umbraco.Cms.Search.Core
.AddExamineSearchProvider() // Umbraco.Cms.Search.Provider.Examine
.AddComposers()
.AddUmbracoForms()
.AddUmbracoFormsSearch() // this package
.Build();
AddUmbracoFormsSearch() does everything needed to put the Forms records index (UmbracoFormsRecordsIndex) onto Umbraco Search — it registers the index, the physical Examine index, and the field options (see How it works). You do not register the Examine index or FieldOptions yourself.
Disable the built-in Examine record indexing (required)
Umbraco Forms ships its own Examine-backed record indexing, which registers an index under the same name (UmbracoFormsRecordsIndex). Left enabled alongside this package, you get a duplicate-index error at startup (The indexer with name UmbracoFormsRecordsIndex already exists) and records indexed twice. Turn the built-in indexing off via configuration:
{
"Umbraco": {
"Forms": {
"Options": {
"DisableRecordIndexing": true
}
}
}
}
With this set, Forms skips registering its Examine record index, indexer, and IRecordReaderService, leaving this package as the sole owner of the records index. The database-backed record listing in the backoffice (IFormRecordSearcher) is unaffected — it is registered independently of record indexing.
How it works
The package routes the Forms records index through the Umbraco.Cms.Search abstractions, reusing the existing records index alias (Umbraco.Forms.Core.Constants.ExamineIndex.RecordIndexName, i.e. UmbracoFormsRecordsIndex).
Registration
UmbracoBuilderExtensions.AddUmbracoFormsSearch wires up:
- Registers the records index alias with
Umbraco.Cms.SearchviaIndexOptions.RegisterIndex<IIndexer, ISearcher>. This is a custom (self-managed) index, not a content index — so it is intentionally not listed in the Settings → Search dashboard and is not driven by the content-change pipeline. - Registers a physical Examine index for the alias via
AddExamineLuceneIndex<LuceneIndex, ConfigurationEnabledDirectoryFactory>. The Examine provider does not create one automatically for a registered alias. - Declares the record
FieldOptions(Examine provider) for the non-textual fields used in filtering, sorting and faceting —state,form,memberKey,umbracoPageId,created,updated. Without this, keyword/integer filters and date/state sorting misbehave (unregistered keyword fields are analysed rather than stored raw, and sortable keyword fields are not created). - Overrides
IRecordReaderServicewith theUmbraco.Cms.Search-backed implementation. - Adds
IFormRecordIndexingServiceplus the startup populator and the save/delete notification handlers that keep the index in sync.
Steps 2 and 3 use Examine-specific APIs, so the package depends on Umbraco.Cms.Search.Provider.Examine.
Note on
IRecordReaderService: this is a public read API. In Umbraco Forms 18 the backoffice "Entries" list does not use it — that goes through the database-backedIFormRecordSearcher. OverridingIRecordReaderServicechanges the read path for any code that calls it, but does not by itself change the backoffice entries screen.
Indexing (FormRecordIndexingService)
For every record save, each field is projected into an IndexField on a single document keyed by Record.UniqueId:
| Field | Value type | Source |
|---|---|---|
blob |
TextsR1 |
All record field values concatenated, for relevance-ranked full-text search |
state |
Keywords |
FormState enum name (Submitted, Approved, …) |
ip |
Keywords |
Record.IP |
uniqueId |
Keywords |
Record.UniqueId |
created |
DateTimeOffsets |
Record.Created (coerced to UTC) |
updated |
DateTimeOffsets |
Record.Updated (coerced to UTC) |
form |
Keywords |
Record.Form (form id) |
memberKey |
Keywords |
Record.MemberKey |
currentPage |
Keywords |
Record.CurrentPage |
umbracoPageId |
Integers |
Record.UmbracoPageId |
recordFields |
Texts |
Record.RecordFields serialized as JSON |
Documents are single-variation (Variation(null, null)) and carry no protection.
Querying (RecordReaderService)
Implements every IRecordReaderService overload — approved/all records scoped by page, form, or member, including the RecordSortOptions overloads — by composing KeywordFilter / IntegerExactFilter filters and a Sorter (mapping RecordSortField to a DateTimeOffsetSorter for Created/Updated or a KeywordSorter for State; default is Created descending), paging with skip / take.
Because the index stores only record IDs, the service rehydrates full Record instances:
- Form is known (e.g.
GetRecordsFromForm): resolves theFormonce and callsIRecordStorage.GetRecords(uniqueIds, form). - Form is unknown (e.g.
GetRecordsFromPage): a lightweightincludeFields: falseload discovers which forms the IDs belong to, then issues one fullGetRecordscall per form so each record is loaded against its own schema.
The loaded records are re-projected onto the order the searcher returned, so the requested sort is preserved.
Startup population (FormRecordIndexPopulationHandler)
On UmbracoApplicationStartedNotification the handler checks IIndexer.GetMetadataAsync. If the index is empty it indexes all existing records; otherwise it logs and returns (so it does not re-index on every boot). The work runs on a background thread so it does not block startup, with per-form error isolation.
Keeping the index in sync (RecordSearchNotificationHandler)
Handles RecordSavingNotification (upsert each saved record) and RecordDeletingNotification (delete by UniqueId). Delete targets the record id directly; no index-wide scan.
Known limitations
- Examine zero-downtime indexing: the package registers a single physical index under the plain alias name. If you enable
Umbraco:CMS:Search:Examine:ZeroDowntimeIndexing, the provider resolves_a/_bshadow index names and the records index will not be found. It is off by default. - No management UI: as a custom Umbraco Search index, the records index is not shown in Settings → Search and has no built-in "rebuild" action. The startup populator and the save/delete handlers are the sync mechanism.
Project layout
src/Umbraco.Forms.Search/
├── Extensions/
│ └── UmbracoBuilderExtensions.cs // AddUmbracoFormsSearch()
├── NotificationHandlers/
│ ├── FormRecordIndexPopulationHandler.cs // Background backfill on startup
│ └── RecordSearchNotificationHandler.cs // Save/delete sync
└── Services/
├── IFormRecordIndexingService.cs // Public indexing contract
├── FormRecordIndexingService.cs // Document projection → IIndexer
└── RecordReaderService.cs // IRecordReaderService over ISearcher
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- Umbraco.Cms.Search.Core (>= 18.0.0)
- Umbraco.Cms.Search.Provider.Examine (>= 18.0.0-beta.1)
- Umbraco.Forms.Core (>= 18.0.3)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 18.0.0-beta.1 | 58 | 7/16/2026 |
| 18.0.0-beta | 62 | 7/16/2026 |
| 17.0.0-beta.1 | 55 | 7/16/2026 |
| 17.0.0-beta | 57 | 7/16/2026 |
| 17.0.0--beta.1.preview.17... | 56 | 7/16/2026 |