diff --git a/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Controllers/AdminController.cs b/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Controllers/AdminController.cs index 4203ebc0241..628acdcb7cd 100644 --- a/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Controllers/AdminController.cs +++ b/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Controllers/AdminController.cs @@ -432,14 +432,15 @@ public async Task ForceDelete(ElasticIndexSettingsViewModel model) return RedirectToAction(nameof(Index)); } - public async Task Mappings(string indexName) + public async Task IndexInfo(string indexName) { - var mappings = await _elasticIndexManager.GetIndexMappings(indexName); - var formattedJson = JNode.Parse(mappings).ToJsonString(JOptions.Indented); - return View(new MappingsViewModel + var info = await _elasticIndexManager.GetIndexInfo(indexName); + + var formattedJson = JNode.Parse(info).ToJsonString(JOptions.Indented); + return View(new IndexInfoViewModel { IndexName = _elasticIndexManager.GetFullIndexName(indexName), - Mappings = formattedJson + IndexInfo = formattedJson }); } @@ -514,7 +515,9 @@ public async Task Query(AdminQueryViewModel model) stopwatch.Start(); var parameters = JConvert.DeserializeObject>(model.Parameters); - var tokenizedContent = await _liquidTemplateManager.RenderStringAsync(model.DecodedQuery, _javaScriptEncoder, parameters.Select(x => new KeyValuePair(x.Key, FluidValue.Create(x.Value, _templateOptions.Value)))); + var props = parameters.Select(x => new KeyValuePair(x.Key, FluidValue.Create(x.Value, _templateOptions.Value))); + + var tokenizedContent = await _liquidTemplateManager.RenderStringAsync(model.DecodedQuery, _javaScriptEncoder, props); try { diff --git a/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Extensions/ElasticsearchOptionsExtensions.cs b/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Extensions/ElasticsearchOptionsExtensions.cs new file mode 100644 index 00000000000..72a4c25aeeb --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Extensions/ElasticsearchOptionsExtensions.cs @@ -0,0 +1,81 @@ +using System.Text.Json; +using System.Text.Json.Nodes; +using Microsoft.Extensions.Configuration; +using OrchardCore.Environment.Shell.Configuration; + +namespace OrchardCore.Search.Elasticsearch.Extensions; +internal static class ElasticsearchOptionsExtensions +{ + internal static ElasticsearchOptions AddAnalyzers(this ElasticsearchOptions options, IConfigurationSection configuration) + { + var jsonNode = configuration.GetSection(nameof(options.Analyzers)).AsJsonNode(); + var jsonElement = JsonSerializer.Deserialize(jsonNode); + + var analyzersObject = JsonObject.Create(jsonElement, new JsonNodeOptions() + { + PropertyNameCaseInsensitive = true, + }); + + if (analyzersObject != null) + { + if (jsonNode is JsonObject jAnalyzers) + { + foreach (var analyzer in jAnalyzers) + { + if (analyzer.Value is not JsonObject jAnalyzer) + { + continue; + } + + options.Analyzers.Add(analyzer.Key, jAnalyzer); + } + } + } + + if (options.Analyzers.Count == 0) + { + // When no analyzers are configured, we'll define a default analyzer. + options.Analyzers.Add(ElasticsearchConstants.DefaultAnalyzer, new JsonObject + { + ["type"] = "standard", + }); + } + + return options; + } + + internal static ElasticsearchOptions AddFilter(this ElasticsearchOptions options, IConfigurationSection configuration) + { + var jsonNode = configuration.GetSection(nameof(options.Filter)).AsJsonNode(); + var jsonElement = JsonSerializer.Deserialize(jsonNode); + + var filterObject = JsonObject.Create(jsonElement, new JsonNodeOptions() + { + PropertyNameCaseInsensitive = true, + }); + + if (filterObject != null) + { + if (jsonNode is JsonObject jFilters) + { + foreach (var filter in jFilters) + { + if (filter.Value is not JsonObject jFilter) + { + continue; + } + + options.Filter.Add(filter.Key, jFilter); + } + } + } + + return options; + } + + internal static ElasticsearchOptions AddIndexPrefix(this ElasticsearchOptions options, IConfigurationSection configuration) + { + options.IndexPrefix = configuration.GetValue(nameof(options.IndexPrefix)); + return options; + } +} diff --git a/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Startup.cs b/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Startup.cs index 6e71de34f8c..ec79e45f516 100644 --- a/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Startup.cs +++ b/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Startup.cs @@ -24,6 +24,7 @@ using OrchardCore.Search.Elasticsearch.Core.Providers; using OrchardCore.Search.Elasticsearch.Core.Services; using OrchardCore.Search.Elasticsearch.Drivers; +using OrchardCore.Search.Elasticsearch.Extensions; using OrchardCore.Search.Elasticsearch.Services; using OrchardCore.Search.Lucene.Handler; using OrchardCore.Security.Permissions; @@ -54,42 +55,9 @@ public override void ConfigureServices(IServiceCollection services) { var configuration = _shellConfiguration.GetSection(ElasticConnectionOptionsConfigurations.ConfigSectionName); - o.IndexPrefix = configuration.GetValue(nameof(o.IndexPrefix)); - - var jsonNode = configuration.GetSection(nameof(o.Analyzers)).AsJsonNode(); - var jsonElement = JsonSerializer.Deserialize(jsonNode); - - var analyzersObject = JsonObject.Create(jsonElement, new JsonNodeOptions() - { - PropertyNameCaseInsensitive = true, - }); - - if (analyzersObject != null) - { - o.IndexPrefix = configuration.GetValue(nameof(o.IndexPrefix)); - - if (jsonNode is JsonObject jAnalyzers) - { - foreach (var analyzer in jAnalyzers) - { - if (analyzer.Value is not JsonObject jAnalyzer) - { - continue; - } - - o.Analyzers.Add(analyzer.Key, jAnalyzer); - } - } - } - - if (o.Analyzers.Count == 0) - { - // When no analyzers are configured, we'll define a default analyzer. - o.Analyzers.Add(ElasticsearchConstants.DefaultAnalyzer, new JsonObject - { - ["type"] = "standard", - }); - } + o.AddIndexPrefix(configuration); + o.AddFilter(configuration); + o.AddAnalyzers(configuration); }); services.AddElasticServices(); diff --git a/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/ViewModels/MappingsViewModel.cs b/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/ViewModels/IndexInfoViewModel.cs similarity index 57% rename from src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/ViewModels/MappingsViewModel.cs rename to src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/ViewModels/IndexInfoViewModel.cs index 0d350b911f0..e44fe18e8d5 100644 --- a/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/ViewModels/MappingsViewModel.cs +++ b/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/ViewModels/IndexInfoViewModel.cs @@ -1,7 +1,7 @@ namespace OrchardCore.Search.Elasticsearch.ViewModels; -public class MappingsViewModel +public class IndexInfoViewModel { public string IndexName { get; set; } - public string Mappings { get; set; } + public string IndexInfo { get; set; } } diff --git a/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Views/Admin/Index.cshtml b/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Views/Admin/Index.cshtml index 8a2ae27893e..84b5c64c2db 100644 --- a/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Views/Admin/Index.cshtml +++ b/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Views/Admin/Index.cshtml @@ -69,7 +69,7 @@

@entry.AnalyzerName

- @T["Mappings"] + @T["Index Info"] @T["Query"] @T["Reset"] @T["Rebuild"] diff --git a/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Views/Admin/Mappings.cshtml b/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Views/Admin/IndexInfo.cshtml similarity index 77% rename from src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Views/Admin/Mappings.cshtml rename to src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Views/Admin/IndexInfo.cshtml index 692698ce721..666caa53a27 100644 --- a/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Views/Admin/Mappings.cshtml +++ b/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Views/Admin/IndexInfo.cshtml @@ -1,16 +1,16 @@ -@model MappingsViewModel +@model IndexInfoViewModel -

@T["Elasticsearch index mappings"]

+

@T["Elasticsearch index information"]

- +
-
+
- - @T["The Elasticsearch index mapping. For reference only."] + + @T["The Elasticsearch index information. For reference only."]
@@ -35,8 +35,8 @@ setTheme(); var modelUri = monaco.Uri.parse("x://orchardcore.search.elastic.mappings.json"); - var editor = document.getElementById('@Html.IdFor(x => x.Mappings)_editor'); - var textArea = document.getElementById('@Html.IdFor(x => x.Mappings)'); + var editor = document.getElementById('@Html.IdFor(x => x.IndexInfo)_editor'); + var textArea = document.getElementById('@Html.IdFor(x => x.IndexInfo)'); var schema = JSON.parse(editor.dataset.schema) var model = monaco.editor.createModel(textArea.value, "json", modelUri); diff --git a/src/OrchardCore/OrchardCore.Search.Elasticsearch.Abstractions/ElasticTopDocs.cs b/src/OrchardCore/OrchardCore.Search.Elasticsearch.Abstractions/ElasticTopDocs.cs index 4dbe694a3ae..a49d55febb6 100644 --- a/src/OrchardCore/OrchardCore.Search.Elasticsearch.Abstractions/ElasticTopDocs.cs +++ b/src/OrchardCore/OrchardCore.Search.Elasticsearch.Abstractions/ElasticTopDocs.cs @@ -1,8 +1,11 @@ +using Nest; + namespace OrchardCore.Search.Elasticsearch; public class ElasticTopDocs { public List> TopDocs { get; set; } public List> Fields { get; set; } + public IReadOnlyCollection>> Hits { get; set; } public long Count { get; set; } } diff --git a/src/OrchardCore/OrchardCore.Search.Elasticsearch.Abstractions/ElasticsearchOptions.cs b/src/OrchardCore/OrchardCore.Search.Elasticsearch.Abstractions/ElasticsearchOptions.cs index 514d5cad837..53a6466ff40 100644 --- a/src/OrchardCore/OrchardCore.Search.Elasticsearch.Abstractions/ElasticsearchOptions.cs +++ b/src/OrchardCore/OrchardCore.Search.Elasticsearch.Abstractions/ElasticsearchOptions.cs @@ -7,4 +7,6 @@ public class ElasticsearchOptions public string IndexPrefix { get; set; } public Dictionary Analyzers { get; } = []; + + public Dictionary Filter { get; } = []; } diff --git a/src/OrchardCore/OrchardCore.Search.Elasticsearch.Core/Services/ElasticIndexManager.cs b/src/OrchardCore/OrchardCore.Search.Elasticsearch.Core/Services/ElasticIndexManager.cs index ad5b12d9f3c..42314ee7a01 100644 --- a/src/OrchardCore/OrchardCore.Search.Elasticsearch.Core/Services/ElasticIndexManager.cs +++ b/src/OrchardCore/OrchardCore.Search.Elasticsearch.Core/Services/ElasticIndexManager.cs @@ -41,6 +41,343 @@ public sealed class ElasticIndexManager { "custom", () => new CustomAnalyzer() }, { "stop", () => new StopAnalyzer() }, }; + + private List _tokenFilterNames = new List() + { + "asciifolding", + "common_grams", + "condition", + "delimited_payload", + "dictionary_decompounder", + "edge_ngram", + "elision", + "fingerprint", + "hunspell", + "hyphenation_decompounder", + "icu_collation", + "icu_folding", + "icu_normalizer", + "icu_transform", + "keep_types", + "keep", + "keyword_marker", + "kstem", + "kuromoji_part_of_speech", + "kuromoji_readingform", + "kuromoji_stemmer", + "length", + "limit", + "lowercase", + "multiplexer", + "ngram", + "nori_part_of_speech", + "pattern_capture", + "pattern_replace", + "phonetic", + "porter_stem", + "predicate_token_filter", + "remove_duplicates", + "reverse", + "shingle", + "snowball", + "stemmer_override", + "stemmer", + "stop", + "synonym_graph", + "synonym", + "trim", + "truncate", + "unique", + "uppercase", + "word_delimiter_graph", + "word_delimiter" + }; + private sealed record TokenFilterBuildingInfo(ITokenFilter TokenFilter, Func AddTokenFilter); + private readonly Dictionary _tokenFilterBuildingInfoGetter = new(StringComparer.OrdinalIgnoreCase) + { + { + "asciifolding", + new TokenFilterBuildingInfo( new AsciiFoldingTokenFilter(), + (TokenFiltersDescriptor d, ITokenFilter tokenFilter, string name) => + d.AsciiFolding(name, f => (AsciiFoldingTokenFilter)tokenFilter) ) + }, + { + "common_grams", + new TokenFilterBuildingInfo( new CommonGramsTokenFilter(), + (TokenFiltersDescriptor d, ITokenFilter tokenFilter, string name) => + d.CommonGrams(name, f => (CommonGramsTokenFilter)tokenFilter) ) + }, + { + "condition", + new TokenFilterBuildingInfo( new ConditionTokenFilter(), + (TokenFiltersDescriptor d, ITokenFilter tokenFilter, string name) => + d.Condition(name, f => (ConditionTokenFilter)tokenFilter) ) + }, + { + "delimited_payload", + new TokenFilterBuildingInfo( new DelimitedPayloadTokenFilter(), + (TokenFiltersDescriptor d, ITokenFilter tokenFilter, string name) => + d.DelimitedPayload(name, f => (DelimitedPayloadTokenFilter)tokenFilter) ) + }, + { + "dictionary_decompounder", + new TokenFilterBuildingInfo( new DictionaryDecompounderTokenFilter(), + (TokenFiltersDescriptor d, ITokenFilter tokenFilter, string name) => + d.DictionaryDecompounder(name, f => (DictionaryDecompounderTokenFilter)tokenFilter) ) + }, + { + "edge_ngram", + new TokenFilterBuildingInfo( new EdgeNGramTokenFilter(), + (TokenFiltersDescriptor d, ITokenFilter tokenFilter, string name) => + d.EdgeNGram(name, f => (EdgeNGramTokenFilter)tokenFilter) ) + }, + { + "elision", + new TokenFilterBuildingInfo( new ElisionTokenFilter(), + (TokenFiltersDescriptor d, ITokenFilter tokenFilter, string name) => + d.Elision(name, f => (ElisionTokenFilter)tokenFilter) ) + }, + { + "fingerprint", + new TokenFilterBuildingInfo( new FingerprintTokenFilter(), + (TokenFiltersDescriptor d, ITokenFilter tokenFilter, string name) => + d.Fingerprint(name, f => (FingerprintTokenFilter)tokenFilter) ) + }, + { + "hunspell", + new TokenFilterBuildingInfo( new HunspellTokenFilter(), + (TokenFiltersDescriptor d, ITokenFilter tokenFilter, string name) => + d.Hunspell(name, f => (HunspellTokenFilter)tokenFilter) ) + }, + { + "hyphenation_decompounder", + new TokenFilterBuildingInfo( new HyphenationDecompounderTokenFilter(), + (TokenFiltersDescriptor d, ITokenFilter tokenFilter, string name) => + d.HyphenationDecompounder(name, f => (HyphenationDecompounderTokenFilter)tokenFilter) ) + }, + { + "icu_collation", + new TokenFilterBuildingInfo( new IcuCollationTokenFilter(), + (TokenFiltersDescriptor d, ITokenFilter tokenFilter, string name) => + d.IcuCollation(name, f => (IcuCollationTokenFilter)tokenFilter) ) + }, + { + "icu_folding", + new TokenFilterBuildingInfo( new IcuFoldingTokenFilter(), + (TokenFiltersDescriptor d, ITokenFilter tokenFilter, string name) => + d.IcuFolding(name, f => (IcuFoldingTokenFilter)tokenFilter) ) + }, + { + "icu_normalizer", + new TokenFilterBuildingInfo( new IcuNormalizationTokenFilter(), + (TokenFiltersDescriptor d, ITokenFilter tokenFilter, string name) => + d.IcuNormalization(name, f => (IcuNormalizationTokenFilter)tokenFilter) ) + }, + { + "icu_transform", + new TokenFilterBuildingInfo( new IcuTransformTokenFilter(), + (TokenFiltersDescriptor d, ITokenFilter tokenFilter, string name) => + d.IcuTransform(name, f => (IcuTransformTokenFilter)tokenFilter) ) + }, + { + "keep_types", + new TokenFilterBuildingInfo( new KeepTypesTokenFilter(), + (TokenFiltersDescriptor d, ITokenFilter tokenFilter, string name) => + d.KeepTypes(name, f => (KeepTypesTokenFilter)tokenFilter) ) + }, + { + "keep", + new TokenFilterBuildingInfo( new KeepWordsTokenFilter(), + (TokenFiltersDescriptor d, ITokenFilter tokenFilter, string name) => + d.KeepWords(name, f => (KeepWordsTokenFilter)tokenFilter) ) + }, + { + "keyword_marker", + new TokenFilterBuildingInfo( new KeywordMarkerTokenFilter(), + (TokenFiltersDescriptor d, ITokenFilter tokenFilter, string name) => + d.KeywordMarker(name, f => (KeywordMarkerTokenFilter)tokenFilter) ) + }, + { + "kstem", + new TokenFilterBuildingInfo( new KStemTokenFilter(), + (TokenFiltersDescriptor d, ITokenFilter tokenFilter, string name) => + d.KStem(name, f => (KStemTokenFilter)tokenFilter) ) + }, + { + "kuromoji_part_of_speech", + new TokenFilterBuildingInfo( new KuromojiPartOfSpeechTokenFilter(), + (TokenFiltersDescriptor d, ITokenFilter tokenFilter, string name) => + d.KuromojiPartOfSpeech(name, f => (KuromojiPartOfSpeechTokenFilter)tokenFilter) ) + }, + { + "kuromoji_readingform", + new TokenFilterBuildingInfo( new KuromojiReadingFormTokenFilter(), + (TokenFiltersDescriptor d, ITokenFilter tokenFilter, string name) => + d.KuromojiReadingForm(name, f => (KuromojiReadingFormTokenFilter)tokenFilter) ) + }, + { + "kuromoji_stemmer", + new TokenFilterBuildingInfo( new KuromojiStemmerTokenFilter(), + (TokenFiltersDescriptor d, ITokenFilter tokenFilter, string name) => + d.KuromojiStemmer(name, f => (KuromojiStemmerTokenFilter)tokenFilter) ) + }, + { + "length", + new TokenFilterBuildingInfo( new LengthTokenFilter(), + (TokenFiltersDescriptor d, ITokenFilter tokenFilter, string name) => + d.Length(name, f => (LengthTokenFilter)tokenFilter) ) + }, + { + "limit", + new TokenFilterBuildingInfo( new LimitTokenCountTokenFilter(), + (TokenFiltersDescriptor d, ITokenFilter tokenFilter, string name) => + d.LimitTokenCount(name, f => (LimitTokenCountTokenFilter)tokenFilter) ) + }, + { + "lowercase", + new TokenFilterBuildingInfo( new LowercaseTokenFilter(), + (TokenFiltersDescriptor d, ITokenFilter tokenFilter, string name) => + d.Lowercase(name, f => (LowercaseTokenFilter)tokenFilter) ) + }, + { + "multiplexer", + new TokenFilterBuildingInfo( new MultiplexerTokenFilter(), + (TokenFiltersDescriptor d, ITokenFilter tokenFilter, string name) => + d.Multiplexer(name, f => (MultiplexerTokenFilter)tokenFilter) ) + }, + { + "ngram", + new TokenFilterBuildingInfo( new NGramTokenFilter(), + (TokenFiltersDescriptor d, ITokenFilter tokenFilter, string name) => + d.NGram(name, f => (NGramTokenFilter)tokenFilter) ) + }, + { + "nori_part_of_speech", + new TokenFilterBuildingInfo( new NoriPartOfSpeechTokenFilter(), + (TokenFiltersDescriptor d, ITokenFilter tokenFilter, string name) => + d.NoriPartOfSpeech(name, f => (NoriPartOfSpeechTokenFilter)tokenFilter) ) + }, + { + "pattern_capture", + new TokenFilterBuildingInfo( new PatternCaptureTokenFilter(), + (TokenFiltersDescriptor d, ITokenFilter tokenFilter, string name) => + d.PatternCapture(name, f => (PatternCaptureTokenFilter)tokenFilter) ) + }, + { + "pattern_replace", + new TokenFilterBuildingInfo( new PatternReplaceTokenFilter(), + (TokenFiltersDescriptor d, ITokenFilter tokenFilter, string name) => + d.PatternReplace(name, f => (PatternReplaceTokenFilter)tokenFilter) ) + }, + { + "phonetic", + new TokenFilterBuildingInfo( new PhoneticTokenFilter(), + (TokenFiltersDescriptor d, ITokenFilter tokenFilter, string name) => + d.Phonetic(name, f => (PhoneticTokenFilter)tokenFilter) ) + }, + { + "porter_stem", + new TokenFilterBuildingInfo( new PorterStemTokenFilter(), + (TokenFiltersDescriptor d, ITokenFilter tokenFilter, string name) => + d.PorterStem(name, f => (PorterStemTokenFilter)tokenFilter) ) + }, + { + "predicate_token_filter", + new TokenFilterBuildingInfo( new PredicateTokenFilter(), + (TokenFiltersDescriptor d, ITokenFilter tokenFilter, string name) => + d.Predicate(name, f => (PredicateTokenFilter)tokenFilter) ) + }, + { + "remove_duplicates", + new TokenFilterBuildingInfo( new RemoveDuplicatesTokenFilter(), + (TokenFiltersDescriptor d, ITokenFilter tokenFilter, string name) => + d.RemoveDuplicates(name, f => (RemoveDuplicatesTokenFilter)tokenFilter) ) + }, + { + "reverse", + new TokenFilterBuildingInfo( new ReverseTokenFilter(), + (TokenFiltersDescriptor d, ITokenFilter tokenFilter, string name) => + d.Reverse(name, f => (ReverseTokenFilter)tokenFilter) ) + }, + { + "shingle", + new TokenFilterBuildingInfo( new ShingleTokenFilter(), + (TokenFiltersDescriptor d, ITokenFilter tokenFilter, string name) => + d.Shingle(name, f => (ShingleTokenFilter)tokenFilter) ) + }, + { + "snowball", + new TokenFilterBuildingInfo( new SnowballTokenFilter(), + (TokenFiltersDescriptor d, ITokenFilter tokenFilter, string name) => + d.Snowball(name, f => (SnowballTokenFilter)tokenFilter) ) + }, + { + "stemmer_override", + new TokenFilterBuildingInfo( new StemmerOverrideTokenFilterDescriptor(), + (TokenFiltersDescriptor d, ITokenFilter tokenFilter, string name) => + d.StemmerOverride(name, f => (StemmerOverrideTokenFilterDescriptor)tokenFilter) ) + }, + { + "stemmer", + new TokenFilterBuildingInfo( new StemmerTokenFilter(), + (TokenFiltersDescriptor d, ITokenFilter tokenFilter, string name) => + d.Stemmer(name, f => (StemmerTokenFilter)tokenFilter) ) + }, + { + "stop", + new TokenFilterBuildingInfo( new StopTokenFilter(), + (TokenFiltersDescriptor d, ITokenFilter tokenFilter, string name) => + d.Stop(name, f => (StopTokenFilter)tokenFilter) ) + }, + { + "synonym_graph", + new TokenFilterBuildingInfo( new SynonymGraphTokenFilter(), + (TokenFiltersDescriptor d, ITokenFilter tokenFilter, string name) => + d.SynonymGraph(name, f => (SynonymGraphTokenFilter)tokenFilter) ) + }, + { + "synonym", + new TokenFilterBuildingInfo( new SynonymTokenFilter(), + (TokenFiltersDescriptor d, ITokenFilter tokenFilter, string name) => + d.Synonym(name, f => (SynonymTokenFilter)tokenFilter) ) + }, + { + "trim", + new TokenFilterBuildingInfo( new TrimTokenFilter(), + (TokenFiltersDescriptor d, ITokenFilter tokenFilter, string name) => + d.Trim(name, f => (TrimTokenFilter)tokenFilter) ) + }, + { + "truncate", + new TokenFilterBuildingInfo( new TruncateTokenFilter(), + (TokenFiltersDescriptor d, ITokenFilter tokenFilter, string name) => + d.Truncate(name, f => (TruncateTokenFilter)tokenFilter) ) + }, + { + "unique", + new TokenFilterBuildingInfo( new UniqueTokenFilter(), + (TokenFiltersDescriptor d, ITokenFilter tokenFilter, string name) => + d.Unique(name, f => (UniqueTokenFilter)tokenFilter) ) + }, + { + "uppercase", + new TokenFilterBuildingInfo( new UppercaseTokenFilter(), + (TokenFiltersDescriptor d, ITokenFilter tokenFilter, string name) => + d.Uppercase(name, f => (UppercaseTokenFilter)tokenFilter) ) + }, + { + "word_delimiter_graph", + new TokenFilterBuildingInfo( new WordDelimiterGraphTokenFilter(), + (TokenFiltersDescriptor d, ITokenFilter tokenFilter, string name) => + d.WordDelimiterGraph(name, f => (WordDelimiterGraphTokenFilter)tokenFilter) ) + }, + { + "word_delimiter", + new TokenFilterBuildingInfo( new WordDelimiterTokenFilter(), + (TokenFiltersDescriptor d, ITokenFilter tokenFilter, string name) => + d.WordDelimiter(name, f => (WordDelimiterTokenFilter)tokenFilter) ) + } + }; private static readonly List _charsToRemove = [ '\\', @@ -101,8 +438,24 @@ public async Task CreateIndexAsync(ElasticIndexSettings elasticIndexSettin if (_elasticsearchOptions.Analyzers.TryGetValue(analyzerName, out var analyzerProperties)) { - var analyzer = CreateAnalyzer(analyzerProperties); - analysisDescriptor.Analyzers(a => a.UserDefined(analyzerName, analyzer)); + if (_elasticsearchOptions.Filter is not null) + { + var tokenFiltersDescriptor = GetTokenFilterDescriptor(_elasticsearchOptions.Filter); + analysisDescriptor.TokenFilters(f => tokenFiltersDescriptor); + } + + if (analyzerProperties.TryGetPropertyValue("filter", out var filterResult)) + { + var filterCollection = JsonSerializer.Deserialize>(filterResult); + + var existingFilters = filterCollection.Where(f => _tokenFilterNames.Contains(f)); + analysisDescriptor.Analyzers(a => a.Custom("default", c => c.Tokenizer("standard").Filters(existingFilters))); + } + else + { + var analyzer = CreateAnalyzer(analyzerProperties); + analysisDescriptor.Analyzers(a => a.UserDefined(analyzerName, analyzer)); + } indexSettingsDescriptor = new IndexSettingsDescriptor(); indexSettingsDescriptor.Analysis(an => analysisDescriptor); @@ -195,6 +548,85 @@ await _elasticClient.MapAsync(p => p return response.Acknowledged; } + private TokenFiltersDescriptor GetTokenFilterDescriptor(Dictionary filter) + { + var descriptor = new TokenFiltersDescriptor(); + + foreach (var filterName in filter.Keys) + { + var filterProps = filter[filterName]; + + if (filterProps.TryGetPropertyValue("type", out var typeObject) is false + || _tokenFilterBuildingInfoGetter.TryGetValue(typeObject.ToString(), out var tokenFilterBuildingInfo) is false) + { + continue; + } + + var properties = tokenFilterBuildingInfo.TokenFilter.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance); + + foreach (var filterProperty in filterProps) + { + if (filterProperty.Value == null || string.Equals(filterProperty.Key, "type", StringComparison.OrdinalIgnoreCase)) + { + continue; + } + + var key = filterProperty.Key.Replace("_", string.Empty); + + var property = properties.FirstOrDefault(p => p.Name.Equals(key, StringComparison.OrdinalIgnoreCase)); + var propertyType = property.PropertyType; + + if (property == null) + { + continue; + } + + try + { + if (property.PropertyType == typeof(StopWords)) + { + if (filterProperty.Value is JsonArray) + { + var propertyValue = JsonSerializer.Deserialize>(filterProperty.Value); + property.SetValue(tokenFilterBuildingInfo.TokenFilter, new StopWords(propertyValue)); + } + else + { + var propertyValue = JsonSerializer.Deserialize(filterProperty.Value); + property.SetValue(tokenFilterBuildingInfo.TokenFilter, new StopWords(propertyValue)); + } + + tokenFilterBuildingInfo.AddTokenFilter(descriptor, tokenFilterBuildingInfo.TokenFilter, filterName); + _tokenFilterNames.Add(filterName); + + continue; + } + + if (filterProperty.Value is JsonArray jsonArray) + { + var propertyValue = JsonSerializer.Deserialize(filterProperty.Value, propertyType); + property.SetValue(tokenFilterBuildingInfo.TokenFilter, propertyValue); + } + else + { + var propertyValue = JsonSerializer.Deserialize(filterProperty.Value, propertyType); + property.SetValue(tokenFilterBuildingInfo.TokenFilter, propertyValue); + } + + tokenFilterBuildingInfo.AddTokenFilter(descriptor, tokenFilterBuildingInfo.TokenFilter, filterName); + _tokenFilterNames.Add(filterName); + } + catch (Exception e) + { + Console.WriteLine(e.Message); + } + } + + } + + return descriptor; + } + private IAnalyzer CreateAnalyzer(JsonObject analyzerProperties) { IAnalyzer analyzer = null; @@ -280,6 +712,30 @@ public async Task GetIndexMappings(string indexName) return response.Body; } + public async Task GetIndexSettings(string indexName) + { + var response = await _elasticClient.LowLevel.Indices.GetSettingsAsync(GetFullIndexName(indexName)); + + if (!response.Success) + { + _logger.LogWarning("There were issues retrieving index settings from Elasticsearch. {OriginalException}", response.OriginalException); + } + + return response.Body; + } + + public async Task GetIndexInfo(string indexName) + { + var response = await _elasticClient.LowLevel.Indices.GetAsync(GetFullIndexName(indexName)); + + if (!response.Success) + { + _logger.LogWarning("There were issues retrieving index info from Elasticsearch. {OriginalException}", response.OriginalException); + } + + return response.Body; + } + /// /// Store a last_task_id in the Elasticsearch index _meta mappings. /// This allows storing the last indexing task id executed on the Elasticsearch index. diff --git a/src/OrchardCore/OrchardCore.Search.Elasticsearch.Core/Services/ElasticQueryService.cs b/src/OrchardCore/OrchardCore.Search.Elasticsearch.Core/Services/ElasticQueryService.cs index 543b41e1eec..d8d7b416500 100644 --- a/src/OrchardCore/OrchardCore.Search.Elasticsearch.Core/Services/ElasticQueryService.cs +++ b/src/OrchardCore/OrchardCore.Search.Elasticsearch.Core/Services/ElasticQueryService.cs @@ -16,7 +16,7 @@ public ElasticQueryService( ILogger logger ) { - _elasticClient = elasticClient; + _elasticClient = elasticClient; _elasticIndexManager = elasticIndexManager; _logger = logger; } @@ -47,6 +47,7 @@ public async Task SearchAsync(string indexName, string query) Fields = deserializedSearchRequest.Fields, Sort = deserializedSearchRequest.Sort, Source = deserializedSearchRequest.Source, + Highlight = deserializedSearchRequest.Highlight }; var searchResponse = await _elasticClient.SearchAsync>(searchRequest); @@ -72,6 +73,7 @@ public async Task SearchAsync(string indexName, string query) elasticTopDocs.Count = searchResponse.Total; elasticTopDocs.TopDocs = new List>(searchResponse.Documents); elasticTopDocs.Fields = hits; + elasticTopDocs.Hits = searchResponse.Hits; } else { diff --git a/src/OrchardCore/OrchardCore.Search.Elasticsearch.Core/Services/ElasticQuerySource.cs b/src/OrchardCore/OrchardCore.Search.Elasticsearch.Core/Services/ElasticQuerySource.cs index 4c1f3544be4..b636e97655b 100644 --- a/src/OrchardCore/OrchardCore.Search.Elasticsearch.Core/Services/ElasticQuerySource.cs +++ b/src/OrchardCore/OrchardCore.Search.Elasticsearch.Core/Services/ElasticQuerySource.cs @@ -1,4 +1,7 @@ +using System.Diagnostics; +using System.Text; using System.Text.Encodings.Web; +using System.Text.Json; using System.Text.Json.Nodes; using Fluid; using Fluid.Values; @@ -24,6 +27,9 @@ public sealed class ElasticQuerySource : IQuerySource private readonly JavaScriptEncoder _javaScriptEncoder; private readonly TemplateOptions _templateOptions; + private readonly JsonSerializerOptions options = new JsonSerializerOptions() { Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping }; + + public ElasticQuerySource( IElasticQueryService queryService, ILiquidTemplateManager liquidTemplateManager, @@ -78,10 +84,17 @@ public async Task ExecuteQueryAsync(Query query, IDictionary(); - foreach (var document in docs.TopDocs) + foreach (var document in docs.Hits) { - results.Add(new JsonObject(document.Select(x => - KeyValuePair.Create(x.Key, (JsonNode)JsonValue.Create(x.Value.ToString()))))); + var keyValuePairs = document.Source.Select(x => + KeyValuePair.Create(x.Key, (JsonNode)JsonValue.Create(x.Value.ToString()))).ToList(); + + keyValuePairs.Add(KeyValuePair.Create("_score", (JsonNode)JsonValue.Create(document.Score.Value.ToString()))); + + var highlights = JsonSerializer.Serialize(document.Highlight, options); + keyValuePairs.Add(KeyValuePair.Create("Highlight", (JsonNode)JsonValue.Create(highlights))); + + results.Add(new JsonObject(keyValuePairs)); } elasticQueryResults.Items = results;