orchardcore-search-indexing
Skill for configuring search and indexing in Orchard Core. Covers Lucene indexing, Elasticsearch integration, search settings, index definitions, and search queries.
Files2
View on GitHubSKILL.mdsrc/CrestApps.AgentSkills/orchardcore/orchardcore-search-indexing/SKILL.md
GitHubOrchard Core Search & Indexing - Prompt Templates
Configure Search and Indexing
You are an Orchard Core expert. Generate search and indexing configurations for Orchard Core.
Guidelines
- Orchard Core uses a unified
IndexProfilefor Lucene, Elasticsearch, and Azure AI Search indexes. - Enable the required provider feature such as
OrchardCore.Lucene,OrchardCore.Elasticsearch, orOrchardCore.AzureAI. - Lucene indexes are stored on the local file system.
- Elasticsearch requires an external Elasticsearch cluster.
- Create indexes with the provider-agnostic
CreateOrUpdateIndexProfilestep. - Use queries to search indexed content programmatically or via Liquid.
- Content indexing is triggered automatically when content is published or updated.
- Rebuild indexes after changing index definitions.
- Use
IDocumentIndexHandlerfor document-wide indexing and read the source record fromBuildDocumentIndexContext.Record. DocumentIndexsupports text, numeric, date, boolean, geo-point,Complex, andVectorentries. Use the matchingSetoverload for the value type.- Use the centralized
IndexingPermissions.QuerySearchIndexpermission (OrchardCore.Indexing.Core). A profile-specificQueryIndex_{name}permission is created byIndexingPermissions.CreateDynamicPermission. - Provider connection settings live under
OrchardCore:Search:ElasticsearchandOrchardCore:Search:AzureAISearchsince Orchard Core 4.0 (theOrchardCore_*names are deprecated). ISearchServiceimplementations are keyed by provider name; resolve one withGetKeyedService<ISearchService>(indexProfile.ProviderName).
Enabling Search Features
{
"steps": [
{
"name": "Feature",
"enable": [
"OrchardCore.Search",
"OrchardCore.Lucene",
"OrchardCore.Indexing"
],
"disable": []
}
]
}
Lucene Index Profile via Recipe
{
"steps": [
{
"name": "CreateOrUpdateIndexProfile",
"indexes": [
{
"Name": "Search",
"IndexName": "search",
"ProviderName": "Lucene",
"Type": "Content",
"Properties": {
"ContentIndexMetadata": {
"IndexLatest": false,
"IndexedContentTypes": [
"Article",
"BlogPost"
],
"Culture": "any"
},
"LuceneIndexMetadata": {
"AnalyzerName": "standardanalyzer",
"StoreSourceData": false
}
}
}
]
}
]
}
Elasticsearch Configuration
Configure in appsettings.json:
{
"OrchardCore": {
"Search": {
"Elasticsearch": {
"Url": "https://localhost",
"Ports": [9200],
"ConnectionType": "SingleNodeConnectionPool"
}
}
}
}
See orchardcore-elasticsearch and orchardcore-azure-ai-search for the full provider settings.
Elasticsearch Index Profile via Recipe
{
"steps": [
{
"name": "CreateOrUpdateIndexProfile",
"indexes": [
{
"Name": "Search",
"IndexName": "search",
"ProviderName": "Elasticsearch",
"Type": "Content",
"Properties": {
"ContentIndexMetadata": {
"IndexLatest": false,
"IndexedContentTypes": [
"Article",
"BlogPost"
],
"Culture": "any"
},
"ElasticsearchIndexMetadata": {
"AnalyzerName": "standard",
"StoreSourceData": false
}
}
}
]
}
]
}
Lucene Queries via Recipe
{
"steps": [
{
"name": "Queries",
"Queries": [
{
"Source": "Lucene",
"Name": "RecentBlogPosts",
"Index": "Search",
"Template": "{\"query\":{\"bool\":{\"filter\":[{\"term\":{\"Content.ContentItem.ContentType\":\"BlogPost\"}}]}},\"sort\":{\"Content.ContentItem.CreatedUtc\":{\"order\":\"desc\"}},\"size\":10}",
"ReturnContentItems": true,
"Schema": "[]"
}
]
}
]
}
Using Search in Liquid
{% assign results = Queries.RecentBlogPosts | query %}
{% for item in results %}
<article>
<h2>{{ item.DisplayText }}</h2>
<p>{{ item.Content.BlogPost.Subtitle.Text }}</p>
</article>
{% endfor %}
Programmatic Search Queries
using System.Security.Claims;
using Microsoft.AspNetCore.Authorization;
using Microsoft.Extensions.DependencyInjection;
using OrchardCore.Indexing;
using OrchardCore.Indexing.Core;
using OrchardCore.Search.Abstractions;
public sealed class SiteSearchService
{
private readonly IIndexProfileManager _indexProfileManager;
private readonly IServiceProvider _serviceProvider;
private readonly IAuthorizationService _authorizationService;
public SiteSearchService(
IIndexProfileManager indexProfileManager,
IServiceProvider serviceProvider,
IAuthorizationService authorizationService)
{
_indexProfileManager = indexProfileManager;
_serviceProvider = serviceProvider;
_authorizationService = authorizationService;
}
public async Task<IList<string>> SearchAsync(ClaimsPrincipal user, string query)
{
var indexProfile = await _indexProfileManager.FindByNameAsync("Search")
?? throw new InvalidOperationException("The Search index profile does not exist.");
// Resolves the profile-specific QueryIndex_{name} permission.
if (!await _authorizationService.AuthorizeAsync(user, IndexingPermissions.QuerySearchIndex, indexProfile))
{
return [];
}
// Search services are registered with AddSearchService<T>(providerName) as keyed services.
var searchService = _serviceProvider.GetKeyedService<ISearchService>(indexProfile.ProviderName)
?? throw new InvalidOperationException($"No search service for '{indexProfile.ProviderName}'.");
var result = await searchService.SearchAsync(indexProfile, query, 0, 10);
return result.ContentItemIds;
}
}
The front-end search page is GET /search/{index?}?terms=... (GET-only since Orchard Core 4.0); index is the index profile name, otherwise the default index of the search settings is used.
Custom Document Indexing
Implement IDocumentIndexHandler when indexing requires the complete source
record. BuildDocumentIndexContext.Record is an object, so check its type
before adding entries. The Complex and Vector overloads preserve structured
values and vector dimensions for providers that support them.
A handler only shapes records that already reach the indexing pipeline. To
index records that are not content items (task log entries, a
NamedIndexingService subclass, a background task, and a profile handler for
sync and reset), follow orchardcore-indexing-sources.
using System.Collections.Generic;
using OrchardCore.Indexing;
public sealed class ProductDocumentIndexHandler : IDocumentIndexHandler
{
public Task BuildIndexAsync(BuildDocumentIndexContext context)
{
if (context.Record is not ProductRecord record)
{
return Task.CompletedTask;
}
context.DocumentIndex.Set(
"Product.Name",
record.Name,
DocumentIndexOptions.Store);
context.DocumentIndex.Set(
"Product.Attributes",
record.Attributes,
DocumentIndexOptions.Store);
context.DocumentIndex.Set(
"Product.Embedding",
record.Embedding,
record.Embedding.Length,
DocumentIndexOptions.None);
return Task.CompletedTask;
}
}
public sealed class ProductRecord
{
public string Name { get; init; } = string.Empty;
public Dictionary<string, object> Attributes { get; init; } = [];
public float[] Embedding { get; init; } = [];
}
Register the handler as a scoped service:
services.AddScoped<IDocumentIndexHandler, ProductDocumentIndexHandler>();
Use ContentIndexingConstants from OrchardCore.Contents.Indexing for
built-in content index keys such as ContentTypeKey, ContentItemIdKey, and
FullTextKey.
Indexing Permissions
IndexingPermissions.QuerySearchIndex is the centralized permission for
querying an index. The indexing authorization handler resolves the
profile-specific dynamic permission created by
IndexingPermissions.CreateDynamicPermission(indexProfile), whose name is
QueryIndex_{indexProfile.Name} and which is implied by
IndexingPermissions.ManageIndexes and IndexingPermissions.QuerySearchIndex.
| Permission | Member |
|---|---|
| Query any index | OrchardCore.Indexing.Core.IndexingPermissions.QuerySearchIndex |
| Manage indexes | OrchardCore.Indexing.Core.IndexingPermissions.ManageIndexes |
| Query one index | IndexingPermissions.CreateDynamicPermission(indexProfile) |
| Manage search settings | OrchardCore.Search.SearchPermissions.ManageSearchSettings |
Upgrading from 3.x
OrchardCore.Search.Permissions.QuerySearchIndexwas removed; useOrchardCore.Indexing.Core.IndexingPermissions.QuerySearchIndex.OrchardCore.Search.Permissions.ManageSearchSettingswas removed; useOrchardCore.Search.SearchPermissions.ManageSearchSettings.ElasticsearchIndexPermissionHelper.GetElasticIndexPermission(string)was removed; useIndexingPermissions.CreateDynamicPermission(IndexProfile).- Rename
OrchardCore_ElasticsearchtoOrchardCore:Search:ElasticsearchandOrchardCore_AzureAISearchtoOrchardCore:Search:AzureAISearch. - The search action only answers
GET; forms posting to/searchmust usemethod="get"(the built-inSearch Formalready does).
Custom Content Part Indexing
using OrchardCore.Indexing;
public sealed class MyPartIndexHandler : ContentPartIndexHandler<MyPart>
{
public override Task BuildIndexAsync(
MyPart part,
BuildPartIndexContext context)
{
var options = DocumentIndexOptions.Store;
context.DocumentIndex.Set(
$"{nameof(MyPart)}.{nameof(MyPart.MyField)}",
part.MyField,
options);
return Task.CompletedTask;
}
}
Registering Index Handler
public sealed class Startup : StartupBase
{
public override void ConfigureServices(IServiceCollection services)
{
services.AddScoped<IContentPartIndexHandler, MyPartIndexHandler>();
}
}
Search Settings via Recipe
{
"steps": [
{
"name": "Settings",
"SearchSettings": {
"DefaultIndexProfileName": "Search",
"Placeholder": "Search...",
"PageTitle": "Search"
}
}
]
}