Orchard Core Search Providers
Implement a New Search Provider
You are an Orchard Core expert. Generate code and configuration for implementing a new search provider module such as OpenSearch by following the current OrchardCore.Elasticsearch pattern.
Guidelines
- Use the current provider pattern based on
OrchardCore.Elasticsearch, not legacy feature aliases. - Create a canonical feature ID such as
OrchardCore.OpenSearch, notOrchardCore.Search.OpenSearch. - Split provider concerns into clear registrations:
- core provider services and client factory
- index profile UI and handlers
- content indexing source registration under
OrchardCore.Contents OrchardCore.Searchintegration throughAddSearchService<TService>()
- Register provider services in
Startupand keep feature-specific registrations in separateStartupBaseclasses decorated with[RequireFeatures(...)]. - Use
IndexProfileHandlerBaseto initialize and update provider-specific metadata and mappings. - Register a
DisplayDriver<IndexProfile>only when the provider exposes editable provider-specific metadata. - Add an
AdminControlleronly when the provider needs extra actions such as index info, run query, or custom diagnostics. - Do not add provider-specific deployment steps just to create, reset, or rebuild indexes. Orchard Core already provides
CreateOrUpdateIndexProfile,ResetIndex, andRebuildIndexfor any provider. - Integrate with
OrchardCore.Searchby adding a scopedISearchServiceand keyed registration throughservices.AddSearchService<TService>(ProviderName). - If the provider exposes query definitions, register the query source and query handler in the main startup.
- Keep examples focused on the latest Orchard Core implementation and omit backward-compatibility guidance.
- All C# classes must use the
sealedmodifier. - All recipe JSON must be wrapped in the root
{ "steps": [...] }format.
Architecture Checklist
For a provider like OpenSearch, the usual pieces are:
Manifest.csfeature definitions- provider constants such as
OpenSearchConstants.ProviderName - connection options and client factory
- provider service extensions such as
AddOpenSearchServices() DisplayDriver<IndexProfile>for provider metadataIndexProfileHandlerBaseimplementation for mappings and query defaults- optional query services and query handlers
- optional
ContentsStartupthat registersAddOpenSearchIndexingSource(...) - optional
SearchStartupthat integrates withOrchardCore.Search
Feature and Dependency Pattern
Follow the current Elasticsearch feature structure, but without the obsolete compatibility feature:
using OrchardCore.Modules.Manifest;
[assembly: Module(
Name = "OpenSearch",
Author = ManifestConstants.OrchardCoreTeam,
Website = ManifestConstants.OrchardCoreWebsite,
Version = ManifestConstants.OrchardCoreVersion
)]
[assembly: Feature(
Id = "OrchardCore.OpenSearch",
Name = "OpenSearch",
Description = "Creates OpenSearch indexes to support search scenarios.",
Dependencies =
[
"OrchardCore.Queries.Core",
"OrchardCore.Indexing",
"OrchardCore.ContentTypes",
],
Category = "Search"
)]
Add separate startup classes for optional integrations:
[RequireFeatures("OrchardCore.Contents")]for content indexing registration[RequireFeatures("OrchardCore.Search")]forISearchService- other features only when the provider really needs them
Provider Service Extensions
Create provider-specific service extensions just like Elasticsearch does:
using Microsoft.Extensions.DependencyInjection;
using OrchardCore.Indexing.Core;
using OrchardCore.OpenSearch.Core.Services;
using OrchardCore.Queries;
namespace OrchardCore.OpenSearch;
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddOpenSearchServices(this IServiceCollection services)
{
services.AddScoped<OpenSearchQueryService>();
services.AddQuerySource<OpenSearchQuerySource>(OpenSearchQuerySource.SourceName);
return services;
}
public static IServiceCollection AddOpenSearchIndexingSource(
this IServiceCollection services,
string implementationType,
Action<IndexingOptionsEntry> action = null)
{
ArgumentException.ThrowIfNullOrEmpty(implementationType);
services.AddIndexingSource<
OpenSearchIndexManager,
OpenSearchDocumentIndexManager,
OpenSearchIndexNameProvider>(
OpenSearchConstants.ProviderName,
implementationType,
action);
return services;
}
}
Main Provider Startup
Register connection options, client factory, provider services, query support, permissions, navigation, and profile UI in the main startup:
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using OrchardCore.Data.Migration;
using OrchardCore.DisplayManagement.Handlers;
using OrchardCore.Environment.Shell.Configuration;
using OrchardCore.Indexing.Models;
using OrchardCore.Modules;
using OrchardCore.Navigation;
using OrchardCore.OpenSearch.Core.Handlers;
using OrchardCore.OpenSearch.Core.Models;
using OrchardCore.OpenSearch.Core.Services;
using OrchardCore.OpenSearch.Drivers;
using OrchardCore.OpenSearch.Services;
using OrchardCore.Queries;
using OrchardCore.Queries.Core;
using OrchardCore.Security.Permissions;
namespace OrchardCore.OpenSearch;
public sealed class Startup : StartupBase
{
private readonly IShellConfiguration _shellConfiguration;
public Startup(IShellConfiguration shellConfiguration)
{
_shellConfiguration = shellConfiguration;
}
public override void ConfigureServices(IServiceCollection services)
{
services.AddTransient<IConfigureOptions<OpenSearchConnectionOptions>, OpenSearchConnectionOptionsConfigurations>();
services.AddTransient<IOpenSearchClientFactory, OpenSearchClientFactory>();
services.AddSingleton(sp =>
{
var factory = sp.GetRequiredService<IOpenSearchClientFactory>();
var options = sp.GetRequiredService<IOptions<OpenSearchConnectionOptions>>().Value;
return factory.Create(options);
});
services.Configure<OpenSearchOptions>(options =>
{
var configuration = _shellConfiguration.GetSection(OpenSearchConnectionOptionsConfigurations.ConfigSectionName);
options.AddIndexPrefix(configuration);
options.AddAnalyzers(configuration);
options.AddTokenFilters(configuration);
});
services.AddOpenSearchServices();
services.AddPermissionProvider<PermissionProvider>();
services.AddNavigationProvider<AdminMenu>();
services.AddDisplayDriver<Query, OpenSearchQueryDisplayDriver>();
services.AddScoped<IQueryHandler, OpenSearchQueryHandler>();
services.AddDisplayDriver<IndexProfile, OpenSearchIndexProfileDisplayDriver>();
services.AddIndexProfileHandler<OpenSearchIndexProfileHandler>();
services.AddDataMigration<OpenSearchMigrations>();
}
}
OrchardCore.Search Integration
Register the search service only when the OrchardCore.Search feature is enabled:
using Microsoft.Extensions.DependencyInjection;
using OrchardCore.Modules;
using OrchardCore.Search;
namespace OrchardCore.OpenSearch;
[RequireFeatures("OrchardCore.Search")]
public sealed class SearchStartup : StartupBase
{
public override void ConfigureServices(IServiceCollection services)
{
services.AddSearchService<OpenSearchService>(OpenSearchConstants.ProviderName);
}
}
This is the current Orchard Core pattern used by Elasticsearch, Lucene, and Azure AI Search.
Content Indexing Registration
Add a separate startup for content indexing support:
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Localization;
using OrchardCore.Data.Migration;
using OrchardCore.Indexing.Core;
using OrchardCore.Modules;
using OrchardCore.OpenSearch.Core.Handlers;
namespace OrchardCore.OpenSearch;
[RequireFeatures("OrchardCore.Contents")]
public sealed class ContentsStartup : StartupBase
{
internal readonly IStringLocalizer S;
public ContentsStartup(IStringLocalizer<ContentsStartup> stringLocalizer)
{
S = stringLocalizer;
}
public override void ConfigureServices(IServiceCollection services)
{
services.AddDataMigration<IndexingMigrations>();
services
.AddIndexProfileHandler<OpenSearchContentIndexProfileHandler>()
.AddOpenSearchIndexingSource(IndexingConstants.ContentsIndexSource, o =>
{
o.DisplayName = S["Content in OpenSearch"];
o.Description = S["Create an OpenSearch index based on site contents."];
});
}
}
Index Profile Handler Pattern
Provider-specific mappings belong in an IndexProfileHandlerBase implementation:
using OpenSearch.Client;
using OrchardCore.Entities;
using OrchardCore.Indexing.Core.Handlers;
using OrchardCore.Indexing.Models;
using OrchardCore.OpenSearch.Core.Models;
namespace OrchardCore.OpenSearch.Core.Handlers;
public sealed class OpenSearchIndexProfileHandler : IndexProfileHandlerBase
{
public override Task InitializingAsync(InitializingContext<IndexProfile> context)
=> ApplyDefaultsAsync(context.Model);
public override Task CreatingAsync(CreatingContext<IndexProfile> context)
=> ApplyDefaultsAsync(context.Model);
public override Task UpdatingAsync(UpdatingContext<IndexProfile> context)
=> ApplyDefaultsAsync(context.Model);
private static Task ApplyDefaultsAsync(IndexProfile indexProfile)
{
if (!string.Equals(indexProfile.ProviderName, OpenSearchConstants.ProviderName, StringComparison.OrdinalIgnoreCase))
{
return Task.CompletedTask;
}
if (!indexProfile.TryGet<OpenSearchIndexMetadata>(out var metadata))
{
metadata = new OpenSearchIndexMetadata();
}
metadata.IndexMappings ??= new OpenSearchIndexMap();
metadata.IndexMappings.Mapping ??= new TypeMapping();
metadata.IndexMappings.Mapping.Properties ??= [];
metadata.IndexMappings.KeyFieldName = "ContentItemId";
indexProfile.Put(metadata);
if (!indexProfile.TryGet<OpenSearchDefaultQueryMetadata>(out var queryMetadata))
{
queryMetadata = new OpenSearchDefaultQueryMetadata();
}
queryMetadata.DefaultSearchFields = ["Content.ContentItem.FullText"];
indexProfile.Put(queryMetadata);
return Task.CompletedTask;
}
}
AdminController Guidance
Do not add an AdminController by default.
Add one only when the provider needs provider-specific actions such as:
- viewing provider index info
- testing query DSL requests
- running diagnostics or custom actions that do not belong in the generic index profile UI
If the provider only needs normal index-profile editing, lifecycle operations, and search registration, the display driver and handler pattern is enough.
Deployment Guidance
Do not add provider-specific deployment steps just to manage indexes.
Use the provider-agnostic steps that already exist:
CreateOrUpdateIndexProfileResetIndexRebuildIndex
Index Profile Recipe Example
{
"steps": [
{
"name": "CreateOrUpdateIndexProfile",
"indexes": [
{
"Name": "OpenSearchContent",
"IndexName": "opensearch-content",
"ProviderName": "OpenSearch",
"Type": "Content",
"Properties": {
"ContentIndexMetadata": {
"IndexLatest": false,
"IndexedContentTypes": ["Article", "BlogPost"],
"Culture": "any"
},
"OpenSearchIndexMetadata": {
"AnalyzerName": "standard"
},
"OpenSearchDefaultQueryMetadata": {
"DefaultSearchFields": [
"Content.ContentItem.FullText"
]
}
}
}
]
}
]
}
Security and Reliability Notes
- Keep provider credentials in configuration, not in recipes or index-profile properties.
- Register provider services with the same lifetimes Orchard Core uses for the current providers.
- Use feature-gated startups instead of runtime
ifblocks where possible. - Prefer provider-specific wrapper methods like
AddOpenSearchIndexingSource()over scattered direct calls toAddIndexingSource(...).