-
Notifications
You must be signed in to change notification settings - Fork 2.4k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add a way to provide a custom Elasticsearch query (#14843)
- Loading branch information
1 parent
e8dace6
commit 6b6a946
Showing
11 changed files
with
296 additions
and
104 deletions.
There are no files selected for viewing
128 changes: 86 additions & 42 deletions
128
...hardCore.Modules/OrchardCore.Search.Elasticsearch/Drivers/ElasticSettingsDisplayDriver.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,75 +1,119 @@ | ||
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.ViewModels; | ||
using OrchardCore.Settings; | ||
|
||
namespace OrchardCore.Search.Elasticsearch.Drivers | ||
namespace OrchardCore.Search.Elasticsearch.Drivers; | ||
|
||
public class ElasticSettingsDisplayDriver : SectionDisplayDriver<ISite, ElasticSettings> | ||
{ | ||
public class ElasticSettingsDisplayDriver : SectionDisplayDriver<ISite, ElasticSettings> | ||
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, | ||
IElasticClient elasticClient, | ||
IStringLocalizer<ElasticSettingsDisplayDriver> stringLocalizer | ||
) | ||
{ | ||
public const string GroupId = "elasticsearch"; | ||
private readonly ElasticIndexSettingsService _elasticIndexSettingsService; | ||
private readonly IHttpContextAccessor _httpContextAccessor; | ||
private readonly IAuthorizationService _authorizationService; | ||
_elasticIndexSettingsService = elasticIndexSettingsService; | ||
_httpContextAccessor = httpContextAccessor; | ||
_authorizationService = authorizationService; | ||
_elasticClient = elasticClient; | ||
S = stringLocalizer; | ||
} | ||
|
||
public ElasticSettingsDisplayDriver( | ||
ElasticIndexSettingsService elasticIndexSettingsService, | ||
IHttpContextAccessor httpContextAccessor, | ||
IAuthorizationService authorizationService | ||
) | ||
public override IDisplayResult Edit(ElasticSettings settings) | ||
=> Initialize<ElasticSettingsViewModel>("ElasticSettings_Edit", async model => | ||
{ | ||
_elasticIndexSettingsService = elasticIndexSettingsService; | ||
_httpContextAccessor = httpContextAccessor; | ||
_authorizationService = authorizationService; | ||
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 override async Task<IDisplayResult> UpdateAsync(ElasticSettings section, BuildEditorContext context) | ||
{ | ||
if (!string.Equals(GroupId, context.GroupId, StringComparison.OrdinalIgnoreCase)) | ||
{ | ||
return null; | ||
} | ||
|
||
public override async Task<IDisplayResult> EditAsync(ElasticSettings settings, BuildEditorContext context) | ||
if (!await _authorizationService.AuthorizeAsync(_httpContextAccessor.HttpContext?.User, Permissions.ManageElasticIndexes)) | ||
{ | ||
var user = _httpContextAccessor.HttpContext?.User; | ||
return null; | ||
} | ||
|
||
if (!await _authorizationService.AuthorizeAsync(user, Permissions.ManageElasticIndexes)) | ||
{ | ||
return null; | ||
} | ||
var model = new ElasticSettingsViewModel(); | ||
|
||
return Initialize<ElasticSettingsViewModel>("ElasticSettings_Edit", async model => | ||
{ | ||
model.SearchIndex = settings.SearchIndex; | ||
model.SearchFields = string.Join(", ", settings.DefaultSearchFields ?? Array.Empty<string>()); | ||
model.SearchIndexes = (await _elasticIndexSettingsService.GetSettingsAsync()).Select(x => x.IndexName); | ||
model.AllowElasticQueryStringQueryInSearch = settings.AllowElasticQueryStringQueryInSearch; | ||
}).Location("Content:2").OnGroup(GroupId); | ||
} | ||
await context.Updater.TryUpdateModelAsync(model, Prefix); | ||
|
||
public override async Task<IDisplayResult> UpdateAsync(ElasticSettings section, BuildEditorContext context) | ||
{ | ||
var user = _httpContextAccessor.HttpContext?.User; | ||
section.DefaultQuery = null; | ||
section.SearchIndex = model.SearchIndex; | ||
section.DefaultSearchFields = model.SearchFields?.Split(_separator, StringSplitOptions.RemoveEmptyEntries); | ||
section.SearchType = model.SearchType ?? string.Empty; | ||
|
||
if (!await _authorizationService.AuthorizeAsync(user, Permissions.ManageElasticIndexes)) | ||
if (model.SearchType == ElasticSettings.CustomSearchType) | ||
{ | ||
if (string.IsNullOrWhiteSpace(model.DefaultQuery)) | ||
{ | ||
return null; | ||
context.Updater.ModelState.AddModelError(Prefix, nameof(model.DefaultQuery), S["Please provide the default query."]); | ||
} | ||
|
||
if (context.GroupId.Equals(GroupId, StringComparison.OrdinalIgnoreCase)) | ||
else if (!JsonHelpers.TryParse(model.DefaultQuery, out var document)) | ||
{ | ||
var model = new ElasticSettingsViewModel(); | ||
context.Updater.ModelState.AddModelError(Prefix, nameof(model.DefaultQuery), S["The provided query is not formatted correctly."]); | ||
} | ||
else | ||
{ | ||
section.DefaultQuery = JsonSerializer.Serialize(document, _jsonSerializerOptions); | ||
|
||
await context.Updater.TryUpdateModelAsync(model, Prefix); | ||
try | ||
{ | ||
using var stream = new MemoryStream(Encoding.UTF8.GetBytes(model.DefaultQuery)); | ||
|
||
section.SearchIndex = model.SearchIndex; | ||
section.DefaultSearchFields = model.SearchFields?.Split(new[] { ',', ' ' }, StringSplitOptions.RemoveEmptyEntries); | ||
section.AllowElasticQueryStringQueryInSearch = model.AllowElasticQueryStringQueryInSearch; | ||
var searchRequest = await _elasticClient.RequestResponseSerializer.DeserializeAsync<SearchRequest>(stream); | ||
} | ||
catch | ||
{ | ||
context.Updater.ModelState.AddModelError(Prefix, nameof(model.DefaultQuery), S["Invalid query provided."]); | ||
} | ||
} | ||
|
||
return await EditAsync(section, context); | ||
} | ||
|
||
return await EditAsync(section, context); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
13 changes: 12 additions & 1 deletion
13
...chardCore.Modules/OrchardCore.Search.Elasticsearch/ViewModels/ElasticSettingsViewModel.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<string> 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<SelectListItem> SearchTypes { get; set; } | ||
} | ||
} |
74 changes: 51 additions & 23 deletions
74
src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Views/ElasticSettings.Edit.cshtml
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,35 +1,63 @@ | ||
@using OrchardCore.Search.Elasticsearch.Core.Models | ||
|
||
@model ElasticSettingsViewModel | ||
|
||
@if (Model.SearchIndexes.Any()) | ||
{ | ||
<div class="mb-3" asp-validation-class-for="SearchIndex"> | ||
<label asp-for="SearchIndex" class="form-label">@T["Default search index"]</label> | ||
<select asp-for="SearchIndex" class="form-select"> | ||
@foreach (var index in Model.SearchIndexes) | ||
{ | ||
<option value="@index" selected="@(Model.SearchIndex == index)">@index</option> | ||
} | ||
</select> | ||
<span asp-validation-for="SearchIndex"></span> | ||
<span class="hint">@T["The default index to use for the search page."]</span> | ||
</div> | ||
} | ||
else | ||
@if (!Model.SearchIndexes.Any()) | ||
{ | ||
<div class="alert alert-warning">@T["You need to create at least an index to set as the Search index."]</div> | ||
|
||
return; | ||
} | ||
|
||
<div class="mb-3" asp-validation-class-for="SearchFields"> | ||
<div class="mb-3" asp-validation-class-for="SearchIndex"> | ||
<label asp-for="SearchIndex" class="form-label">@T["Default search index"]</label> | ||
<select asp-for="SearchIndex" class="form-select"> | ||
@foreach (var index in Model.SearchIndexes) | ||
{ | ||
<option value="@index" selected="@(Model.SearchIndex == index)">@index</option> | ||
} | ||
</select> | ||
<span asp-validation-for="SearchIndex"></span> | ||
<span class="hint">@T["The default index to use for the search page."]</span> | ||
</div> | ||
|
||
<div class="mb-3" asp-validation-class-for="SearchType"> | ||
<label asp-for="SearchType" class="form-label">@T["Default search type"]</label> | ||
<select asp-for="SearchType" class="form-select" asp-items="Model.SearchTypes" data-raw-type="@ElasticSettings.CustomSearchType"></select> | ||
<span asp-validation-for="SearchType"></span> | ||
</div> | ||
|
||
<div class="mb-3" asp-validation-class-for="DefaultQuery" id="DefaultQueryContainer"> | ||
<label asp-for="DefaultQuery" class="form-label">@T["Default query"]</label> | ||
<textarea asp-for="DefaultQuery" class="form-control" rows="10"></textarea> | ||
<span asp-validation-for="DefaultQuery"></span> | ||
<span class="hint">@T["Create a custom Elasticsearch query to be utilized for each search request. Liquid is supported, so use <code>{0}</code> template as a substitute for the user-provided search term.", "{{ term }}"]</span> | ||
</div> | ||
|
||
<div class="mb-3" asp-validation-class-for="SearchFields" id="DefaultQueryFields"> | ||
<label asp-for="SearchFields" class="form-label">@T["Default searched fields"]</label> | ||
<input asp-for="SearchFields" class="form-control" /> | ||
<span asp-validation-for="SearchFields"></span> | ||
<span class="hint">@T["A comma separated list of fields to use for search pages. The default value is <code>Content.ContentItem.FullText</code>."]</span> | ||
</div> | ||
|
||
<div class="mb-3" asp-validation-class-for="AllowElasticQueryStringQueryInSearch"> | ||
<div class="form-check"> | ||
<input type="checkbox" class="form-check-input" asp-for="AllowElasticQueryStringQueryInSearch" /> | ||
<label class="form-check-label" asp-for="AllowElasticQueryStringQueryInSearch">@T["Allow Elasticsearch \"query string query\" in search forms"]</label> | ||
<span class="hint dashed">@T["Whether search queries should be allowed to use <a href=\"https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-query-string-query.html#query-dsl-query-string-query\">Elasticsearch \"query string query\" syntax</a>."] <a class="seedoc" href="https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-query-string-query.html#query-dsl-query-string-query" target="_blank">@T["See documentation"]</a></span> | ||
</div> | ||
</div> | ||
<script at="Foot"> | ||
document.addEventListener('DOMContentLoaded', function () { | ||
const menu = document.getElementById('@Html.IdFor(m => m.SearchType)'); | ||
const queryContainer = document.getElementById('DefaultQueryContainer'); | ||
const fieldsContainer = document.getElementById('DefaultQueryFields'); | ||
menu.addEventListener('change', function (e) { | ||
if (e.target.value == e.target.getAttribute('data-raw-type')) { | ||
queryContainer.classList.remove('d-none'); | ||
fieldsContainer.classList.add('d-none'); | ||
} else { | ||
queryContainer.classList.add('d-none'); | ||
fieldsContainer.classList.remove('d-none'); | ||
} | ||
}); | ||
menu.dispatchEvent(new Event('change')); | ||
}); | ||
</script> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.