Skip to main content
net10.0

Umbraco Forms Search provides record indexing and querying via Umbraco.Cms.Search abstractions.

Published
Jul 16, 2026
3mo ago
Total Downloads
363
363

Install

dotnet add package Umbraco.Forms.Search --version 17.0.0--beta.1.preview.17.gb0d383b
ProductVersions
.NET
net10.0

README

Umbraco.Forms.Search

Provides record indexing and querying for Umbraco Forms via the Umbraco.Cms.Search abstractions.

Compatible with Umbraco CMS and Forms 17+.

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:

  1. Registers the records index alias with Umbraco.Cms.Search via IndexOptions.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.
  2. Registers a physical Examine index for the alias via AddExamineLuceneIndex<LuceneIndex, ConfigurationEnabledDirectoryFactory>. The Examine provider does not create one automatically for a registered alias.
  3. 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).
  4. Overrides IRecordReaderService with the Umbraco.Cms.Search-backed implementation.
  5. Adds IFormRecordIndexingService plus 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 17 the backoffice "Entries" list does not use it β€” that goes through the database-backed IFormRecordSearcher. Overriding IRecordReaderService changes 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 the Form once and calls IRecordStorage.GetRecords(uniqueIds, form).
  • Form is unknown (e.g. GetRecordsFromPage): a lightweight includeFields: false load discovers which forms the IDs belong to, then issues one full GetRecords call 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/_b shadow 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

Adoption Assessment

A verified publisher signal is available. Assess fit from the package's supported frameworks, license and dependency graph rather than popularity alone.

This version is marked prerelease. Validate its API and upgrade path before production use.No SPDX license expression is published. Review the license text and your organisation's policy before redistribution.Compatibility metadata lists 1 target framework; confirm the exact target used by your application.

Related Guides & Workflows

Learn how to evaluate, integrate, or audit this package in your .NET project.