From 55f241affec669dd9f0f1b45e994277feb5c2f08 Mon Sep 17 00:00:00 2001 From: Mike Alhayek Date: Tue, 5 Dec 2023 15:08:05 -0800 Subject: [PATCH 1/8] Add a way to provide a custom Elasticsearch query Fix #14842 --- .../Drivers/ElasticSettingsDisplayDriver.cs | 83 +++++++++++++++++-- .../Services/ElasticSearchService.cs | 51 ++++++++++-- .../ViewModels/ElasticSettingsViewModel.cs | 13 ++- .../Views/ElasticSettings.Edit.cshtml | 74 ++++++++++++----- .../OrchardCore.Abstractions/JsonHelpers.cs | 33 ++++++++ .../Models/ElasticSettings.cs | 7 +- .../Services/ElasticIndexManager.cs | 2 +- .../Services/ElasticQueryService.cs | 8 +- 8 files changed, 224 insertions(+), 47 deletions(-) create mode 100644 src/OrchardCore/OrchardCore.Abstractions/JsonHelpers.cs diff --git a/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Drivers/ElasticSettingsDisplayDriver.cs b/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Drivers/ElasticSettingsDisplayDriver.cs index 869bea6bdf0..11c82f19527 100644 --- a/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Drivers/ElasticSettingsDisplayDriver.cs +++ b/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Drivers/ElasticSettingsDisplayDriver.cs @@ -1,13 +1,20 @@ using System; +using System.IO; using System.Linq; +using System.Text; +using System.Text.Json; using System.Threading.Tasks; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Localization; +using Nest; using OrchardCore.DisplayManagement.Entities; using OrchardCore.DisplayManagement.Handlers; using OrchardCore.DisplayManagement.Views; +using OrchardCore.Mvc.ModelBinding; using OrchardCore.Search.Elasticsearch.Core.Models; using OrchardCore.Search.Elasticsearch.Core.Services; +using OrchardCore.Search.Elasticsearch.Services; using OrchardCore.Search.Elasticsearch.ViewModels; using OrchardCore.Settings; @@ -16,19 +23,33 @@ namespace OrchardCore.Search.Elasticsearch.Drivers public class ElasticSettingsDisplayDriver : SectionDisplayDriver { public const string GroupId = "elasticsearch"; + + private static readonly char[] _separator = [',', ' ']; + private static readonly JsonSerializerOptions _jsonSerializerOptions = new() + { + WriteIndented = true, + + }; private readonly ElasticIndexSettingsService _elasticIndexSettingsService; private readonly IHttpContextAccessor _httpContextAccessor; private readonly IAuthorizationService _authorizationService; + private readonly IElasticClient _elasticClient; + protected readonly IStringLocalizer S; public ElasticSettingsDisplayDriver( ElasticIndexSettingsService elasticIndexSettingsService, IHttpContextAccessor httpContextAccessor, - IAuthorizationService authorizationService + IAuthorizationService authorizationService, + IElasticClient elasticClient, + IStringLocalizer stringLocalizer + ) { _elasticIndexSettingsService = elasticIndexSettingsService; _httpContextAccessor = httpContextAccessor; _authorizationService = authorizationService; + _elasticClient = elasticClient; + S = stringLocalizer; } public override async Task EditAsync(ElasticSettings settings, BuildEditorContext context) @@ -41,12 +62,30 @@ public override async Task EditAsync(ElasticSettings settings, B } return Initialize("ElasticSettings_Edit", async model => + { + model.SearchIndex = settings.SearchIndex; + model.SearchFields = string.Join(", ", settings.DefaultSearchFields ?? []); + model.SearchIndexes = (await _elasticIndexSettingsService.GetSettingsAsync()).Select(x => x.IndexName); + model.DefaultQuery = settings.DefaultQuery; + +#pragma warning disable CS0618 // Type or member is obsolete + if (settings.SearchType == null && settings.AllowElasticQueryStringQueryInSearch) { - model.SearchIndex = settings.SearchIndex; - model.SearchFields = string.Join(", ", settings.DefaultSearchFields ?? Array.Empty()); - model.SearchIndexes = (await _elasticIndexSettingsService.GetSettingsAsync()).Select(x => x.IndexName); - model.AllowElasticQueryStringQueryInSearch = settings.AllowElasticQueryStringQueryInSearch; - }).Location("Content:2").OnGroup(GroupId); + model.SearchType = "query_string"; + } + else + { + model.SearchType = settings.SearchType; + } +#pragma warning restore CS0618 // Type or member is obsolete + + model.SearchTypes = [ + new(S["Multi-match query"], string.Empty), + new(S["String query"], ElasticsearchService.QueryStringSearchType), + new(S["Raw query"], ElasticsearchService.RawSearchType), + ]; + }).Location("Content:2") + .OnGroup(GroupId); } public override async Task UpdateAsync(ElasticSettings section, BuildEditorContext context) @@ -64,9 +103,37 @@ public override async Task UpdateAsync(ElasticSettings section, await context.Updater.TryUpdateModelAsync(model, Prefix); + section.DefaultQuery = null; section.SearchIndex = model.SearchIndex; - section.DefaultSearchFields = model.SearchFields?.Split(new[] { ',', ' ' }, StringSplitOptions.RemoveEmptyEntries); - section.AllowElasticQueryStringQueryInSearch = model.AllowElasticQueryStringQueryInSearch; + section.DefaultSearchFields = model.SearchFields?.Split(_separator, StringSplitOptions.RemoveEmptyEntries); + section.SearchType = model.SearchType ?? string.Empty; + + if (model.SearchType == ElasticsearchService.RawSearchType) + { + if (string.IsNullOrWhiteSpace(model.DefaultQuery)) + { + context.Updater.ModelState.AddModelError(Prefix, nameof(model.DefaultQuery), S["Please provide the default query."]); + } + else if (!JsonHelpers.TryParse(model.DefaultQuery, out var document)) + { + context.Updater.ModelState.AddModelError(Prefix, nameof(model.DefaultQuery), S["The provided query is not formatted correctly."]); + } + else + { + section.DefaultQuery = JsonSerializer.Serialize(document, _jsonSerializerOptions); + + try + { + using var stream = new MemoryStream(Encoding.UTF8.GetBytes(model.DefaultQuery)); + + var searchRequest = await _elasticClient.RequestResponseSerializer.DeserializeAsync(stream); + } + catch + { + context.Updater.ModelState.AddModelError(Prefix, nameof(model.DefaultQuery), S["Invalid query provided."]); + } + } + } } return await EditAsync(section, context); diff --git a/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Services/ElasticSearchService.cs b/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Services/ElasticSearchService.cs index 32a03be3032..cb3b6ac0e5d 100644 --- a/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Services/ElasticSearchService.cs +++ b/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Services/ElasticSearchService.cs @@ -1,8 +1,13 @@ using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using System.Text.Encodings.Web; using System.Threading.Tasks; +using Fluid.Values; using Microsoft.Extensions.Logging; using Nest; -using OrchardCore.Entities; +using OrchardCore.Liquid; using OrchardCore.Search.Abstractions; using OrchardCore.Search.Elasticsearch.Core.Models; using OrchardCore.Search.Elasticsearch.Core.Services; @@ -12,10 +17,16 @@ namespace OrchardCore.Search.Elasticsearch.Services; public class ElasticsearchService : ISearchService { + public const string RawSearchType = "raw"; + public const string QueryStringSearchType = "query_string"; + private readonly ISiteService _siteService; private readonly ElasticIndexManager _elasticIndexManager; private readonly ElasticIndexSettingsService _elasticIndexSettingsService; private readonly IElasticSearchQueryService _elasticsearchQueryService; + private readonly IElasticClient _elasticClient; + private readonly JavaScriptEncoder _javaScriptEncoder; + private readonly ILiquidTemplateManager _liquidTemplateManager; private readonly ILogger _logger; public ElasticsearchService( @@ -23,6 +34,9 @@ public ElasticsearchService( ElasticIndexManager elasticIndexManager, ElasticIndexSettingsService elasticIndexSettingsService, IElasticSearchQueryService elasticsearchQueryService, + IElasticClient elasticClient, + JavaScriptEncoder javaScriptEncoder, + ILiquidTemplateManager liquidTemplateManager, ILogger logger ) { @@ -30,6 +44,9 @@ ILogger logger _elasticIndexManager = elasticIndexManager; _elasticIndexSettingsService = elasticIndexSettingsService; _elasticsearchQueryService = elasticsearchQueryService; + _elasticClient = elasticClient; + _javaScriptEncoder = javaScriptEncoder; + _liquidTemplateManager = liquidTemplateManager; _logger = logger; } @@ -65,18 +82,27 @@ public async Task SearchAsync(string indexName, string term, int s { QueryContainer query = null; - if (searchSettings.AllowElasticQueryStringQueryInSearch) + if (searchSettings.SearchType == RawSearchType) { - query = new QueryStringQuery + var tokenizedContent = await _liquidTemplateManager.RenderStringAsync(searchSettings.DefaultQuery, _javaScriptEncoder, + new Dictionary() + { + ["term"] = new StringValue(term) + }); + + try { - Fields = searchSettings.DefaultSearchFields, - Analyzer = await _elasticIndexSettingsService.GetQueryAnalyzerAsync(index), - Query = term - }; + using var stream = new MemoryStream(Encoding.UTF8.GetBytes(tokenizedContent)); + + var searchRequest = await _elasticClient.RequestResponseSerializer.DeserializeAsync(stream); + + query = searchRequest.Query; + } + catch { } } - else + else if (searchSettings.SearchType == "query_string") { - query = new MultiMatchQuery + query = new QueryStringQuery { Fields = searchSettings.DefaultSearchFields, Analyzer = await _elasticIndexSettingsService.GetQueryAnalyzerAsync(index), @@ -84,6 +110,13 @@ public async Task SearchAsync(string indexName, string term, int s }; } + query ??= new MultiMatchQuery + { + Fields = searchSettings.DefaultSearchFields, + Analyzer = await _elasticIndexSettingsService.GetQueryAnalyzerAsync(index), + Query = term + }; + result.ContentItemIds = await _elasticsearchQueryService.ExecuteQueryAsync(index, query, null, start, pageSize); result.Success = true; } diff --git a/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/ViewModels/ElasticSettingsViewModel.cs b/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/ViewModels/ElasticSettingsViewModel.cs index 5e6383c8951..c06bc3f4ff1 100644 --- a/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/ViewModels/ElasticSettingsViewModel.cs +++ b/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/ViewModels/ElasticSettingsViewModel.cs @@ -1,13 +1,24 @@ using System.Collections.Generic; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.AspNetCore.Mvc.Rendering; namespace OrchardCore.Search.Elasticsearch.ViewModels { public class ElasticSettingsViewModel { public string Analyzer { get; set; } + public string SearchIndex { get; set; } + public IEnumerable SearchIndexes { get; set; } + public string SearchFields { get; set; } - public bool AllowElasticQueryStringQueryInSearch { get; set; } + + public string DefaultQuery { get; set; } + + public string SearchType { get; set; } + + [BindNever] + public IEnumerable SearchTypes { get; set; } } } diff --git a/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Views/ElasticSettings.Edit.cshtml b/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Views/ElasticSettings.Edit.cshtml index 7dc6c6c7974..9642b3a95a9 100644 --- a/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Views/ElasticSettings.Edit.cshtml +++ b/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Views/ElasticSettings.Edit.cshtml @@ -1,35 +1,63 @@ +@using OrchardCore.Search.Elasticsearch.Services + @model ElasticSettingsViewModel -@if (Model.SearchIndexes.Any()) -{ -
- - - - @T["The default index to use for the search page."] -
-} -else +@if (!Model.SearchIndexes.Any()) {
@T["You need to create at least an index to set as the Search index."]
+ + return; } -
+
+ + + + @T["The default index to use for the search page."] +
+ +
+ + + +
+ +
+ + + + @T["Create a custom Elasticsearch query to be utilized for each search request. Liquid is supported, so use {0} template as a substitute for the user-provided search term.", "{{ term }}"] +
+ +
@T["A comma separated list of fields to use for search pages. The default value is Content.ContentItem.FullText."]
-
-
- - - @T["Whether search queries should be allowed to use Elasticsearch \"query string query\" syntax."] @T["See documentation"] -
-
+ diff --git a/src/OrchardCore/OrchardCore.Abstractions/JsonHelpers.cs b/src/OrchardCore/OrchardCore.Abstractions/JsonHelpers.cs new file mode 100644 index 00000000000..371b1778ad3 --- /dev/null +++ b/src/OrchardCore/OrchardCore.Abstractions/JsonHelpers.cs @@ -0,0 +1,33 @@ +using System.Text.Json; + +namespace OrchardCore; + +public class JsonHelpers +{ + public static bool IsValid(string json, JsonDocumentOptions options = default) + { + try + { + JsonDocument.Parse(json, options); + + return true; + } + catch { } + + return false; + } + + public static bool TryParse(string json, out JsonDocument document, JsonDocumentOptions options = default) + { + try + { + document = JsonDocument.Parse(json, options); + + return true; + } + catch { } + + document = null; + return false; + } +} diff --git a/src/OrchardCore/OrchardCore.Search.Elasticsearch.Core/Models/ElasticSettings.cs b/src/OrchardCore/OrchardCore.Search.Elasticsearch.Core/Models/ElasticSettings.cs index acd5500dd1f..b942715f858 100644 --- a/src/OrchardCore/OrchardCore.Search.Elasticsearch.Core/Models/ElasticSettings.cs +++ b/src/OrchardCore/OrchardCore.Search.Elasticsearch.Core/Models/ElasticSettings.cs @@ -5,15 +5,20 @@ namespace OrchardCore.Search.Elasticsearch.Core.Models { public class ElasticSettings { - public static readonly string[] FullTextField = new string[] { IndexingConstants.FullTextKey }; + public static readonly string[] FullTextField = [IndexingConstants.FullTextKey]; [Obsolete("This property will be removed in future releases.")] public const string StandardAnalyzer = "standardanalyzer"; public string SearchIndex { get; set; } + public string DefaultQuery { get; set; } + public string[] DefaultSearchFields { get; set; } = FullTextField; + [Obsolete("Instead use SearchType property.")] public bool AllowElasticQueryStringQueryInSearch { get; set; } = false; + + public string SearchType { get; set; } } } diff --git a/src/OrchardCore/OrchardCore.Search.Elasticsearch.Core/Services/ElasticIndexManager.cs b/src/OrchardCore/OrchardCore.Search.Elasticsearch.Core/Services/ElasticIndexManager.cs index d14732d9295..881717f898d 100644 --- a/src/OrchardCore/OrchardCore.Search.Elasticsearch.Core/Services/ElasticIndexManager.cs +++ b/src/OrchardCore/OrchardCore.Search.Elasticsearch.Core/Services/ElasticIndexManager.cs @@ -102,7 +102,7 @@ public async Task CreateIndexAsync(ElasticIndexSettings elasticIndexSettin var indexSettingsDescriptor = new IndexSettingsDescriptor(); // The name "standardanalyzer" is a legacy used prior OC 1.6 release. It can be removed in future releases. - var analyzerName = elasticIndexSettings.AnalyzerName == "standardanalyzer" ? "standard" : elasticIndexSettings.AnalyzerName; + var analyzerName = (elasticIndexSettings.AnalyzerName == "standardanalyzer" ? null : elasticIndexSettings.AnalyzerName) ?? "standard"; if (_elasticsearchOptions.Analyzers.TryGetValue(analyzerName, out var analyzerProperties)) { diff --git a/src/OrchardCore/OrchardCore.Search.Elasticsearch.Core/Services/ElasticQueryService.cs b/src/OrchardCore/OrchardCore.Search.Elasticsearch.Core/Services/ElasticQueryService.cs index ea3817561a7..12243eee9e9 100644 --- a/src/OrchardCore/OrchardCore.Search.Elasticsearch.Core/Services/ElasticQueryService.cs +++ b/src/OrchardCore/OrchardCore.Search.Elasticsearch.Core/Services/ElasticQueryService.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.IO; -using System.Linq; using System.Text; using System.Threading.Tasks; using Microsoft.Extensions.Logging; @@ -38,7 +37,8 @@ public async Task SearchAsync(string indexName, string query) try { - var deserializedSearchRequest = _elasticClient.RequestResponseSerializer.Deserialize(new MemoryStream(Encoding.UTF8.GetBytes(query))); + using var stream = new MemoryStream(Encoding.UTF8.GetBytes(query)); + var deserializedSearchRequest = _elasticClient.RequestResponseSerializer.Deserialize(stream); var searchRequest = new SearchRequest(_indexPrefix + indexName) { @@ -47,7 +47,7 @@ public async Task SearchAsync(string indexName, string query) Size = deserializedSearchRequest.Size, Fields = deserializedSearchRequest.Fields, Sort = deserializedSearchRequest.Sort, - Source = deserializedSearchRequest.Source + Source = deserializedSearchRequest.Source, }; var searchResponse = await _elasticClient.SearchAsync>(searchRequest); @@ -71,7 +71,7 @@ public async Task SearchAsync(string indexName, string query) if (searchResponse.IsValid) { elasticTopDocs.Count = searchResponse.Total; - elasticTopDocs.TopDocs = searchResponse.Documents.ToList(); + elasticTopDocs.TopDocs = [.. searchResponse.Documents]; elasticTopDocs.Fields = hits; } else From 4aafe47cb3a1a21284b4c10ed319cc1cef6f4c10 Mon Sep 17 00:00:00 2001 From: Mike Alhayek Date: Tue, 5 Dec 2023 15:23:11 -0800 Subject: [PATCH 2/8] cleanup --- .../Drivers/ElasticSettingsDisplayDriver.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Drivers/ElasticSettingsDisplayDriver.cs b/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Drivers/ElasticSettingsDisplayDriver.cs index 11c82f19527..31305b70f36 100644 --- a/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Drivers/ElasticSettingsDisplayDriver.cs +++ b/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Drivers/ElasticSettingsDisplayDriver.cs @@ -80,9 +80,9 @@ public override async Task EditAsync(ElasticSettings settings, B #pragma warning restore CS0618 // Type or member is obsolete model.SearchTypes = [ - new(S["Multi-match query"], string.Empty), - new(S["String query"], ElasticsearchService.QueryStringSearchType), - new(S["Raw query"], ElasticsearchService.RawSearchType), + new(S["Multi-Match Query (Default)"], string.Empty), + new(S["Query String Query"], ElasticsearchService.QueryStringSearchType), + new(S["Raw Query"], ElasticsearchService.RawSearchType), ]; }).Location("Content:2") .OnGroup(GroupId); From 4cfd41532465f127feccfb4be026631a110bfd6b Mon Sep 17 00:00:00 2001 From: Mike Alhayek Date: Thu, 7 Dec 2023 08:59:41 -0800 Subject: [PATCH 3/8] Cleanup and update docs --- .../Drivers/ElasticSettingsDisplayDriver.cs | 34 ++++--------------- .../Services/ElasticSearchService.cs | 9 +++-- .../Views/ElasticSettings.Edit.cshtml | 4 +-- .../Recipes/blog.elastic.search.recipe.json | 5 +-- .../Models/ElasticSettings.cs | 26 +++++++++++--- .../reference/modules/Elasticsearch/README.md | 16 +++++---- 6 files changed, 47 insertions(+), 47 deletions(-) diff --git a/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Drivers/ElasticSettingsDisplayDriver.cs b/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Drivers/ElasticSettingsDisplayDriver.cs index 31305b70f36..7a776f83c6f 100644 --- a/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Drivers/ElasticSettingsDisplayDriver.cs +++ b/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Drivers/ElasticSettingsDisplayDriver.cs @@ -14,7 +14,6 @@ using OrchardCore.Mvc.ModelBinding; using OrchardCore.Search.Elasticsearch.Core.Models; using OrchardCore.Search.Elasticsearch.Core.Services; -using OrchardCore.Search.Elasticsearch.Services; using OrchardCore.Search.Elasticsearch.ViewModels; using OrchardCore.Settings; @@ -52,41 +51,22 @@ IStringLocalizer stringLocalizer S = stringLocalizer; } - public override async Task EditAsync(ElasticSettings settings, BuildEditorContext context) - { - var user = _httpContextAccessor.HttpContext?.User; - - if (!await _authorizationService.AuthorizeAsync(user, Permissions.ManageElasticIndexes)) - { - return null; - } - - return Initialize("ElasticSettings_Edit", async model => + public override IDisplayResult Edit(ElasticSettings settings) + => Initialize("ElasticSettings_Edit", async model => { model.SearchIndex = settings.SearchIndex; model.SearchFields = string.Join(", ", settings.DefaultSearchFields ?? []); model.SearchIndexes = (await _elasticIndexSettingsService.GetSettingsAsync()).Select(x => x.IndexName); model.DefaultQuery = settings.DefaultQuery; - -#pragma warning disable CS0618 // Type or member is obsolete - if (settings.SearchType == null && settings.AllowElasticQueryStringQueryInSearch) - { - model.SearchType = "query_string"; - } - else - { - model.SearchType = settings.SearchType; - } -#pragma warning restore CS0618 // Type or member is obsolete - + model.SearchType = settings.GetSearchType(); model.SearchTypes = [ new(S["Multi-Match Query (Default)"], string.Empty), - new(S["Query String Query"], ElasticsearchService.QueryStringSearchType), - new(S["Raw Query"], ElasticsearchService.RawSearchType), + new(S["Query String Query"], ElasticSettings.QueryStringSearchType), + new(S["Raw Query"], ElasticSettings.RawSearchType), ]; }).Location("Content:2") + .RenderWhen(() => _authorizationService.AuthorizeAsync(_httpContextAccessor.HttpContext.User, Permissions.ManageElasticIndexes)) .OnGroup(GroupId); - } public override async Task UpdateAsync(ElasticSettings section, BuildEditorContext context) { @@ -108,7 +88,7 @@ public override async Task UpdateAsync(ElasticSettings section, section.DefaultSearchFields = model.SearchFields?.Split(_separator, StringSplitOptions.RemoveEmptyEntries); section.SearchType = model.SearchType ?? string.Empty; - if (model.SearchType == ElasticsearchService.RawSearchType) + if (model.SearchType == ElasticSettings.RawSearchType) { if (string.IsNullOrWhiteSpace(model.DefaultQuery)) { diff --git a/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Services/ElasticSearchService.cs b/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Services/ElasticSearchService.cs index cb3b6ac0e5d..ea510093d72 100644 --- a/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Services/ElasticSearchService.cs +++ b/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Services/ElasticSearchService.cs @@ -17,9 +17,6 @@ namespace OrchardCore.Search.Elasticsearch.Services; public class ElasticsearchService : ISearchService { - public const string RawSearchType = "raw"; - public const string QueryStringSearchType = "query_string"; - private readonly ISiteService _siteService; private readonly ElasticIndexManager _elasticIndexManager; private readonly ElasticIndexSettingsService _elasticIndexSettingsService; @@ -78,11 +75,13 @@ public async Task SearchAsync(string indexName, string term, int s return result; } + var searchType = searchSettings.GetSearchType(); + try { QueryContainer query = null; - if (searchSettings.SearchType == RawSearchType) + if (searchType == ElasticSettings.RawSearchType) { var tokenizedContent = await _liquidTemplateManager.RenderStringAsync(searchSettings.DefaultQuery, _javaScriptEncoder, new Dictionary() @@ -100,7 +99,7 @@ public async Task SearchAsync(string indexName, string term, int s } catch { } } - else if (searchSettings.SearchType == "query_string") + else if (searchType == ElasticSettings.QueryStringSearchType) { query = new QueryStringQuery { diff --git a/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Views/ElasticSettings.Edit.cshtml b/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Views/ElasticSettings.Edit.cshtml index 9642b3a95a9..f8c73c5a473 100644 --- a/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Views/ElasticSettings.Edit.cshtml +++ b/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Views/ElasticSettings.Edit.cshtml @@ -1,4 +1,4 @@ -@using OrchardCore.Search.Elasticsearch.Services +@using OrchardCore.Search.Elasticsearch.Core.Models @model ElasticSettingsViewModel @@ -23,7 +23,7 @@
- +
diff --git a/src/OrchardCore.Themes/TheBlogTheme/Recipes/blog.elastic.search.recipe.json b/src/OrchardCore.Themes/TheBlogTheme/Recipes/blog.elastic.search.recipe.json index b80d5883180..b7957a0bed5 100644 --- a/src/OrchardCore.Themes/TheBlogTheme/Recipes/blog.elastic.search.recipe.json +++ b/src/OrchardCore.Themes/TheBlogTheme/Recipes/blog.elastic.search.recipe.json @@ -59,8 +59,9 @@ "DefaultSearchFields": [ "Content.ContentItem.FullText" ], - "AllowElasticQueryStringQueryInSearch": false, - "SyncWithLucene": true + "SearchType": "", + "DefaultQuery": null, + "SyncWithLucene": true } }, { diff --git a/src/OrchardCore/OrchardCore.Search.Elasticsearch.Core/Models/ElasticSettings.cs b/src/OrchardCore/OrchardCore.Search.Elasticsearch.Core/Models/ElasticSettings.cs index b942715f858..93ea26fa565 100644 --- a/src/OrchardCore/OrchardCore.Search.Elasticsearch.Core/Models/ElasticSettings.cs +++ b/src/OrchardCore/OrchardCore.Search.Elasticsearch.Core/Models/ElasticSettings.cs @@ -5,10 +5,11 @@ namespace OrchardCore.Search.Elasticsearch.Core.Models { public class ElasticSettings { - public static readonly string[] FullTextField = [IndexingConstants.FullTextKey]; + public const string RawSearchType = "raw"; - [Obsolete("This property will be removed in future releases.")] - public const string StandardAnalyzer = "standardanalyzer"; + public const string QueryStringSearchType = "query_string"; + + public static readonly string[] FullTextField = [IndexingConstants.FullTextKey]; public string SearchIndex { get; set; } @@ -16,9 +17,24 @@ public class ElasticSettings public string[] DefaultSearchFields { get; set; } = FullTextField; - [Obsolete("Instead use SearchType property.")] + public string SearchType { get; set; } + + [Obsolete("This property will be removed in future releases.")] + public const string StandardAnalyzer = "standardanalyzer"; + + [Obsolete($"This property will be removed in future releases. Instead use {nameof(SearchType)} property.")] public bool AllowElasticQueryStringQueryInSearch { get; set; } = false; - public string SearchType { get; set; } + public string GetSearchType() + { +#pragma warning disable CS0618 // Type or member is obsolete + if (SearchType == null && AllowElasticQueryStringQueryInSearch) + { + return QueryStringSearchType; + } +#pragma warning restore CS0618 // Type or member is obsolete + + return SearchType; + } } } diff --git a/src/docs/reference/modules/Elasticsearch/README.md b/src/docs/reference/modules/Elasticsearch/README.md index 3807f793ab7..7eb50debb28 100644 --- a/src/docs/reference/modules/Elasticsearch/README.md +++ b/src/docs/reference/modules/Elasticsearch/README.md @@ -90,7 +90,8 @@ Here is an example for setting default search settings: "DefaultSearchFields":[ "Content.ContentItem.FullText" ], - "AllowElasticQueryStringQueryInSearch":false, + "SearchType": "", // Use 'raw' for a custom query in DefaultQuery and 'query_string' for a Query String Query search. Leave it blank for the default, which is a Multi-Match Query search. + "DefaultQuery": null, "SyncWithLucene":true // Allows to sync content index settings. } } @@ -169,11 +170,14 @@ Here is an example for creating a Elasticsearch query from a Queries recipe step ```json { - "Source": "Elasticsearch", - "Name": "RecentBlogPosts", - "Index": "Search", - "Template": "...", // json encoded query template - "ReturnContentItems": true + "steps":[ + { + "Source": "Elasticsearch", + "Name": "RecentBlogPosts", + "Index": "Search", + "Template": "...", // json encoded query template + "ReturnContentItems": true + } } ``` From f8301c65d3d16405bbec98ebe0e186ba352713fc Mon Sep 17 00:00:00 2001 From: Mike Alhayek Date: Thu, 7 Dec 2023 09:11:10 -0800 Subject: [PATCH 4/8] updated the release notes --- src/docs/releases/1.8.0.md | 34 +++++++++++++++++++++++++++------- 1 file changed, 27 insertions(+), 7 deletions(-) diff --git a/src/docs/releases/1.8.0.md b/src/docs/releases/1.8.0.md index 86cd0443b10..e102f4d75e6 100644 --- a/src/docs/releases/1.8.0.md +++ b/src/docs/releases/1.8.0.md @@ -48,13 +48,13 @@ Additionally, if you've customized the `Error.cshtml` view, it should be renamed The following interfaces have been updated to incorporate new asynchronous methods, corresponding to each synchronous method. All synchronous methods have been deprecated and should no longer be utilized. -- IStereotypeService -- IStereotypesProvider -- IRouteableContentTypeProvider -- IRouteableContentTypeCoordinator -- IContentDefinitionService -- IContentDefinitionManager -- IContentDefinitionService +- `IStereotypeService` +- `IStereotypesProvider` +- `IRouteableContentTypeProvider` +- `IRouteableContentTypeCoordinator` +- `IContentDefinitionService` +- `IContentDefinitionManager` +- `IContentDefinitionService` ## Change Logs @@ -104,3 +104,23 @@ To add the `Navbar` shape into your own back-end theme, add the following code i @await DisplayAsync(await DisplayManager.BuildDisplayAsync(UpdateModelAccessor.ModelUpdater, "DetailAdmin")) ``` + +### Elasticsearch Module + +Introduced a new option in `ElasticSettings` that permits the definition of a custom query for the default search. For instance, the following is an example of a search query supporting the [fuzziness](https://www.elastic.co/guide/en/elasticsearch/reference/7.17/common-options.html#fuzziness) option: + +``` +{ + "query": { + "match": { + "Content.ContentItem.FullText": { + "query": "{{ term }}", + "fuzziness": "AUTO", + "analyzer": "whitespace" + } + } + } +} +``` + +![elastic-search-ex](https://github.com/OrchardCMS/OrchardCore/assets/24724371/15aae13e-0fc0-4df6-98be-352a441618c0) From 84dbe2de1d3f338a6092af52de0be9bbf67f73ae Mon Sep 17 00:00:00 2001 From: Mike Alhayek Date: Thu, 7 Dec 2023 09:27:13 -0800 Subject: [PATCH 5/8] cleanup --- .../Drivers/ElasticSettingsDisplayDriver.cs | 2 -- .../Services/ElasticSearchService.cs | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Drivers/ElasticSettingsDisplayDriver.cs b/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Drivers/ElasticSettingsDisplayDriver.cs index 7a776f83c6f..fd2f9793d7b 100644 --- a/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Drivers/ElasticSettingsDisplayDriver.cs +++ b/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Drivers/ElasticSettingsDisplayDriver.cs @@ -27,7 +27,6 @@ public class ElasticSettingsDisplayDriver : SectionDisplayDriver stringLocalizer - ) { _elasticIndexSettingsService = elasticIndexSettingsService; diff --git a/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Services/ElasticSearchService.cs b/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Services/ElasticSearchService.cs index ea510093d72..e6c690974c9 100644 --- a/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Services/ElasticSearchService.cs +++ b/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Services/ElasticSearchService.cs @@ -81,7 +81,7 @@ public async Task SearchAsync(string indexName, string term, int s { QueryContainer query = null; - if (searchType == ElasticSettings.RawSearchType) + if (searchType == ElasticSettings.RawSearchType && !string.IsNullOrWhiteSpace(searchSettings.DefaultQuery)) { var tokenizedContent = await _liquidTemplateManager.RenderStringAsync(searchSettings.DefaultQuery, _javaScriptEncoder, new Dictionary() From 8be2fa9d8459bf4b1a15bc2fa05b19f76cf0cd9b Mon Sep 17 00:00:00 2001 From: Mike Alhayek Date: Thu, 7 Dec 2023 11:20:27 -0800 Subject: [PATCH 6/8] Update label --- .../Views/ElasticSettings.Edit.cshtml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Views/ElasticSettings.Edit.cshtml b/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Views/ElasticSettings.Edit.cshtml index f8c73c5a473..6b71d290439 100644 --- a/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Views/ElasticSettings.Edit.cshtml +++ b/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Views/ElasticSettings.Edit.cshtml @@ -22,7 +22,7 @@
- +
From 2a8a43e8374fdb15cb23d6aab923995b351d80a5 Mon Sep 17 00:00:00 2001 From: Mike Alhayek Date: Thu, 7 Dec 2023 11:45:10 -0800 Subject: [PATCH 7/8] cleanup --- .../Drivers/ElasticSettingsDisplayDriver.cs | 64 ++++++++++--------- .../Services/ElasticSearchService.cs | 2 +- .../Views/ElasticSettings.Edit.cshtml | 2 +- .../Models/ElasticSettings.cs | 43 ++++++------- .../reference/modules/Elasticsearch/README.md | 2 +- 5 files changed, 57 insertions(+), 56 deletions(-) diff --git a/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Drivers/ElasticSettingsDisplayDriver.cs b/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Drivers/ElasticSettingsDisplayDriver.cs index fd2f9793d7b..8873758dbd0 100644 --- a/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Drivers/ElasticSettingsDisplayDriver.cs +++ b/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Drivers/ElasticSettingsDisplayDriver.cs @@ -27,6 +27,7 @@ public class ElasticSettingsDisplayDriver : SectionDisplayDriver stringLocalizer + ) { _elasticIndexSettingsService = elasticIndexSettingsService; @@ -60,7 +62,7 @@ public override IDisplayResult Edit(ElasticSettings settings) model.SearchTypes = [ new(S["Multi-Match Query (Default)"], string.Empty), new(S["Query String Query"], ElasticSettings.QueryStringSearchType), - new(S["Raw Query"], ElasticSettings.RawSearchType), + new(S["Custom Query"], ElasticSettings.CustomSearchType), ]; }).Location("Content:2") .RenderWhen(() => _authorizationService.AuthorizeAsync(_httpContextAccessor.HttpContext.User, Permissions.ManageElasticIndexes)) @@ -68,48 +70,48 @@ public override IDisplayResult Edit(ElasticSettings settings) public override async Task UpdateAsync(ElasticSettings section, BuildEditorContext context) { - var user = _httpContextAccessor.HttpContext?.User; - - if (!await _authorizationService.AuthorizeAsync(user, Permissions.ManageElasticIndexes)) + if (!string.Equals(GroupId, context.GroupId, StringComparison.OrdinalIgnoreCase)) { return null; } - if (context.GroupId.Equals(GroupId, StringComparison.OrdinalIgnoreCase)) + if (!await _authorizationService.AuthorizeAsync(_httpContextAccessor.HttpContext?.User, Permissions.ManageElasticIndexes)) { - var model = new ElasticSettingsViewModel(); + return null; + } - await context.Updater.TryUpdateModelAsync(model, Prefix); + var model = new ElasticSettingsViewModel(); - section.DefaultQuery = null; - section.SearchIndex = model.SearchIndex; - section.DefaultSearchFields = model.SearchFields?.Split(_separator, StringSplitOptions.RemoveEmptyEntries); - section.SearchType = model.SearchType ?? string.Empty; + await context.Updater.TryUpdateModelAsync(model, Prefix); - if (model.SearchType == ElasticSettings.RawSearchType) + section.DefaultQuery = null; + section.SearchIndex = model.SearchIndex; + section.DefaultSearchFields = model.SearchFields?.Split(_separator, StringSplitOptions.RemoveEmptyEntries); + section.SearchType = model.SearchType ?? string.Empty; + + if (model.SearchType == ElasticSettings.CustomSearchType) + { + if (string.IsNullOrWhiteSpace(model.DefaultQuery)) { - if (string.IsNullOrWhiteSpace(model.DefaultQuery)) - { - context.Updater.ModelState.AddModelError(Prefix, nameof(model.DefaultQuery), S["Please provide the default query."]); - } - else if (!JsonHelpers.TryParse(model.DefaultQuery, out var document)) + context.Updater.ModelState.AddModelError(Prefix, nameof(model.DefaultQuery), S["Please provide the default query."]); + } + else if (!JsonHelpers.TryParse(model.DefaultQuery, out var document)) + { + context.Updater.ModelState.AddModelError(Prefix, nameof(model.DefaultQuery), S["The provided query is not formatted correctly."]); + } + else + { + section.DefaultQuery = JsonSerializer.Serialize(document, _jsonSerializerOptions); + + try { - context.Updater.ModelState.AddModelError(Prefix, nameof(model.DefaultQuery), S["The provided query is not formatted correctly."]); + using var stream = new MemoryStream(Encoding.UTF8.GetBytes(model.DefaultQuery)); + + var searchRequest = await _elasticClient.RequestResponseSerializer.DeserializeAsync(stream); } - else + catch { - section.DefaultQuery = JsonSerializer.Serialize(document, _jsonSerializerOptions); - - try - { - using var stream = new MemoryStream(Encoding.UTF8.GetBytes(model.DefaultQuery)); - - var searchRequest = await _elasticClient.RequestResponseSerializer.DeserializeAsync(stream); - } - catch - { - context.Updater.ModelState.AddModelError(Prefix, nameof(model.DefaultQuery), S["Invalid query provided."]); - } + context.Updater.ModelState.AddModelError(Prefix, nameof(model.DefaultQuery), S["Invalid query provided."]); } } } diff --git a/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Services/ElasticSearchService.cs b/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Services/ElasticSearchService.cs index e6c690974c9..05055270957 100644 --- a/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Services/ElasticSearchService.cs +++ b/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Services/ElasticSearchService.cs @@ -81,7 +81,7 @@ public async Task SearchAsync(string indexName, string term, int s { QueryContainer query = null; - if (searchType == ElasticSettings.RawSearchType && !string.IsNullOrWhiteSpace(searchSettings.DefaultQuery)) + if (searchType == ElasticSettings.CustomSearchType) { var tokenizedContent = await _liquidTemplateManager.RenderStringAsync(searchSettings.DefaultQuery, _javaScriptEncoder, new Dictionary() diff --git a/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Views/ElasticSettings.Edit.cshtml b/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Views/ElasticSettings.Edit.cshtml index 6b71d290439..85e1e6801bf 100644 --- a/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Views/ElasticSettings.Edit.cshtml +++ b/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Views/ElasticSettings.Edit.cshtml @@ -23,7 +23,7 @@
- +
diff --git a/src/OrchardCore/OrchardCore.Search.Elasticsearch.Core/Models/ElasticSettings.cs b/src/OrchardCore/OrchardCore.Search.Elasticsearch.Core/Models/ElasticSettings.cs index 93ea26fa565..7fce5c796a0 100644 --- a/src/OrchardCore/OrchardCore.Search.Elasticsearch.Core/Models/ElasticSettings.cs +++ b/src/OrchardCore/OrchardCore.Search.Elasticsearch.Core/Models/ElasticSettings.cs @@ -1,40 +1,39 @@ using System; using OrchardCore.Contents.Indexing; -namespace OrchardCore.Search.Elasticsearch.Core.Models +namespace OrchardCore.Search.Elasticsearch.Core.Models; + +public class ElasticSettings { - public class ElasticSettings - { - public const string RawSearchType = "raw"; + public const string CustomSearchType = "custom"; - public const string QueryStringSearchType = "query_string"; + public const string QueryStringSearchType = "query_string"; - public static readonly string[] FullTextField = [IndexingConstants.FullTextKey]; + public static readonly string[] FullTextField = [IndexingConstants.FullTextKey]; - public string SearchIndex { get; set; } + public string SearchIndex { get; set; } - public string DefaultQuery { get; set; } + public string DefaultQuery { get; set; } - public string[] DefaultSearchFields { get; set; } = FullTextField; + public string[] DefaultSearchFields { get; set; } = FullTextField; - public string SearchType { get; set; } + public string SearchType { get; set; } - [Obsolete("This property will be removed in future releases.")] - public const string StandardAnalyzer = "standardanalyzer"; + [Obsolete("This property will be removed in future releases.")] + public const string StandardAnalyzer = "standardanalyzer"; - [Obsolete($"This property will be removed in future releases. Instead use {nameof(SearchType)} property.")] - public bool AllowElasticQueryStringQueryInSearch { get; set; } = false; + [Obsolete($"This property will be removed in future releases. Instead use {nameof(SearchType)} property.")] + public bool AllowElasticQueryStringQueryInSearch { get; set; } = false; - public string GetSearchType() - { + public string GetSearchType() + { #pragma warning disable CS0618 // Type or member is obsolete - if (SearchType == null && AllowElasticQueryStringQueryInSearch) - { - return QueryStringSearchType; - } + if (SearchType == null && AllowElasticQueryStringQueryInSearch) + { + return QueryStringSearchType; + } #pragma warning restore CS0618 // Type or member is obsolete - return SearchType; - } + return SearchType; } } diff --git a/src/docs/reference/modules/Elasticsearch/README.md b/src/docs/reference/modules/Elasticsearch/README.md index 7eb50debb28..4f5ec84dedb 100644 --- a/src/docs/reference/modules/Elasticsearch/README.md +++ b/src/docs/reference/modules/Elasticsearch/README.md @@ -90,7 +90,7 @@ Here is an example for setting default search settings: "DefaultSearchFields":[ "Content.ContentItem.FullText" ], - "SearchType": "", // Use 'raw' for a custom query in DefaultQuery and 'query_string' for a Query String Query search. Leave it blank for the default, which is a Multi-Match Query search. + "SearchType": "", // Use 'custom' for a custom query in DefaultQuery and 'query_string' for a Query String Query search. Leave it blank for the default, which is a Multi-Match Query search. "DefaultQuery": null, "SyncWithLucene":true // Allows to sync content index settings. } From 9896ab2325d7a095e5f6a6a1e0d645e851488019 Mon Sep 17 00:00:00 2001 From: Mike Alhayek Date: Thu, 7 Dec 2023 11:51:42 -0800 Subject: [PATCH 8/8] final changes --- .../Drivers/ElasticSettingsDisplayDriver.cs | 155 +++++++++--------- .../Services/ElasticSearchService.cs | 5 +- 2 files changed, 78 insertions(+), 82 deletions(-) diff --git a/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Drivers/ElasticSettingsDisplayDriver.cs b/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Drivers/ElasticSettingsDisplayDriver.cs index 8873758dbd0..dac5b29ebe9 100644 --- a/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Drivers/ElasticSettingsDisplayDriver.cs +++ b/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Drivers/ElasticSettingsDisplayDriver.cs @@ -17,106 +17,103 @@ using OrchardCore.Search.Elasticsearch.ViewModels; using OrchardCore.Settings; -namespace OrchardCore.Search.Elasticsearch.Drivers +namespace OrchardCore.Search.Elasticsearch.Drivers; + +public class ElasticSettingsDisplayDriver : SectionDisplayDriver { - public class ElasticSettingsDisplayDriver : SectionDisplayDriver + public const string GroupId = "elasticsearch"; + + private static readonly char[] _separator = [',', ' ']; + private static readonly JsonSerializerOptions _jsonSerializerOptions = new() { - public const string GroupId = "elasticsearch"; + WriteIndented = true, + }; + private readonly ElasticIndexSettingsService _elasticIndexSettingsService; + private readonly IHttpContextAccessor _httpContextAccessor; + private readonly IAuthorizationService _authorizationService; + private readonly IElasticClient _elasticClient; + protected readonly IStringLocalizer S; - private static readonly char[] _separator = [',', ' ']; - private static readonly JsonSerializerOptions _jsonSerializerOptions = new() - { - WriteIndented = true, + public ElasticSettingsDisplayDriver( + ElasticIndexSettingsService elasticIndexSettingsService, + IHttpContextAccessor httpContextAccessor, + IAuthorizationService authorizationService, + IElasticClient elasticClient, + IStringLocalizer stringLocalizer + ) + { + _elasticIndexSettingsService = elasticIndexSettingsService; + _httpContextAccessor = httpContextAccessor; + _authorizationService = authorizationService; + _elasticClient = elasticClient; + S = stringLocalizer; + } - }; - private readonly ElasticIndexSettingsService _elasticIndexSettingsService; - private readonly IHttpContextAccessor _httpContextAccessor; - private readonly IAuthorizationService _authorizationService; - private readonly IElasticClient _elasticClient; - protected readonly IStringLocalizer S; + public override IDisplayResult Edit(ElasticSettings settings) + => Initialize("ElasticSettings_Edit", async model => + { + model.SearchIndex = settings.SearchIndex; + model.SearchFields = string.Join(", ", settings.DefaultSearchFields ?? []); + model.SearchIndexes = (await _elasticIndexSettingsService.GetSettingsAsync()).Select(x => x.IndexName); + model.DefaultQuery = settings.DefaultQuery; + model.SearchType = settings.GetSearchType(); + model.SearchTypes = [ + new(S["Multi-Match Query (Default)"], string.Empty), + new(S["Query String Query"], ElasticSettings.QueryStringSearchType), + new(S["Custom Query"], ElasticSettings.CustomSearchType), + ]; + }).Location("Content:2") + .RenderWhen(() => _authorizationService.AuthorizeAsync(_httpContextAccessor.HttpContext.User, Permissions.ManageElasticIndexes)) + .OnGroup(GroupId); - public ElasticSettingsDisplayDriver( - ElasticIndexSettingsService elasticIndexSettingsService, - IHttpContextAccessor httpContextAccessor, - IAuthorizationService authorizationService, - IElasticClient elasticClient, - IStringLocalizer stringLocalizer + public override async Task UpdateAsync(ElasticSettings section, BuildEditorContext context) + { + if (!string.Equals(GroupId, context.GroupId, StringComparison.OrdinalIgnoreCase)) + { + return null; + } - ) + if (!await _authorizationService.AuthorizeAsync(_httpContextAccessor.HttpContext?.User, Permissions.ManageElasticIndexes)) { - _elasticIndexSettingsService = elasticIndexSettingsService; - _httpContextAccessor = httpContextAccessor; - _authorizationService = authorizationService; - _elasticClient = elasticClient; - S = stringLocalizer; + return null; } - public override IDisplayResult Edit(ElasticSettings settings) - => Initialize("ElasticSettings_Edit", async model => - { - model.SearchIndex = settings.SearchIndex; - model.SearchFields = string.Join(", ", settings.DefaultSearchFields ?? []); - model.SearchIndexes = (await _elasticIndexSettingsService.GetSettingsAsync()).Select(x => x.IndexName); - model.DefaultQuery = settings.DefaultQuery; - model.SearchType = settings.GetSearchType(); - model.SearchTypes = [ - new(S["Multi-Match Query (Default)"], string.Empty), - new(S["Query String Query"], ElasticSettings.QueryStringSearchType), - new(S["Custom Query"], ElasticSettings.CustomSearchType), - ]; - }).Location("Content:2") - .RenderWhen(() => _authorizationService.AuthorizeAsync(_httpContextAccessor.HttpContext.User, Permissions.ManageElasticIndexes)) - .OnGroup(GroupId); + var model = new ElasticSettingsViewModel(); - public override async Task UpdateAsync(ElasticSettings section, BuildEditorContext context) + await context.Updater.TryUpdateModelAsync(model, Prefix); + + section.DefaultQuery = null; + section.SearchIndex = model.SearchIndex; + section.DefaultSearchFields = model.SearchFields?.Split(_separator, StringSplitOptions.RemoveEmptyEntries); + section.SearchType = model.SearchType ?? string.Empty; + + if (model.SearchType == ElasticSettings.CustomSearchType) { - if (!string.Equals(GroupId, context.GroupId, StringComparison.OrdinalIgnoreCase)) + if (string.IsNullOrWhiteSpace(model.DefaultQuery)) { - return null; + context.Updater.ModelState.AddModelError(Prefix, nameof(model.DefaultQuery), S["Please provide the default query."]); } - - if (!await _authorizationService.AuthorizeAsync(_httpContextAccessor.HttpContext?.User, Permissions.ManageElasticIndexes)) + else if (!JsonHelpers.TryParse(model.DefaultQuery, out var document)) { - return null; + context.Updater.ModelState.AddModelError(Prefix, nameof(model.DefaultQuery), S["The provided query is not formatted correctly."]); } - - var model = new ElasticSettingsViewModel(); - - await context.Updater.TryUpdateModelAsync(model, Prefix); - - section.DefaultQuery = null; - section.SearchIndex = model.SearchIndex; - section.DefaultSearchFields = model.SearchFields?.Split(_separator, StringSplitOptions.RemoveEmptyEntries); - section.SearchType = model.SearchType ?? string.Empty; - - if (model.SearchType == ElasticSettings.CustomSearchType) + else { - if (string.IsNullOrWhiteSpace(model.DefaultQuery)) - { - context.Updater.ModelState.AddModelError(Prefix, nameof(model.DefaultQuery), S["Please provide the default query."]); - } - else if (!JsonHelpers.TryParse(model.DefaultQuery, out var document)) + section.DefaultQuery = JsonSerializer.Serialize(document, _jsonSerializerOptions); + + try { - context.Updater.ModelState.AddModelError(Prefix, nameof(model.DefaultQuery), S["The provided query is not formatted correctly."]); + using var stream = new MemoryStream(Encoding.UTF8.GetBytes(model.DefaultQuery)); + + var searchRequest = await _elasticClient.RequestResponseSerializer.DeserializeAsync(stream); } - else + catch { - section.DefaultQuery = JsonSerializer.Serialize(document, _jsonSerializerOptions); - - try - { - using var stream = new MemoryStream(Encoding.UTF8.GetBytes(model.DefaultQuery)); - - var searchRequest = await _elasticClient.RequestResponseSerializer.DeserializeAsync(stream); - } - catch - { - context.Updater.ModelState.AddModelError(Prefix, nameof(model.DefaultQuery), S["Invalid query provided."]); - } + context.Updater.ModelState.AddModelError(Prefix, nameof(model.DefaultQuery), S["Invalid query provided."]); } } - - return await EditAsync(section, context); } + + return await EditAsync(section, context); } } diff --git a/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Services/ElasticSearchService.cs b/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Services/ElasticSearchService.cs index 05055270957..403281a17fe 100644 --- a/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Services/ElasticSearchService.cs +++ b/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Services/ElasticSearchService.cs @@ -75,13 +75,12 @@ public async Task SearchAsync(string indexName, string term, int s return result; } - var searchType = searchSettings.GetSearchType(); - try { + var searchType = searchSettings.GetSearchType(); QueryContainer query = null; - if (searchType == ElasticSettings.CustomSearchType) + if (searchType == ElasticSettings.CustomSearchType && !string.IsNullOrWhiteSpace(searchSettings.DefaultQuery)) { var tokenizedContent = await _liquidTemplateManager.RenderStringAsync(searchSettings.DefaultQuery, _javaScriptEncoder, new Dictionary()