diff --git a/OrchardCore.sln b/OrchardCore.sln index dd740f4577f..af62c8cdd55 100644 --- a/OrchardCore.sln +++ b/OrchardCore.sln @@ -128,7 +128,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OrchardCore.Layers", "src\O EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OrchardCore.Lists", "src\OrchardCore.Modules\OrchardCore.Lists\OrchardCore.Lists.csproj", "{ED165AA1-D202-405A-8EC6-5DD0E0B77636}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OrchardCore.Lucene", "src\OrchardCore.Modules\OrchardCore.Lucene\OrchardCore.Lucene.csproj", "{245E865F-AC80-4963-8595-F380C458F99E}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OrchardCore.Search.Lucene", "src\OrchardCore.Modules\OrchardCore.Search.Lucene\OrchardCore.Search.Lucene.csproj", "{245E865F-AC80-4963-8595-F380C458F99E}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OrchardCore.Markdown", "src\OrchardCore.Modules\OrchardCore.Markdown\OrchardCore.Markdown.csproj", "{DA02314F-7BF6-47C5-B11E-196F13BDDB77}" EndProject @@ -176,11 +176,11 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OrchardCore.Queries", "src\ EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OrchardCore.Liquid.Abstractions", "src\OrchardCore\OrchardCore.Liquid.Abstractions\OrchardCore.Liquid.Abstractions.csproj", "{2DF44392-8882-46B1-8061-61DDBCA358BD}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OrchardCore.Lucene.Abstractions", "src\OrchardCore\OrchardCore.Lucene.Abstractions\OrchardCore.Lucene.Abstractions.csproj", "{78F13261-B843-4590-8DD5-E9491874D0CD}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OrchardCore.Search.Lucene.Abstractions", "src\OrchardCore\OrchardCore.Search.Lucene.Abstractions\OrchardCore.Search.Lucene.Abstractions.csproj", "{78F13261-B843-4590-8DD5-E9491874D0CD}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OrchardCore.Liquid", "src\OrchardCore.Modules\OrchardCore.Liquid\OrchardCore.Liquid.csproj", "{F8973BF9-4F56-4A59-9153-72BEDB6AB269}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OrchardCore.Lucene.Core", "src\OrchardCore\OrchardCore.Lucene.Core\OrchardCore.Lucene.Core.csproj", "{F621B369-15FD-4923-98AD-17740C24CD5C}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OrchardCore.Search.Lucene.Core", "src\OrchardCore\OrchardCore.Search.Lucene.Core\OrchardCore.Search.Lucene.Core.csproj", "{F621B369-15FD-4923-98AD-17740C24CD5C}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OrchardCore.Queries.Abstractions", "src\OrchardCore\OrchardCore.Queries.Abstractions\OrchardCore.Queries.Abstractions.csproj", "{AEB44D2A-1493-4550-8B13-EB611B1FB12C}" EndProject @@ -404,6 +404,12 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OrchardCore.Roles.Core", "s EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OrchardCore.AutoSetup", "src\OrchardCore.Modules\OrchardCore.AutoSetup\OrchardCore.AutoSetup.csproj", "{1E76C17C-099A-4E6D-BC26-E93CBA4D0BD6}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OrchardCore.Search.Elasticsearch.Abstractions", "src\OrchardCore\OrchardCore.Search.Elasticsearch.Abstractions\OrchardCore.Search.Elasticsearch.Abstractions.csproj", "{E6A90BFD-AB5C-4AED-A5AB-799136AB1521}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OrchardCore.Search.Elasticsearch.Core", "src\OrchardCore\OrchardCore.Search.Elasticsearch.Core\OrchardCore.Search.Elasticsearch.Core.csproj", "{848C919B-6829-4758-9E59-51F13D25ABBC}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OrchardCore.Search.Elasticsearch", "src\OrchardCore.Modules\OrchardCore.Search.Elasticsearch\OrchardCore.Search.Elasticsearch.csproj", "{B6A54EA8-F285-436D-8257-6F6EDE4C3339}" +EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OrchardCore.Data.YesSql", "src\OrchardCore\OrchardCore.Data.YesSql\OrchardCore.Data.YesSql.csproj", "{F06E4E20-3675-4BA5-AD2D-4538FAB154D5}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OrchardCore.Data.YesSql.Abstractions", "src\OrchardCore\OrchardCore.Data.YesSql.Abstractions\OrchardCore.Data.YesSql.Abstractions.csproj", "{AB47A65C-7BA9-4CE7-BA73-285EB7A2CEFD}" @@ -472,6 +478,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OrchardCore.Media.AmazonS3" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OrchardCore.ArchiveLater", "src\OrchardCore.Modules\OrchardCore.ArchiveLater\OrchardCore.ArchiveLater.csproj", "{190C4BEB-C506-4F7F-BDCA-93F3C1C221BC}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OrchardCore.Search", "src\OrchardCore.Modules\OrchardCore.Search\OrchardCore.Search.csproj", "{7BDF280B-70B7-4AFC-A6F7-B5759DCA2A2C}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -1162,6 +1170,18 @@ Global {1E76C17C-099A-4E6D-BC26-E93CBA4D0BD6}.Debug|Any CPU.Build.0 = Debug|Any CPU {1E76C17C-099A-4E6D-BC26-E93CBA4D0BD6}.Release|Any CPU.ActiveCfg = Release|Any CPU {1E76C17C-099A-4E6D-BC26-E93CBA4D0BD6}.Release|Any CPU.Build.0 = Release|Any CPU + {E6A90BFD-AB5C-4AED-A5AB-799136AB1521}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E6A90BFD-AB5C-4AED-A5AB-799136AB1521}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E6A90BFD-AB5C-4AED-A5AB-799136AB1521}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E6A90BFD-AB5C-4AED-A5AB-799136AB1521}.Release|Any CPU.Build.0 = Release|Any CPU + {848C919B-6829-4758-9E59-51F13D25ABBC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {848C919B-6829-4758-9E59-51F13D25ABBC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {848C919B-6829-4758-9E59-51F13D25ABBC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {848C919B-6829-4758-9E59-51F13D25ABBC}.Release|Any CPU.Build.0 = Release|Any CPU + {B6A54EA8-F285-436D-8257-6F6EDE4C3339}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B6A54EA8-F285-436D-8257-6F6EDE4C3339}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B6A54EA8-F285-436D-8257-6F6EDE4C3339}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B6A54EA8-F285-436D-8257-6F6EDE4C3339}.Release|Any CPU.Build.0 = Release|Any CPU {F06E4E20-3675-4BA5-AD2D-4538FAB154D5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {F06E4E20-3675-4BA5-AD2D-4538FAB154D5}.Debug|Any CPU.Build.0 = Debug|Any CPU {F06E4E20-3675-4BA5-AD2D-4538FAB154D5}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -1248,6 +1268,10 @@ Global {190C4BEB-C506-4F7F-BDCA-93F3C1C221BC}.Debug|Any CPU.Build.0 = Debug|Any CPU {190C4BEB-C506-4F7F-BDCA-93F3C1C221BC}.Release|Any CPU.ActiveCfg = Release|Any CPU {190C4BEB-C506-4F7F-BDCA-93F3C1C221BC}.Release|Any CPU.Build.0 = Release|Any CPU + {7BDF280B-70B7-4AFC-A6F7-B5759DCA2A2C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7BDF280B-70B7-4AFC-A6F7-B5759DCA2A2C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7BDF280B-70B7-4AFC-A6F7-B5759DCA2A2C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7BDF280B-70B7-4AFC-A6F7-B5759DCA2A2C}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -1438,6 +1462,9 @@ Global {1D0144D0-9E6D-441B-A393-B62F6DC8E97E} = {90030E85-0C4F-456F-B879-443E8A3F220D} {15E0499A-815D-4E98-B1E4-1C9D7B3D1461} = {F23AC6C2-DE44-4699-999D-3C478EF3D691} {1E76C17C-099A-4E6D-BC26-E93CBA4D0BD6} = {A066395F-6F73-45DC-B5A6-B4E306110DCE} + {E6A90BFD-AB5C-4AED-A5AB-799136AB1521} = {F23AC6C2-DE44-4699-999D-3C478EF3D691} + {848C919B-6829-4758-9E59-51F13D25ABBC} = {F23AC6C2-DE44-4699-999D-3C478EF3D691} + {B6A54EA8-F285-436D-8257-6F6EDE4C3339} = {90030E85-0C4F-456F-B879-443E8A3F220D} {F06E4E20-3675-4BA5-AD2D-4538FAB154D5} = {F23AC6C2-DE44-4699-999D-3C478EF3D691} {AB47A65C-7BA9-4CE7-BA73-285EB7A2CEFD} = {F23AC6C2-DE44-4699-999D-3C478EF3D691} {442C544F-6587-4FA5-8459-710ED8492AD4} = {F23AC6C2-DE44-4699-999D-3C478EF3D691} @@ -1466,6 +1493,7 @@ Global {38F43FA0-5BA8-4D6B-8F66-C708D590EF76} = {F23AC6C2-DE44-4699-999D-3C478EF3D691} {FF1C550C-6D30-499A-AF11-68DE7C8B6869} = {90030E85-0C4F-456F-B879-443E8A3F220D} {190C4BEB-C506-4F7F-BDCA-93F3C1C221BC} = {90030E85-0C4F-456F-B879-443E8A3F220D} + {7BDF280B-70B7-4AFC-A6F7-B5759DCA2A2C} = {90030E85-0C4F-456F-B879-443E8A3F220D} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {46A1D25A-78D1-4476-9CBF-25B75E296341} diff --git a/mkdocs.yml b/mkdocs.yml index 2d2d87818fa..be752da9004 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -154,6 +154,7 @@ nav: - Indexing: docs/reference/modules/Indexing/README.md - SQL Indexing: docs/reference/modules/SQLIndexing/README.md - Lucene: docs/reference/modules/Lucene/README.md + - Elasticsearch: docs/reference/modules/Elasticsearch/README.md - Queries: docs/reference/modules/Queries/README.md - Media: - Media: docs/reference/modules/Media/README.md diff --git a/src/OrchardCore.Build/Dependencies.props b/src/OrchardCore.Build/Dependencies.props index 99fb6d15ef9..a5293f20778 100644 --- a/src/OrchardCore.Build/Dependencies.props +++ b/src/OrchardCore.Build/Dependencies.props @@ -41,6 +41,7 @@ + @@ -62,5 +63,4 @@ - diff --git a/src/OrchardCore.Cms.Web/appsettings.json b/src/OrchardCore.Cms.Web/appsettings.json index faee58a0d4c..68a158527c8 100644 --- a/src/OrchardCore.Cms.Web/appsettings.json +++ b/src/OrchardCore.Cms.Web/appsettings.json @@ -120,7 +120,17 @@ // "MaxPagedCount": 500 // } //}, - + // Provides Elasticsearch Connection + //"OrchardCore_Elasticsearch": { + // "ConnectionType": "SingleNodeConnectionPool", + // "Url": "http://localhost", + // "Ports": [ 9200 ], + // "Username": "admin", + // "Password": "admin", + // "CloudId": "Orchard_Core_deployment:ZWFzdHVzMi5henVyZS5lbGFzdGljLWNsb3VkLmNvbTo0NDMkNmMxZGQ4YzAzN2=", + // "CertificateFingerprint": "75:21:E7:92:8F:D5:7A:27:06:38:8E:A4:35:FE:F5:17:D7:37:F4:DF:F0:9A:D2:C0:C4:B6:FF:EE:D1:EA:2B:A7", + // "EnableApiVersioningHeader": false + //} // WARNING: AutoSetup section given as an example for Development only, for Production use "Environment Variables" instead //"OrchardCore_AutoSetup": { // "AutoSetupPath": "", diff --git a/src/OrchardCore.Modules/OrchardCore.AdminDashboard/Migrations/dashboard-widgets.recipe.json b/src/OrchardCore.Modules/OrchardCore.AdminDashboard/Migrations/dashboard-widgets.recipe.json index b11882c75dd..5d05a994083 100644 --- a/src/OrchardCore.Modules/OrchardCore.AdminDashboard/Migrations/dashboard-widgets.recipe.json +++ b/src/OrchardCore.Modules/OrchardCore.AdminDashboard/Migrations/dashboard-widgets.recipe.json @@ -50,8 +50,7 @@ "Editor": "Wysiwyg" }, "GraphQLContentTypePartSettings": {}, - "HtmlBodyPartSettings": {}, - "ContentIndexSettings": {} + "HtmlBodyPartSettings": {} } }, { diff --git a/src/OrchardCore.Modules/OrchardCore.Alias/Indexing/AliasPartIndexHandler.cs b/src/OrchardCore.Modules/OrchardCore.Alias/Indexing/AliasPartIndexHandler.cs index eac9c5cf744..3268267d6bf 100644 --- a/src/OrchardCore.Modules/OrchardCore.Alias/Indexing/AliasPartIndexHandler.cs +++ b/src/OrchardCore.Modules/OrchardCore.Alias/Indexing/AliasPartIndexHandler.cs @@ -1,4 +1,4 @@ -using System.Threading.Tasks; +using System.Threading.Tasks; using OrchardCore.Alias.Models; using OrchardCore.Indexing; @@ -8,7 +8,7 @@ public class AliasPartIndexHandler : ContentPartIndexHandler { public override Task BuildIndexAsync(AliasPart part, BuildPartIndexContext context) { - var options = DocumentIndexOptions.Store; + var options = DocumentIndexOptions.Keyword | DocumentIndexOptions.Store; foreach (var key in context.Keys) { diff --git a/src/OrchardCore.Modules/OrchardCore.Autoroute/Indexing/AutoroutePartIndexHandler.cs b/src/OrchardCore.Modules/OrchardCore.Autoroute/Indexing/AutoroutePartIndexHandler.cs index af441641986..60a089e0490 100644 --- a/src/OrchardCore.Modules/OrchardCore.Autoroute/Indexing/AutoroutePartIndexHandler.cs +++ b/src/OrchardCore.Modules/OrchardCore.Autoroute/Indexing/AutoroutePartIndexHandler.cs @@ -10,7 +10,6 @@ public override Task BuildIndexAsync(AutoroutePart part, BuildPartIndexContext c { var options = context.Settings.ToOptions() & ~DocumentIndexOptions.Sanitize - & ~DocumentIndexOptions.Analyze ; foreach (var key in context.Keys) diff --git a/src/OrchardCore.Modules/OrchardCore.ContentFields/Fields/TextField.cs b/src/OrchardCore.Modules/OrchardCore.ContentFields/Fields/TextField.cs index 9cf3cd46dae..f85e5091fdb 100644 --- a/src/OrchardCore.Modules/OrchardCore.ContentFields/Fields/TextField.cs +++ b/src/OrchardCore.Modules/OrchardCore.ContentFields/Fields/TextField.cs @@ -1,4 +1,4 @@ -using OrchardCore.ContentManagement; +using OrchardCore.ContentManagement; namespace OrchardCore.ContentFields.Fields { diff --git a/src/OrchardCore.Modules/OrchardCore.ContentFields/Indexing/ContentPickerFieldIndexHandler.cs b/src/OrchardCore.Modules/OrchardCore.ContentFields/Indexing/ContentPickerFieldIndexHandler.cs index 75d51d54dd3..5f679c4f52e 100644 --- a/src/OrchardCore.Modules/OrchardCore.ContentFields/Indexing/ContentPickerFieldIndexHandler.cs +++ b/src/OrchardCore.Modules/OrchardCore.ContentFields/Indexing/ContentPickerFieldIndexHandler.cs @@ -8,7 +8,7 @@ public class ContentPickerFieldIndexHandler : ContentFieldIndexHandler 0) { diff --git a/src/OrchardCore.Modules/OrchardCore.ContentFields/Indexing/LocalizationSetContentPickerFieldIndexHandler.cs b/src/OrchardCore.Modules/OrchardCore.ContentFields/Indexing/LocalizationSetContentPickerFieldIndexHandler.cs index bd421254ab1..543822d5d72 100644 --- a/src/OrchardCore.Modules/OrchardCore.ContentFields/Indexing/LocalizationSetContentPickerFieldIndexHandler.cs +++ b/src/OrchardCore.Modules/OrchardCore.ContentFields/Indexing/LocalizationSetContentPickerFieldIndexHandler.cs @@ -10,7 +10,7 @@ public class LocalizationSetContentPickerFieldIndexHandler : ContentFieldIndexHa { public override Task BuildIndexAsync(LocalizationSetContentPickerField field, BuildFieldIndexContext context) { - var options = DocumentIndexOptions.Store; + var options = DocumentIndexOptions.Keyword | DocumentIndexOptions.Store; if (field.LocalizationSets.Length > 0) { diff --git a/src/OrchardCore.Modules/OrchardCore.ContentFields/Indexing/UserPickerFieldIndexHandler.cs b/src/OrchardCore.Modules/OrchardCore.ContentFields/Indexing/UserPickerFieldIndexHandler.cs index fb052234b94..68e9e44df89 100644 --- a/src/OrchardCore.Modules/OrchardCore.ContentFields/Indexing/UserPickerFieldIndexHandler.cs +++ b/src/OrchardCore.Modules/OrchardCore.ContentFields/Indexing/UserPickerFieldIndexHandler.cs @@ -8,7 +8,7 @@ public class UserPickerFieldIndexHandler : ContentFieldIndexHandler 0) { diff --git a/src/OrchardCore.Modules/OrchardCore.ContentLocalization/Indexing/LocalizationPartIndexHandler.cs b/src/OrchardCore.Modules/OrchardCore.ContentLocalization/Indexing/LocalizationPartIndexHandler.cs index 523a7481abf..d5a572d55f4 100644 --- a/src/OrchardCore.Modules/OrchardCore.ContentLocalization/Indexing/LocalizationPartIndexHandler.cs +++ b/src/OrchardCore.Modules/OrchardCore.ContentLocalization/Indexing/LocalizationPartIndexHandler.cs @@ -8,7 +8,7 @@ public class LocalizationPartIndexHandler : ContentPartIndexHandler + /// This handler provides backward compatibility with ContentIndexSettings that have been migrated to LuceneContentIndexSettings. + /// + public class LuceneRecipeEventHandler : IRecipeEventHandler + { + public RecipeExecutionContext Context { get; private set; } + + public Task RecipeStepExecutedAsync(RecipeExecutionContext context) => Task.CompletedTask; + + public Task ExecutionFailedAsync(string executionId, RecipeDescriptor descriptor) => Task.CompletedTask; + + public Task RecipeExecutedAsync(string executionId, RecipeDescriptor descriptor) => Task.CompletedTask; + + public Task RecipeExecutingAsync(string executionId, RecipeDescriptor descriptor) => Task.CompletedTask; + + public Task RecipeStepExecutingAsync(RecipeExecutionContext context) + { + if (context.Name == "ReplaceContentDefinition" || context.Name == "ContentDefinition") + { + var step = context.Step.ToObject(); + + foreach (var contentType in step.ContentTypes) + { + foreach (var partDefinition in contentType.ContentTypePartDefinitionRecords) + { + if (partDefinition.Settings != null) + { + if (partDefinition.Settings.TryGetValue("ContentIndexSettings", out var existingPartSettings) && + !partDefinition.Settings.ContainsKey("LuceneContentIndexSettings")) + { + partDefinition.Settings.Add(new JProperty("LuceneContentIndexSettings", existingPartSettings)); + } + + partDefinition.Settings.Remove("ContentIndexSettings"); + } + } + } + + foreach (var partDefinition in step.ContentParts) + { + if (partDefinition.Settings != null) + { + if (partDefinition.Settings.TryGetValue("ContentIndexSettings", out var existingPartSettings) && + !partDefinition.Settings.ContainsKey("LuceneContentIndexSettings")) + { + partDefinition.Settings.Add(new JProperty("LuceneContentIndexSettings", existingPartSettings)); + } + + partDefinition.Settings.Remove("ContentIndexSettings"); + + foreach (var fieldDefinition in partDefinition.ContentPartFieldDefinitionRecords) + { + if (fieldDefinition.Settings != null) + { + if (fieldDefinition.Settings.TryGetValue("ContentIndexSettings", out var existingFieldSettings) && + !fieldDefinition.Settings.ContainsKey("LuceneContentIndexSettings")) + { + fieldDefinition.Settings.Add(new JProperty("LuceneContentIndexSettings", existingFieldSettings)); + } + + fieldDefinition.Settings.Remove("ContentIndexSettings"); + } + } + } + } + + context.Step = JObject.FromObject(step); + } + + return Task.CompletedTask; + } + + private class ContentDefinitionStepModel + { + public string Name { get; set; } + public ContentTypeDefinitionRecord[] ContentTypes { get; set; } = Array.Empty(); + public ContentPartDefinitionRecord[] ContentParts { get; set; } = Array.Empty(); + } + } +} diff --git a/src/OrchardCore.Modules/OrchardCore.ContentTypes/Startup.cs b/src/OrchardCore.Modules/OrchardCore.ContentTypes/Startup.cs index 83191781047..028324999b2 100644 --- a/src/OrchardCore.Modules/OrchardCore.ContentTypes/Startup.cs +++ b/src/OrchardCore.Modules/OrchardCore.ContentTypes/Startup.cs @@ -15,6 +15,7 @@ using OrchardCore.Mvc.Core.Utilities; using OrchardCore.Navigation; using OrchardCore.Recipes; +using OrchardCore.Recipes.Events; using OrchardCore.Security.Permissions; namespace OrchardCore.ContentTypes @@ -47,6 +48,8 @@ public override void ConfigureServices(IServiceCollection services) services.AddRecipeExecutionStep(); services.AddRecipeExecutionStep(); services.AddRecipeExecutionStep(); + + services.AddTransient(); } public override void Configure(IApplicationBuilder builder, IEndpointRouteBuilder routes, IServiceProvider serviceProvider) diff --git a/src/OrchardCore.Modules/OrchardCore.Contents/Indexing/AspectsContentIndexHandler.cs b/src/OrchardCore.Modules/OrchardCore.Contents/Indexing/AspectsContentIndexHandler.cs index 213860f2638..375ca1a40a1 100644 --- a/src/OrchardCore.Modules/OrchardCore.Contents/Indexing/AspectsContentIndexHandler.cs +++ b/src/OrchardCore.Modules/OrchardCore.Contents/Indexing/AspectsContentIndexHandler.cs @@ -24,23 +24,25 @@ public async Task BuildIndexAsync(BuildIndexContext context) context.DocumentIndex.Set( IndexingConstants.BodyAspectBodyKey, body.Body, - DocumentIndexOptions.Analyze | DocumentIndexOptions.Sanitize); + DocumentIndexOptions.Sanitize); } context.DocumentIndex.Set( IndexingConstants.DisplayTextAnalyzedKey, context.ContentItem.DisplayText, - DocumentIndexOptions.Analyze | DocumentIndexOptions.Sanitize); + DocumentIndexOptions.Sanitize); + // We need to store because of ContentPickerResultProvider(s) context.DocumentIndex.Set( - IndexingConstants.DisplayTextKey, + IndexingConstants.DisplayTextKey + IndexingConstants.KeywordKey, context.ContentItem.DisplayText, - DocumentIndexOptions.Store); + DocumentIndexOptions.Keyword | DocumentIndexOptions.Store); + // We need to store because of ContentPickerResultProvider(s) context.DocumentIndex.Set( IndexingConstants.DisplayTextNormalizedKey, context.ContentItem.DisplayText?.ReplaceDiacritics().ToLower(), - DocumentIndexOptions.Store); + DocumentIndexOptions.Keyword | DocumentIndexOptions.Store); } } } diff --git a/src/OrchardCore.Modules/OrchardCore.Contents/Indexing/ContentItemIndexCoordinator.cs b/src/OrchardCore.Modules/OrchardCore.Contents/Indexing/ContentItemIndexCoordinator.cs index cef19857349..ae8b1ed789f 100644 --- a/src/OrchardCore.Modules/OrchardCore.Contents/Indexing/ContentItemIndexCoordinator.cs +++ b/src/OrchardCore.Modules/OrchardCore.Contents/Indexing/ContentItemIndexCoordinator.cs @@ -49,7 +49,9 @@ public async Task BuildIndexAsync(BuildIndexContext context) var partActivator = _contentPartFactory.GetTypeActivator(partTypeName); var part = (ContentPart)context.ContentItem.Get(partActivator.Type, partName); - var typePartIndexSettings = contentTypePartDefinition.GetSettings(); + var contentTypePartDefinitionMethod = contentTypePartDefinition.GetType().GetMethod("GetSettings"); + var contentTypePartDefinitionGeneric = contentTypePartDefinitionMethod.MakeGenericMethod(context.Settings.GetType()); + var typePartIndexSettings = (IContentIndexSettings)contentTypePartDefinitionGeneric.Invoke(contentTypePartDefinition, null); // Skip this part if it's not included in the index and it's not the default type part if (contentTypeDefinition.Name != partTypeName && !typePartIndexSettings.Included) @@ -63,7 +65,9 @@ await _partIndexHandlers.InvokeAsync((handler, part, contentTypePartDefinition, foreach (var contentPartFieldDefinition in contentTypePartDefinition.PartDefinition.Fields) { - var partFieldIndexSettings = contentPartFieldDefinition.GetSettings(); + var contentPartFieldDefinitionMethod = contentPartFieldDefinition.GetType().GetMethod("GetSettings"); + var contentPartFieldDefinitionGeneric = contentPartFieldDefinitionMethod.MakeGenericMethod(context.Settings.GetType()); + var partFieldIndexSettings = (IContentIndexSettings)contentPartFieldDefinitionGeneric.Invoke(contentPartFieldDefinition, null); if (!partFieldIndexSettings.Included) { diff --git a/src/OrchardCore.Modules/OrchardCore.Contents/Indexing/DefaultContentIndexHandler.cs b/src/OrchardCore.Modules/OrchardCore.Contents/Indexing/DefaultContentIndexHandler.cs index e4d36ed0a18..4c987038256 100644 --- a/src/OrchardCore.Modules/OrchardCore.Contents/Indexing/DefaultContentIndexHandler.cs +++ b/src/OrchardCore.Modules/OrchardCore.Contents/Indexing/DefaultContentIndexHandler.cs @@ -7,55 +7,46 @@ public class DefaultContentIndexHandler : IContentItemIndexHandler { public Task BuildIndexAsync(BuildIndexContext context) { - context.DocumentIndex.Set( - IndexingConstants.ContentItemIdKey, - context.ContentItem.ContentItemId, - DocumentIndexOptions.Store); - - context.DocumentIndex.Set( - IndexingConstants.ContentItemVersionIdKey, - context.ContentItem.ContentItemVersionId, - DocumentIndexOptions.Store); - context.DocumentIndex.Set( IndexingConstants.ContentTypeKey, context.ContentItem.ContentType, - DocumentIndexOptions.Store); + DocumentIndexOptions.Keyword | DocumentIndexOptions.Store); context.DocumentIndex.Set( IndexingConstants.CreatedUtcKey, context.ContentItem.CreatedUtc, - DocumentIndexOptions.Store); + DocumentIndexOptions.Keyword | DocumentIndexOptions.Store); context.DocumentIndex.Set( IndexingConstants.LatestKey, context.ContentItem.Latest, - DocumentIndexOptions.Store); + DocumentIndexOptions.Keyword | DocumentIndexOptions.Store); context.DocumentIndex.Set( IndexingConstants.OwnerKey, context.ContentItem.Owner, - DocumentIndexOptions.Store); + DocumentIndexOptions.Keyword | DocumentIndexOptions.Store); context.DocumentIndex.Set( IndexingConstants.AuthorKey, context.ContentItem.Author, - DocumentIndexOptions.Store); + DocumentIndexOptions.Keyword | DocumentIndexOptions.Store); context.DocumentIndex.Set( IndexingConstants.ModifiedUtcKey, context.ContentItem.ModifiedUtc, - DocumentIndexOptions.Store); + DocumentIndexOptions.Keyword | DocumentIndexOptions.Store); + // We need to store because of ContentPickerResultProvider(s) context.DocumentIndex.Set( IndexingConstants.PublishedKey, context.ContentItem.Published, - DocumentIndexOptions.Store); + DocumentIndexOptions.Keyword | DocumentIndexOptions.Store); context.DocumentIndex.Set( IndexingConstants.PublishedUtcKey, context.ContentItem.PublishedUtc, - DocumentIndexOptions.Store); + DocumentIndexOptions.Keyword | DocumentIndexOptions.Store); return Task.CompletedTask; } diff --git a/src/OrchardCore.Modules/OrchardCore.Contents/Indexing/FullTextContentIndexHandler.cs b/src/OrchardCore.Modules/OrchardCore.Contents/Indexing/FullTextContentIndexHandler.cs index 550ca7b64c8..a80093393d8 100644 --- a/src/OrchardCore.Modules/OrchardCore.Contents/Indexing/FullTextContentIndexHandler.cs +++ b/src/OrchardCore.Modules/OrchardCore.Contents/Indexing/FullTextContentIndexHandler.cs @@ -1,5 +1,6 @@ using System; using System.Threading.Tasks; +using Cysharp.Text; using OrchardCore.ContentManagement; using OrchardCore.ContentManagement.Models; using OrchardCore.Indexing; @@ -19,16 +20,19 @@ public async Task BuildIndexAsync(BuildIndexContext context) { var result = await _contentManager.PopulateAspectAsync(context.ContentItem); - // Index each segment as a new value to prevent from allocation a new string + using var stringBuilder = ZString.CreateStringBuilder(); + foreach (var segment in result.Segments) { - if (!String.IsNullOrEmpty(segment)) - { - context.DocumentIndex.Set( - IndexingConstants.FullTextKey, - segment, - DocumentIndexOptions.Analyze | DocumentIndexOptions.Sanitize); - } + stringBuilder.Append(segment + " "); + } + + if (!String.IsNullOrEmpty(stringBuilder.ToString())) + { + context.DocumentIndex.Set( + IndexingConstants.FullTextKey, + stringBuilder.ToString(), + DocumentIndexOptions.Sanitize); } } } diff --git a/src/OrchardCore.Modules/OrchardCore.Facebook/Migrations/Widgets/migration.recipe.json b/src/OrchardCore.Modules/OrchardCore.Facebook/Migrations/Widgets/migration.recipe.json index 994e61b55b3..dbbcbe6303f 100644 --- a/src/OrchardCore.Modules/OrchardCore.Facebook/Migrations/Widgets/migration.recipe.json +++ b/src/OrchardCore.Modules/OrchardCore.Facebook/Migrations/Widgets/migration.recipe.json @@ -245,8 +245,7 @@ }, "FacebookPluginPartSettings": { "Liquid": "\">\r\n" - }, - "ContentIndexSettings": {} + } } } ] diff --git a/src/OrchardCore.Modules/OrchardCore.Flows/Indexing/BagPartIndexHandler.cs b/src/OrchardCore.Modules/OrchardCore.Flows/Indexing/BagPartIndexHandler.cs index 4143e252da0..962afc4ee3d 100644 --- a/src/OrchardCore.Modules/OrchardCore.Flows/Indexing/BagPartIndexHandler.cs +++ b/src/OrchardCore.Modules/OrchardCore.Flows/Indexing/BagPartIndexHandler.cs @@ -40,7 +40,7 @@ public override async Task BuildIndexAsync(BagPart bagPart, BuildPartIndexContex keys.Add($"{key}.{contentItem.ContentType}"); } - var buildIndexContext = new BuildIndexContext(context.DocumentIndex, contentItem, keys); + var buildIndexContext = new BuildIndexContext(context.DocumentIndex, contentItem, keys, context.Settings); await contentItemIndexHandler.BuildIndexAsync(buildIndexContext); } diff --git a/src/OrchardCore.Modules/OrchardCore.Html/Indexing/HtmlBodyPartIndexHandler.cs b/src/OrchardCore.Modules/OrchardCore.Html/Indexing/HtmlBodyPartIndexHandler.cs index 15274868e96..85dfdb996bf 100644 --- a/src/OrchardCore.Modules/OrchardCore.Html/Indexing/HtmlBodyPartIndexHandler.cs +++ b/src/OrchardCore.Modules/OrchardCore.Html/Indexing/HtmlBodyPartIndexHandler.cs @@ -8,10 +8,7 @@ public class HtmlBodyPartIndexHandler : ContentPartIndexHandler { public override Task BuildIndexAsync(HtmlBodyPart part, BuildPartIndexContext context) { - var options = context.Settings.ToOptions() - | DocumentIndexOptions.Sanitize - | DocumentIndexOptions.Analyze - ; + var options = context.Settings.ToOptions() | DocumentIndexOptions.Sanitize; foreach (var key in context.Keys) { diff --git a/src/OrchardCore.Modules/OrchardCore.Indexing/Manifest.cs b/src/OrchardCore.Modules/OrchardCore.Indexing/Manifest.cs index 55c52bb8554..4593ff51fb5 100644 --- a/src/OrchardCore.Modules/OrchardCore.Indexing/Manifest.cs +++ b/src/OrchardCore.Modules/OrchardCore.Indexing/Manifest.cs @@ -6,5 +6,5 @@ Website = ManifestConstants.OrchardCoreWebsite, Version = ManifestConstants.OrchardCoreVersion, Description = "Provides index management for content items.", - Category = "Content" + Category = "Search" )] diff --git a/src/OrchardCore.Modules/OrchardCore.Indexing/OrchardCore.Indexing.csproj b/src/OrchardCore.Modules/OrchardCore.Indexing/OrchardCore.Indexing.csproj index 1aa7650d588..e183ee33db1 100644 --- a/src/OrchardCore.Modules/OrchardCore.Indexing/OrchardCore.Indexing.csproj +++ b/src/OrchardCore.Modules/OrchardCore.Indexing/OrchardCore.Indexing.csproj @@ -1,6 +1,7 @@ - + + true OrchardCore Indexing $(OCCMSDescription) diff --git a/src/OrchardCore.Modules/OrchardCore.Indexing/Permissions.cs b/src/OrchardCore.Modules/OrchardCore.Indexing/Permissions.cs new file mode 100644 index 00000000000..c57c081eb10 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Indexing/Permissions.cs @@ -0,0 +1,36 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using OrchardCore.Security.Permissions; + +namespace OrchardCore.Indexing +{ + public class Permissions : IPermissionProvider + { + public static readonly Permission ManageIndexes = new Permission("ManageIndexes", "Manage Indexes"); + + public Task> GetPermissionsAsync() + { + return Task.FromResult(GetPermissions()); + } + + private IEnumerable GetPermissions() + { + return new[] +{ + ManageIndexes + }; + } + + public IEnumerable GetDefaultStereotypes() + { + return new[] + { + new PermissionStereotype + { + Name = "Administrator", + Permissions = new[] { ManageIndexes } + } + }; + } + } +} diff --git a/src/OrchardCore.Modules/OrchardCore.Liquid/Indexing/LiquidPartIndexHandler.cs b/src/OrchardCore.Modules/OrchardCore.Liquid/Indexing/LiquidPartIndexHandler.cs index 88d95adfdfd..9a3fbb394a6 100644 --- a/src/OrchardCore.Modules/OrchardCore.Liquid/Indexing/LiquidPartIndexHandler.cs +++ b/src/OrchardCore.Modules/OrchardCore.Liquid/Indexing/LiquidPartIndexHandler.cs @@ -8,10 +8,7 @@ public class LiquidPartIndexHandler : ContentPartIndexHandler { public override Task BuildIndexAsync(LiquidPart part, BuildPartIndexContext context) { - var options = context.Settings.ToOptions() - | DocumentIndexOptions.Sanitize - | DocumentIndexOptions.Analyze - ; + var options = context.Settings.ToOptions() | DocumentIndexOptions.Sanitize; foreach (var key in context.Keys) { diff --git a/src/OrchardCore.Modules/OrchardCore.Lists/Indexes/ContainedPartContentIndexHandler.cs b/src/OrchardCore.Modules/OrchardCore.Lists/Indexes/ContainedPartContentIndexHandler.cs index 9316912363e..50d0e3b01ba 100644 --- a/src/OrchardCore.Modules/OrchardCore.Lists/Indexes/ContainedPartContentIndexHandler.cs +++ b/src/OrchardCore.Modules/OrchardCore.Lists/Indexes/ContainedPartContentIndexHandler.cs @@ -1,5 +1,6 @@ using System.Threading.Tasks; using OrchardCore.ContentManagement; +using OrchardCore.Contents.Indexing; using OrchardCore.Indexing; using OrchardCore.Lists.Models; @@ -7,9 +8,6 @@ namespace OrchardCore.Lists.Indexes { public class ContainedPartContentIndexHandler : IContentItemIndexHandler { - public const string ListContentItemIdKey = "Content.ContentItem.ContainedPart.ListContentItemId"; - public const string OrderKey = "Content.ContentItem.ContainedPart.Order"; - public Task BuildIndexAsync(BuildIndexContext context) { var parent = context.ContentItem.As(); @@ -20,14 +18,14 @@ public Task BuildIndexAsync(BuildIndexContext context) } context.DocumentIndex.Set( - ListContentItemIdKey, + IndexingConstants.ContainedPartKey + IndexingConstants.IdsKey, parent.ListContentItemId, - DocumentIndexOptions.Store); + DocumentIndexOptions.Keyword | DocumentIndexOptions.Store); context.DocumentIndex.Set( - OrderKey, + IndexingConstants.ContainedPartKey + IndexingConstants.OrderKey, parent.Order, - DocumentIndexOptions.Store); + DocumentIndexOptions.Keyword | DocumentIndexOptions.Store); return Task.CompletedTask; } diff --git a/src/OrchardCore.Modules/OrchardCore.Lucene/Settings/ContentIndexSettingsViewModel.cs b/src/OrchardCore.Modules/OrchardCore.Lucene/Settings/ContentIndexSettingsViewModel.cs deleted file mode 100644 index 2bdf56af91e..00000000000 --- a/src/OrchardCore.Modules/OrchardCore.Lucene/Settings/ContentIndexSettingsViewModel.cs +++ /dev/null @@ -1,9 +0,0 @@ -using OrchardCore.Indexing; - -namespace OrchardCore.Lucene.Settings -{ - public class ContentIndexSettingsViewModel - { - public ContentIndexSettings ContentIndexSettings { get; set; } - } -} diff --git a/src/OrchardCore.Modules/OrchardCore.Lucene/Settings/TypeIndexSettings.cs b/src/OrchardCore.Modules/OrchardCore.Lucene/Settings/TypeIndexSettings.cs deleted file mode 100644 index b22e2beceb7..00000000000 --- a/src/OrchardCore.Modules/OrchardCore.Lucene/Settings/TypeIndexSettings.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System.Collections.Generic; - -namespace OrchardCore.Indexing.Settings -{ - /// - /// Represents the indexing settings for a content type. - /// - public class TypeIndexSettings - { - /// - /// The list of indexes that this type should be included into. - /// - public List Indexes { get; set; } - } - - public class TypeIndexEntry - { - public const string Published = "published"; - public const string Latest = "latest"; - - public string Name { get; set; } - public string Version { get; set; } - } -} diff --git a/src/OrchardCore.Modules/OrchardCore.Lucene/Views/ContentIndexSettings.Edit.cshtml b/src/OrchardCore.Modules/OrchardCore.Lucene/Views/ContentIndexSettings.Edit.cshtml deleted file mode 100644 index 7e72e28c74d..00000000000 --- a/src/OrchardCore.Modules/OrchardCore.Lucene/Views/ContentIndexSettings.Edit.cshtml +++ /dev/null @@ -1,24 +0,0 @@ -@model OrchardCore.Lucene.Settings.ContentIndexSettingsViewModel - - - - - @T["Include this element in the index"] - @T["Check to include the value of this element in the index."] - - - - - - @T["Stored"] - @T["Check to be able to retrieve the value from the index."] - - - - - @T["Analyzed"] - @T["Check to analyze the value as readable text."] - - - - diff --git a/src/OrchardCore.Modules/OrchardCore.Markdown/Indexing/MarkdownBodyPartIndexHandler.cs b/src/OrchardCore.Modules/OrchardCore.Markdown/Indexing/MarkdownBodyPartIndexHandler.cs index 15e0f06374b..99a2cc3069c 100644 --- a/src/OrchardCore.Modules/OrchardCore.Markdown/Indexing/MarkdownBodyPartIndexHandler.cs +++ b/src/OrchardCore.Modules/OrchardCore.Markdown/Indexing/MarkdownBodyPartIndexHandler.cs @@ -8,10 +8,7 @@ public class MarkdownBodyPartIndexHandler : ContentPartIndexHandler localizer) + { + S = localizer; + } + + public Task BuildNavigationAsync(string name, NavigationBuilder builder) + { + if (!String.Equals(name, "admin", StringComparison.OrdinalIgnoreCase)) + { + return Task.CompletedTask; + } + + builder + .Add(S["Search"], "7", search => search + .AddClass("elasticsearch").Id("Elasticsearch") + .Add(S["Indexing"], S["Indexing"].PrefixPosition(), import => import + .Add(S["Elasticsearch Indices"], S["Elasticsearch Indices"].PrefixPosition(), indexes => indexes + .Action("Index", "Admin", new { area = "OrchardCore.Search.Elasticsearch" }) + .Permission(Permissions.ManageElasticIndexes) + .LocalNav()) + .Add(S["Run Elasticsearch Query"], S["Run Elasticsearch Query"].PrefixPosition(), queries => queries + .Action("Query", "Admin", new { area = "OrchardCore.Search.Elasticsearch" }) + .Permission(Permissions.ManageElasticIndexes) + .LocalNav())) + .Add(S["Settings"], settings => settings + .Add(S["Elasticsearch"], S["Elasticsearch"].PrefixPosition(), entry => entry + .Action("Index", "Admin", new { area = "OrchardCore.Settings", groupId = ElasticSettingsDisplayDriver.GroupId }) + .Permission(Permissions.ManageElasticIndexes) + .LocalNav() + ))); + + return Task.CompletedTask; + } + } +} diff --git a/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Controllers/AdminController.cs b/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Controllers/AdminController.cs new file mode 100644 index 00000000000..180054a39b3 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Controllers/AdminController.cs @@ -0,0 +1,522 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; +using System.Linq; +using System.Text.Encodings.Web; +using System.Threading.Tasks; +using Fluid; +using Fluid.Values; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Localization; +using Microsoft.AspNetCore.Mvc.Rendering; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.Localization; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using OrchardCore.ContentManagement.Metadata; +using OrchardCore.DisplayManagement; +using OrchardCore.DisplayManagement.Notify; +using OrchardCore.Environment.Shell; +using OrchardCore.Liquid; +using OrchardCore.Navigation; +using OrchardCore.Routing; +using OrchardCore.Search.Elasticsearch.Core.Models; +using OrchardCore.Search.Elasticsearch.Core.Services; +using OrchardCore.Search.Elasticsearch.ViewModels; +using OrchardCore.Settings; +using YesSql; + +namespace OrchardCore.Search.Elasticsearch +{ + public class AdminController : Controller + { + private readonly ISession _session; + private readonly ISiteService _siteService; + private readonly ILiquidTemplateManager _liquidTemplateManager; + private readonly IContentDefinitionManager _contentDefinitionManager; + private readonly IAuthorizationService _authorizationService; + private readonly IElasticQueryService _queryService; + private readonly ElasticIndexManager _elasticIndexManager; + private readonly ElasticIndexingService _elasticIndexingService; + private readonly ElasticAnalyzerManager _elasticAnalyzerManager; + private readonly ElasticIndexSettingsService _elasticIndexSettingsService; + private readonly dynamic New; + private readonly JavaScriptEncoder _javaScriptEncoder; + private readonly IStringLocalizer S; + private readonly IHtmlLocalizer H; + private readonly INotifier _notifier; + private readonly ILogger _logger; + private readonly IOptions _templateOptions; + private readonly string _indexPrefix; + + public AdminController( + ISession session, + ISiteService siteService, + ILiquidTemplateManager liquidTemplateManager, + IContentDefinitionManager contentDefinitionManager, + IAuthorizationService authorizationService, + IElasticQueryService queryService, + ElasticIndexManager elasticIndexManager, + ElasticIndexingService elasticIndexingService, + ElasticAnalyzerManager elasticAnalyzerManager, + ElasticIndexSettingsService elasticIndexSettingsService, + IShapeFactory shapeFactory, + JavaScriptEncoder javaScriptEncoder, + IStringLocalizer stringLocalizer, + IHtmlLocalizer htmlLocalizer, + INotifier notifier, + ILogger logger, + IOptions templateOptions, + ShellSettings shellSettings + ) + { + _session = session; + _siteService = siteService; + _liquidTemplateManager = liquidTemplateManager; + _contentDefinitionManager = contentDefinitionManager; + _authorizationService = authorizationService; + _queryService = queryService; + _elasticIndexManager = elasticIndexManager; + _elasticIndexingService = elasticIndexingService; + _elasticAnalyzerManager = elasticAnalyzerManager; + _elasticIndexSettingsService = elasticIndexSettingsService; + New = shapeFactory; + _javaScriptEncoder = javaScriptEncoder; + S = stringLocalizer; + H = htmlLocalizer; + _notifier = notifier; + _logger = logger; + _templateOptions = templateOptions; + _indexPrefix = shellSettings.Name.ToLowerInvariant() + "_"; + } + + public async Task Index(ContentOptions options, PagerParameters pagerParameters) + { + if (!await _authorizationService.AuthorizeAsync(User, Permissions.ManageElasticIndexes)) + { + return Forbid(); + } + + var indexes = (await _elasticIndexSettingsService.GetSettingsAsync()).Select(i => new IndexViewModel { Name = i.IndexName }); + + var siteSettings = await _siteService.GetSiteSettingsAsync(); + var pager = new Pager(pagerParameters, siteSettings.PageSize); + var count = indexes.Count(); + var results = indexes; + + if (!String.IsNullOrWhiteSpace(options.Search)) + { + results = results.Where(q => q.Name.Contains(options.Search, StringComparison.OrdinalIgnoreCase)); + } + + results = results + .Skip(pager.GetStartIndex()) + .Take(pager.PageSize).ToList(); + + // Maintain previous route data when generating page links + var routeData = new RouteData(); + var pagerShape = (await New.Pager(pager)).TotalItemCount(count).RouteData(routeData); + + var model = new AdminIndexViewModel + { + Indexes = results, + Options = options, + Pager = pagerShape + }; + + model.Options.ContentsBulkAction = new List() { + new SelectListItem() { Text = S["Reset"], Value = nameof(ContentsBulkAction.Reset) }, + new SelectListItem() { Text = S["Rebuild"], Value = nameof(ContentsBulkAction.Rebuild) }, + new SelectListItem() { Text = S["Delete"], Value = nameof(ContentsBulkAction.Remove) } + }; + + return View(model); + } + + [HttpPost, ActionName("Index")] + [FormValueRequired("submit.Filter")] + public ActionResult IndexFilterPOST(AdminIndexViewModel model) + { + return RedirectToAction("Index", new RouteValueDictionary { + { "Options.Search", model.Options.Search } + }); + } + + public async Task Edit(string indexName = null) + { + var IsCreate = String.IsNullOrWhiteSpace(indexName); + var settings = new ElasticIndexSettings(); + + if (!await _authorizationService.AuthorizeAsync(User, Permissions.ManageElasticIndexes)) + { + return Forbid(); + } + + if (!IsCreate) + { + settings = await _elasticIndexSettingsService.GetSettingsAsync(indexName); + + if (settings == null) + { + return NotFound(); + } + } + + var model = new ElasticIndexSettingsViewModel + { + IsCreate = IsCreate, + IndexName = IsCreate ? "" : settings.IndexName, + AnalyzerName = IsCreate ? "standardanalyzer" : settings.AnalyzerName, + IndexLatest = settings.IndexLatest, + Culture = settings.Culture, + Cultures = CultureInfo.GetCultures(CultureTypes.AllCultures) + .Select(x => new SelectListItem { Text = x.Name + " (" + x.DisplayName + ")", Value = x.Name }).Prepend(new SelectListItem { Text = S["Any culture"], Value = "any" }), + Analyzers = _elasticAnalyzerManager.GetAnalyzers() + .Select(x => new SelectListItem { Text = x.Name, Value = x.Name }), + IndexedContentTypes = IsCreate ? _contentDefinitionManager.ListTypeDefinitions() + .Select(x => x.Name).ToArray() : settings.IndexedContentTypes, + StoreSourceData = settings.StoreSourceData + }; + + return View(model); + } + + [HttpPost, ActionName("Edit")] + public async Task EditPost(ElasticIndexSettingsViewModel model, string[] indexedContentTypes) + { + if (!await _authorizationService.AuthorizeAsync(User, Permissions.ManageElasticIndexes)) + { + return Forbid(); + } + + ValidateModel(model); + + if (model.IsCreate) + { + if (await _elasticIndexManager.Exists(model.IndexName)) + { + ModelState.AddModelError(nameof(ElasticIndexSettingsViewModel.IndexName), S["An index named {0} already exists.", _indexPrefix + model.IndexName]); + } + } + else + { + if (!await _elasticIndexManager.Exists(model.IndexName)) + { + ModelState.AddModelError(nameof(ElasticIndexSettingsViewModel.IndexName), S["An index named {0} doesn't exist.", _indexPrefix + model.IndexName]); + } + } + + if (!ModelState.IsValid) + { + model.Cultures = CultureInfo.GetCultures(CultureTypes.AllCultures) + .Select(x => new SelectListItem { Text = x.Name + " (" + x.DisplayName + ")", Value = x.Name }).Prepend(new SelectListItem { Text = S["Any culture"], Value = "any" }); + model.Analyzers = _elasticAnalyzerManager.GetAnalyzers() + .Select(x => new SelectListItem { Text = x.Name, Value = x.Name }); + return View(model); + } + + if (model.IsCreate) + { + try + { + var settings = new ElasticIndexSettings { IndexName = model.IndexName, AnalyzerName = model.AnalyzerName, IndexLatest = model.IndexLatest, IndexedContentTypes = indexedContentTypes, Culture = model.Culture ?? "", StoreSourceData = model.StoreSourceData }; + + // We call Rebuild in order to reset the index state cursor too in case the same index + // name was also used previously. + await _elasticIndexingService.CreateIndexAsync(settings); + } + catch (Exception e) + { + await _notifier.ErrorAsync(H["An error occurred while creating the index."]); + _logger.LogError(e, "An error occurred while creating an index."); + return View(model); + } + + await _notifier.SuccessAsync(H["Index {0} created successfully.", _indexPrefix + model.IndexName]); + } + else + { + try + { + var settings = new ElasticIndexSettings { IndexName = model.IndexName, AnalyzerName = model.AnalyzerName, IndexLatest = model.IndexLatest, IndexedContentTypes = indexedContentTypes, Culture = model.Culture ?? "", StoreSourceData = model.StoreSourceData }; + + await _elasticIndexingService.UpdateIndexAsync(settings); + } + catch (Exception e) + { + await _notifier.ErrorAsync(H["An error occurred while editing the index."]); + _logger.LogError(e, "An error occurred while editing an index."); + return View(model); + } + + await _notifier.SuccessAsync(H["Index {0} modified successfully, please consider doing a rebuild on the index.", _indexPrefix + model.IndexName]); + } + + return RedirectToAction("Index"); + } + + [HttpPost] + public async Task Reset(string id) + { + if (!await _authorizationService.AuthorizeAsync(User, Permissions.ManageElasticIndexes)) + { + return Forbid(); + } + + if (!await _elasticIndexManager.Exists(id)) + { + return NotFound(); + } + + await _elasticIndexingService.ResetIndexAsync(id); + await _elasticIndexingService.ProcessContentItemsAsync(id); + + await _notifier.SuccessAsync(H["Index {0} reset successfully.", id]); + + return RedirectToAction("Index"); + } + + [HttpPost] + public async Task Rebuild(string id) + { + if (!await _authorizationService.AuthorizeAsync(User, Permissions.ManageElasticIndexes)) + { + return Forbid(); + } + + if (!await _elasticIndexManager.Exists(id)) + { + return NotFound(); + } + + await _elasticIndexingService.RebuildIndexAsync(await _elasticIndexSettingsService.GetSettingsAsync(id)); + await _elasticIndexingService.ProcessContentItemsAsync(id); + + await _notifier.SuccessAsync(H["Index {0} rebuilt successfully.", id]); + + return RedirectToAction("Index"); + } + + [HttpPost] + public async Task Delete(ElasticIndexSettingsViewModel model) + { + if (!await _authorizationService.AuthorizeAsync(User, Permissions.ManageElasticIndexes)) + { + return Forbid(); + } + + if (!await _elasticIndexManager.Exists(model.IndexName)) + { + await _notifier.SuccessAsync(H["Index not found on Elasticsearch server.", _indexPrefix + model.IndexName]); + return RedirectToAction("Index"); + } + + try + { + await _elasticIndexingService.DeleteIndexAsync(model.IndexName); + + await _notifier.SuccessAsync(H["Index {0} deleted successfully.", _indexPrefix + model.IndexName]); + } + catch (Exception e) + { + await _notifier.ErrorAsync(H["An error occurred while deleting the index."]); + _logger.LogError(e, "An error occurred while deleting the index {indexname}", _indexPrefix + model.IndexName); + } + + return RedirectToAction("Index"); + } + + [HttpPost] + public async Task ForceDelete(ElasticIndexSettingsViewModel model) + { + if (!await _authorizationService.AuthorizeAsync(User, Permissions.ManageElasticIndexes)) + { + return Forbid(); + } + + try + { + await _elasticIndexingService.DeleteIndexAsync(model.IndexName); + + await _notifier.SuccessAsync(H["Index {0} deleted successfully.", _indexPrefix + model.IndexName]); + } + catch (Exception e) + { + await _notifier.ErrorAsync(H["An error occurred while deleting the index."]); + _logger.LogError(e, "An error occurred while deleting the index {indexName}", _indexPrefix + model.IndexName); + } + + return RedirectToAction("Index"); + } + + public async Task Mappings(string indexName) + { + var mappings = await _elasticIndexManager.GetIndexMappings(indexName); + var formattedJson = JValue.Parse(mappings).ToString(Formatting.Indented); + return View(new MappingsViewModel { IndexName = _indexPrefix + indexName, Mappings = formattedJson }); + } + + public async Task SyncSettings() + { + if (!await _authorizationService.AuthorizeAsync(User, Permissions.ManageElasticIndexes)) + { + return Forbid(); + } + + await _elasticIndexingService.SyncSettings(); + + return RedirectToAction("index"); + } + + public Task Query(string indexName, string query) + { + query = String.IsNullOrWhiteSpace(query) ? "" : System.Text.Encoding.UTF8.GetString(Convert.FromBase64String(query)); + return Query(new AdminQueryViewModel { IndexName = indexName, DecodedQuery = query }); + } + + [HttpPost] + public async Task Query(AdminQueryViewModel model) + { + if (!await _authorizationService.AuthorizeAsync(User, Permissions.ManageElasticIndexes)) + { + return Forbid(); + } + + model.Indices = (await _elasticIndexSettingsService.GetSettingsAsync()).Select(x => x.IndexName).ToArray(); + + // Can't query if there are no indices + if (model.Indices.Length == 0) + { + return RedirectToAction("Index"); + } + + if (String.IsNullOrEmpty(model.IndexName)) + { + model.IndexName = model.Indices[0]; + } + + if (!await _elasticIndexManager.Exists(model.IndexName)) + { + return NotFound(); + } + + if (String.IsNullOrWhiteSpace(model.DecodedQuery)) + { + return View(model); + } + + if (String.IsNullOrEmpty(model.Parameters)) + { + model.Parameters = "{ }"; + } + + var stopwatch = new Stopwatch(); + stopwatch.Start(); + + var parameters = JsonConvert.DeserializeObject>(model.Parameters); + var tokenizedContent = await _liquidTemplateManager.RenderStringAsync(model.DecodedQuery, _javaScriptEncoder, parameters.Select(x => new KeyValuePair(x.Key, FluidValue.Create(x.Value, _templateOptions.Value)))); + + try + { + var elasticTopDocs = await _queryService.SearchAsync(model.IndexName, tokenizedContent); + + if (elasticTopDocs != null) + { + model.Documents = elasticTopDocs.TopDocs.Where(x => x != null); + model.Fields = elasticTopDocs.Fields; + model.Count = elasticTopDocs.Count; + } + } + catch (Exception e) + { + _logger.LogError(e, "Error while executing query"); + ModelState.AddModelError(nameof(model.DecodedQuery), S["Invalid query : {0}", e.Message]); + } + + stopwatch.Stop(); + model.Elapsed = stopwatch.Elapsed; + return View(model); + } + + [HttpPost, ActionName("Index")] + [FormValueRequired("submit.BulkAction")] + public async Task IndexPost(ContentOptions options, IEnumerable itemIds) + { + if (!await _authorizationService.AuthorizeAsync(User, Permissions.ManageElasticIndexes)) + { + return Forbid(); + } + + if (itemIds?.Count() > 0) + { + var elasticIndexSettings = await _elasticIndexSettingsService.GetSettingsAsync(); + var checkedContentItems = elasticIndexSettings.Where(x => itemIds.Contains(x.IndexName)); + + switch (options.BulkAction) + { + case ContentsBulkAction.None: + break; + case ContentsBulkAction.Remove: + foreach (var item in checkedContentItems) + { + await _elasticIndexingService.DeleteIndexAsync(item.IndexName); + } + await _notifier.SuccessAsync(H["Indices successfully removed."]); + break; + case ContentsBulkAction.Reset: + foreach (var item in checkedContentItems) + { + if (!await _elasticIndexManager.Exists(item.IndexName)) + { + return NotFound(); + } + + await _elasticIndexingService.ResetIndexAsync(item.IndexName); + await _elasticIndexingService.ProcessContentItemsAsync(item.IndexName); + + await _notifier.SuccessAsync(H["Index {0} reset successfully.", _indexPrefix + item.IndexName]); + } + break; + case ContentsBulkAction.Rebuild: + foreach (var item in checkedContentItems) + { + if (!await _elasticIndexManager.Exists(item.IndexName)) + { + return NotFound(); + } + + await _elasticIndexingService.RebuildIndexAsync(await _elasticIndexSettingsService.GetSettingsAsync(item.IndexName)); + await _elasticIndexingService.ProcessContentItemsAsync(item.IndexName); + await _notifier.SuccessAsync(H["Index {0} rebuilt successfully.", _indexPrefix + item.IndexName]); + } + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + + return RedirectToAction("Index"); + } + + private void ValidateModel(ElasticIndexSettingsViewModel model) + { + if (model.IndexedContentTypes == null || model.IndexedContentTypes.Length < 1) + { + ModelState.AddModelError(nameof(ElasticIndexSettingsViewModel.IndexedContentTypes), S["At least one content type selection is required."]); + } + + if (String.IsNullOrWhiteSpace(model.IndexName)) + { + ModelState.AddModelError(nameof(ElasticIndexSettingsViewModel.IndexName), S["The index name is required."]); + } + else if (ElasticIndexManager.ToSafeIndexName(model.IndexName) != model.IndexName) + { + ModelState.AddModelError(nameof(ElasticIndexSettingsViewModel.IndexName), S["The index name contains unallowed chars."]); + } + } + } +} diff --git a/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Controllers/ApiController.cs b/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Controllers/ApiController.cs new file mode 100644 index 00000000000..339dbd70ebe --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Controllers/ApiController.cs @@ -0,0 +1,101 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Newtonsoft.Json; +using OrchardCore.Search.Elasticsearch.Core.Models; +using OrchardCore.Search.Elasticsearch.Core.Services; +using OrchardCore.Search.Elasticsearch.ViewModels; + +namespace OrchardCore.Search.Elasticsearch +{ + [Route("api/elasticsearch")] + [ApiController] + [Authorize(AuthenticationSchemes = "Api"), IgnoreAntiforgeryToken, AllowAnonymous] + public class ApiController : Controller + { + private readonly IAuthorizationService _authorizationService; + private readonly ElasticQuerySource _elasticQuerySource; + + public ApiController( + IAuthorizationService authorizationService, + ElasticQuerySource elasticQuerySource) + { + _authorizationService = authorizationService; + _elasticQuerySource = elasticQuerySource; + } + + [HttpGet] + [Route("content")] + public async Task Content([FromQuery] ElasticApiQueryViewModel queryModel) + { + if (!await _authorizationService.AuthorizeAsync(User, Permissions.QueryElasticApi)) + { + return this.ChallengeOrForbid("Api"); + } + + var result = await ElasticQueryApiAsync(queryModel, returnContentItems: true); + + return new ObjectResult(result); + } + + [HttpPost] + [Route("content")] + public async Task ContentPost(ElasticApiQueryViewModel queryModel) + { + if (!await _authorizationService.AuthorizeAsync(User, Permissions.QueryElasticApi)) + { + return this.ChallengeOrForbid(); + } + + var result = await ElasticQueryApiAsync(queryModel, returnContentItems: true); + + return new ObjectResult(result); + } + + [HttpGet] + [Route("documents")] + public async Task Documents([FromQuery] ElasticApiQueryViewModel queryModel) + { + if (!await _authorizationService.AuthorizeAsync(User, Permissions.QueryElasticApi)) + { + return this.ChallengeOrForbid(); + } + + var result = await ElasticQueryApiAsync(queryModel); + + return new ObjectResult(result); + } + + [HttpPost] + [Route("documents")] + public async Task DocumentsPost(ElasticApiQueryViewModel queryModel) + { + if (!await _authorizationService.AuthorizeAsync(User, Permissions.QueryElasticApi)) + { + return this.ChallengeOrForbid("Api"); + } + + var result = await ElasticQueryApiAsync(queryModel); + + return new ObjectResult(result); + } + + private Task ElasticQueryApiAsync(ElasticApiQueryViewModel queryModel, bool returnContentItems = false) + { + var elasticQuery = new ElasticQuery + { + Index = queryModel.IndexName, + Template = queryModel.Query, + ReturnContentItems = returnContentItems + }; + + var queryParameters = queryModel.Parameters != null ? + JsonConvert.DeserializeObject>(queryModel.Parameters) + : new Dictionary(); + + var result = _elasticQuerySource.ExecuteQueryAsync(elasticQuery, queryParameters); + return result; + } + } +} diff --git a/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Controllers/SearchController.cs b/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Controllers/SearchController.cs new file mode 100644 index 00000000000..87594210e8a --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Controllers/SearchController.cs @@ -0,0 +1,233 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Localization; +using Microsoft.Extensions.Logging; +using Nest; +using OrchardCore.ContentManagement; +using OrchardCore.ContentManagement.Records; +using OrchardCore.DisplayManagement; +using OrchardCore.Entities; +using OrchardCore.Navigation; +using OrchardCore.Search.Abstractions.ViewModels; +using OrchardCore.Search.Elasticsearch.Core.Models; +using OrchardCore.Search.Elasticsearch.Core.Services; +using OrchardCore.Security.Permissions; +using OrchardCore.Settings; +using YesSql; +using YesSql.Services; + +namespace OrchardCore.Search.Elasticsearch +{ + public class SearchController : Controller + { + private readonly IAuthorizationService _authorizationService; + private readonly ISiteService _siteService; + private readonly ElasticIndexManager _elasticIndexManager; + private readonly ElasticIndexingService _elasticIndexingService; + private readonly ElasticIndexSettingsService _elasticIndexSettingsService; + private readonly ElasticAnalyzerManager _elasticAnalyzerManager; + private readonly IElasticSearchQueryService _searchQueryService; + private readonly ISession _session; + private readonly IStringLocalizer S; + private readonly IEnumerable _permissionProviders; + private readonly dynamic New; + private readonly ILogger _logger; + + public SearchController( + IAuthorizationService authorizationService, + ISiteService siteService, + ElasticIndexManager elasticIndexManager, + ElasticIndexingService elasticIndexingService, + ElasticIndexSettingsService elasticIndexSettingsService, + ElasticAnalyzerManager elasticAnalyzerManager, + IElasticSearchQueryService searchQueryService, + ISession session, + IStringLocalizer stringLocalizer, + IEnumerable permissionProviders, + IShapeFactory shapeFactory, + ILogger logger + ) + { + _authorizationService = authorizationService; + _siteService = siteService; + _elasticIndexManager = elasticIndexManager; + _elasticIndexingService = elasticIndexingService; + _elasticIndexSettingsService = elasticIndexSettingsService; + _elasticAnalyzerManager = elasticAnalyzerManager; + _searchQueryService = searchQueryService; + _session = session; + S = stringLocalizer; + _permissionProviders = permissionProviders; + New = shapeFactory; + _logger = logger; + } + + [HttpGet] + public async Task Search(SearchIndexViewModel viewModel, PagerSlimParameters pagerParameters) + { + var permissionsProvider = _permissionProviders.FirstOrDefault(x => x.GetType().FullName == "OrchardCore.Search.Elasticsearch.Permissions"); + var permissions = await permissionsProvider.GetPermissionsAsync(); + + var siteSettings = await _siteService.GetSiteSettingsAsync(); + var searchSettings = siteSettings.As(); + + if (permissions.FirstOrDefault(x => x.Name == "QueryElasticsearch" + searchSettings.SearchIndex + "Index") != null) + { + if (!await _authorizationService.AuthorizeAsync(User, permissions.FirstOrDefault(x => x.Name == "QueryElasticsearch" + searchSettings.SearchIndex + "Index"))) + { + return this.ChallengeOrForbid(); + } + } + else + { + _logger.LogInformation("Couldn't execute Elasticsearch search. The search index doesn't exist."); + return BadRequest("Elasticsearch is not configured."); + } + + if (searchSettings.SearchIndex != null && !await _elasticIndexManager.Exists(searchSettings.SearchIndex)) + { + _logger.LogInformation("Couldn't execute Elasticsearch search. The Elasticsearch search index doesn't exist."); + return BadRequest("Elasticsearch is not configured."); + } + + var elasticSettings = await _elasticIndexingService.GetElasticSettingsAsync(); + + if (elasticSettings == null || elasticSettings?.DefaultSearchFields == null) + { + _logger.LogInformation("Couldn't execute Elasticsearch search. No Elasticsearch settings was defined."); + return BadRequest("Elasticsearch is not configured."); + } + + var elasticIndexSettings = await _elasticIndexSettingsService.GetSettingsAsync(searchSettings.SearchIndex); + + if (elasticIndexSettings == null) + { + _logger.LogInformation("Couldn't execute search. No Elasticsearch index settings was defined for '{SearchIndex}' index.", searchSettings.SearchIndex); + return BadRequest($"Search index ({searchSettings.SearchIndex}) is not configured."); + } + + if (String.IsNullOrWhiteSpace(viewModel.Terms)) + { + return View(new SearchIndexViewModel + { + SearchForm = new SearchFormViewModel("Search__Form") { }, + }); + } + + var pager = new PagerSlim(pagerParameters, siteSettings.PageSize); + + // Fetch one more result than PageSize to generate "More" links + var from = 0; + var size = pager.PageSize + 1; + + if (pagerParameters.Before != null) + { + from = Convert.ToInt32(pagerParameters.Before) - pager.PageSize - 1; + size = Convert.ToInt32(pagerParameters.Before); + } + else if (pagerParameters.After != null) + { + from = Convert.ToInt32(pagerParameters.After); + size = Convert.ToInt32(pagerParameters.After) + pager.PageSize + 1; + } + + var terms = viewModel.Terms; + IList contentItemIds; + var analyzer = _elasticAnalyzerManager.CreateAnalyzer(await _elasticIndexSettingsService.GetIndexAnalyzerAsync(elasticIndexSettings.IndexName)); + + try + { + QueryContainer query = null; + + if (searchSettings.AllowElasticQueryStringQueryInSearch) + { + query = new QueryStringQuery + { + Fields = searchSettings.DefaultSearchFields, + Analyzer = analyzer.Type, + Query = terms + }; + } + else + { + query = new MultiMatchQuery + { + Fields = searchSettings.DefaultSearchFields, + Analyzer = analyzer.Type, + Query = terms + }; + } + + contentItemIds = (await _searchQueryService.ExecuteQueryAsync(searchSettings.SearchIndex, query, null, from, size)) + .ToList(); + } + catch (Exception e) + { + ModelState.AddModelError("Terms", S["Incorrect query syntax."]); + _logger.LogError(e, "Incorrect Elasticsearch search query syntax provided in search:"); + + // Return a SearchIndexViewModel without SearchResults or Pager shapes since there is an error. + return View(new SearchIndexViewModel + { + Terms = viewModel.Terms, + SearchForm = new SearchFormViewModel("Search__Form") { Terms = viewModel.Terms }, + }); + } + + // We Query database to retrieve content items. + IQuery queryDb; + + if (elasticIndexSettings.IndexLatest) + { + queryDb = _session.Query() + .Where(x => x.ContentItemId.IsIn(contentItemIds) && x.Latest == true) + .Take(pager.PageSize + 1); + } + else + { + queryDb = _session.Query() + .Where(x => x.ContentItemId.IsIn(contentItemIds) && x.Published == true) + .Take(pager.PageSize + 1); + } + + // Sort the content items by their rank in the search results returned by Elasticsearch. + var containedItems = (await queryDb.ListAsync()).OrderBy(x => contentItemIds.IndexOf(x.ContentItemId)); + + // We set the PagerSlim before and after links + if (pagerParameters.After != null || pagerParameters.Before != null) + { + if (from + 1 > 1) + { + pager.Before = (from + 1).ToString(); + } + else + { + pager.Before = null; + } + } + + if (containedItems.Count() == pager.PageSize + 1) + { + pager.After = (size - 1).ToString(); + } + else + { + pager.After = null; + } + + var model = new SearchIndexViewModel + { + Terms = viewModel.Terms, + SearchForm = new SearchFormViewModel("Search__Form") { Terms = viewModel.Terms }, + SearchResults = new SearchResultsViewModel("Search__Results") { ContentItems = containedItems.Take(pager.PageSize) }, + Pager = (await New.PagerSlim(pager)).UrlParams(new Dictionary() { { "Terms", viewModel.Terms } }) + }; + + return View(model); + } + } +} diff --git a/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Drivers/ContentPartFieldIndexSettingsDisplayDriver.cs b/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Drivers/ContentPartFieldIndexSettingsDisplayDriver.cs new file mode 100644 index 00000000000..367bfd16664 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Drivers/ContentPartFieldIndexSettingsDisplayDriver.cs @@ -0,0 +1,53 @@ +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using OrchardCore.ContentManagement.Metadata.Models; +using OrchardCore.ContentTypes.Editors; +using OrchardCore.DisplayManagement.ModelBinding; +using OrchardCore.DisplayManagement.Views; +using OrchardCore.Search.Elasticsearch.Core.Models; +using OrchardCore.Search.Elasticsearch.ViewModels; + +namespace OrchardCore.Search.Elasticsearch.Drivers +{ + public class ContentPartFieldIndexSettingsDisplayDriver : ContentPartFieldDefinitionDisplayDriver + { + private readonly IHttpContextAccessor _httpContextAccessor; + private readonly IAuthorizationService _authorizationService; + + public ContentPartFieldIndexSettingsDisplayDriver(IAuthorizationService authorizationService, IHttpContextAccessor httpContextAccessor) + { + _httpContextAccessor = httpContextAccessor; + _authorizationService = authorizationService; + } + + public override async Task EditAsync(ContentPartFieldDefinition contentPartFieldDefinition, IUpdateModel updater) + { + if (!await _authorizationService.AuthorizeAsync(_httpContextAccessor.HttpContext.User, Permissions.ManageElasticIndexes)) + { + return null; + } + + return Initialize("ElasticContentIndexSettings_Edit", model => + { + model.ElasticContentIndexSettings = contentPartFieldDefinition.GetSettings(); + }).Location("Content:10"); + } + + public override async Task UpdateAsync(ContentPartFieldDefinition contentPartFieldDefinition, UpdatePartFieldEditorContext context) + { + if (!await _authorizationService.AuthorizeAsync(_httpContextAccessor.HttpContext.User, Permissions.ManageElasticIndexes)) + { + return null; + } + + var model = new ElasticContentIndexSettingsViewModel(); + + await context.Updater.TryUpdateModelAsync(model, Prefix); + + context.Builder.WithSettings(model.ElasticContentIndexSettings); + + return await EditAsync(contentPartFieldDefinition, context.Updater); + } + } +} diff --git a/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Drivers/ContentPickerFieldElasticEditorSettingsDriver.cs b/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Drivers/ContentPickerFieldElasticEditorSettingsDriver.cs new file mode 100644 index 00000000000..c65ba173d37 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Drivers/ContentPickerFieldElasticEditorSettingsDriver.cs @@ -0,0 +1,49 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using OrchardCore.ContentManagement.Metadata.Models; +using OrchardCore.ContentTypes.Editors; +using OrchardCore.DisplayManagement.Views; +using OrchardCore.Search.Elasticsearch.Core.Services; +using OrchardCore.Search.Elasticsearch.Core.Models; + +namespace OrchardCore.Search.Elasticsearch.Drivers +{ + public class ContentPickerFieldElasticEditorSettingsDriver : ContentPartFieldDefinitionDisplayDriver + { + private readonly ElasticIndexSettingsService _elasticIndexSettingsService; + + public ContentPickerFieldElasticEditorSettingsDriver(ElasticIndexSettingsService elasticIndexSettingsService) + { + _elasticIndexSettingsService = elasticIndexSettingsService; + } + + public override IDisplayResult Edit(ContentPartFieldDefinition partFieldDefinition) + { + return Initialize("ContentPickerFieldElasticEditorSettings_Edit", async model => + { + partFieldDefinition.PopulateSettings(model); + model.Indices = (await _elasticIndexSettingsService.GetSettingsAsync()).Select(x => x.IndexName).ToArray(); + }).Location("Editor"); + } + + public override async Task UpdateAsync(ContentPartFieldDefinition partFieldDefinition, UpdatePartFieldEditorContext context) + { + if (partFieldDefinition.Editor() == "Elasticsearch") + { + var model = new ContentPickerFieldElasticEditorSettings(); + + await context.Updater.TryUpdateModelAsync(model, Prefix); + + context.Builder.WithSettings(model); + } + + return Edit(partFieldDefinition); + } + + public override bool CanHandleModel(ContentPartFieldDefinition model) + { + return String.Equals("ContentPickerField", model.FieldDefinition.Name); + } + } +} diff --git a/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Drivers/ContentTypePartIndexSettingsDisplayDriver.cs b/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Drivers/ContentTypePartIndexSettingsDisplayDriver.cs new file mode 100644 index 00000000000..f9b07c864ef --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Drivers/ContentTypePartIndexSettingsDisplayDriver.cs @@ -0,0 +1,53 @@ +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using OrchardCore.ContentManagement.Metadata.Models; +using OrchardCore.ContentTypes.Editors; +using OrchardCore.DisplayManagement.ModelBinding; +using OrchardCore.DisplayManagement.Views; +using OrchardCore.Search.Elasticsearch.Core.Models; +using OrchardCore.Search.Elasticsearch.ViewModels; + +namespace OrchardCore.Search.Elasticsearch.Drivers +{ + public class ContentTypePartIndexSettingsDisplayDriver : ContentTypePartDefinitionDisplayDriver + { + private readonly IHttpContextAccessor _httpContextAccessor; + private readonly IAuthorizationService _authorizationService; + + public ContentTypePartIndexSettingsDisplayDriver(IAuthorizationService authorizationService, IHttpContextAccessor httpContextAccessor) + { + _httpContextAccessor = httpContextAccessor; + _authorizationService = authorizationService; + } + + public override async Task EditAsync(ContentTypePartDefinition contentTypePartDefinition, IUpdateModel updater) + { + if (!await _authorizationService.AuthorizeAsync(_httpContextAccessor.HttpContext.User, Permissions.ManageElasticIndexes)) + { + return null; + } + + return Initialize("ElasticContentIndexSettings_Edit", model => + { + model.ElasticContentIndexSettings = contentTypePartDefinition.GetSettings(); + }).Location("Content:10"); + } + + public override async Task UpdateAsync(ContentTypePartDefinition contentTypePartDefinition, UpdateTypePartEditorContext context) + { + if (!await _authorizationService.AuthorizeAsync(_httpContextAccessor.HttpContext.User, Permissions.ManageElasticIndexes)) + { + return null; + } + + var model = new ElasticContentIndexSettingsViewModel(); + + await context.Updater.TryUpdateModelAsync(model, Prefix); + + context.Builder.WithSettings(model.ElasticContentIndexSettings); + + return await EditAsync(contentTypePartDefinition, context.Updater); + } + } +} diff --git a/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Drivers/ElasticIndexDeploymentStepDriver.cs b/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Drivers/ElasticIndexDeploymentStepDriver.cs new file mode 100644 index 00000000000..f90ca1bb822 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Drivers/ElasticIndexDeploymentStepDriver.cs @@ -0,0 +1,59 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using OrchardCore.Deployment; +using OrchardCore.DisplayManagement.Handlers; +using OrchardCore.DisplayManagement.ModelBinding; +using OrchardCore.DisplayManagement.Views; +using OrchardCore.Search.Elasticsearch.Core.Services; +using OrchardCore.Search.Elasticsearch.ViewModels; + +namespace OrchardCore.Search.Elasticsearch.Core.Deployment +{ + public class ElasticIndexDeploymentStepDriver : DisplayDriver + { + private readonly ElasticIndexSettingsService _elasticIndexSettingsService; + + public ElasticIndexDeploymentStepDriver(ElasticIndexSettingsService elasticIndexSettingsService) + { + _elasticIndexSettingsService = elasticIndexSettingsService; + } + + public override IDisplayResult Display(ElasticIndexDeploymentStep step) + { + return + Combine( + View("ElasticIndexDeploymentStep_Fields_Summary", step).Location("Summary", "Content"), + View("ElasticIndexDeploymentStep_Fields_Thumbnail", step).Location("Thumbnail", "Content") + ); + } + + public override IDisplayResult Edit(ElasticIndexDeploymentStep step) + { + return Initialize("ElasticIndexDeploymentStep_Fields_Edit", async model => + { + model.IncludeAll = step.IncludeAll; + model.IndexNames = step.IndexNames; + model.AllIndexNames = (await _elasticIndexSettingsService.GetSettingsAsync()).Select(x => x.IndexName).ToArray(); + }).Location("Content"); + } + + public override async Task UpdateAsync(ElasticIndexDeploymentStep step, IUpdateModel updater) + { + step.IndexNames = Array.Empty(); + + await updater.TryUpdateModelAsync(step, + Prefix, + x => x.IndexNames, + x => x.IncludeAll); + + // don't have the selected option if include all + if (step.IncludeAll) + { + step.IndexNames = Array.Empty(); + } + + return Edit(step); + } + } +} diff --git a/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Drivers/ElasticIndexRebuildDeploymentStepDriver.cs b/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Drivers/ElasticIndexRebuildDeploymentStepDriver.cs new file mode 100644 index 00000000000..62e5ed08e64 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Drivers/ElasticIndexRebuildDeploymentStepDriver.cs @@ -0,0 +1,55 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using OrchardCore.Deployment; +using OrchardCore.DisplayManagement.Handlers; +using OrchardCore.DisplayManagement.ModelBinding; +using OrchardCore.DisplayManagement.Views; +using OrchardCore.Search.Elasticsearch.Core.Services; +using OrchardCore.Search.Elasticsearch.ViewModels; + +namespace OrchardCore.Search.Elasticsearch.Core.Deployment +{ + public class ElasticIndexRebuildDeploymentStepDriver : DisplayDriver + { + private readonly ElasticIndexSettingsService _elasticIndexSettingsService; + + public ElasticIndexRebuildDeploymentStepDriver(ElasticIndexSettingsService elasticIndexSettingsService) + { + _elasticIndexSettingsService = elasticIndexSettingsService; + } + + public override IDisplayResult Display(ElasticIndexRebuildDeploymentStep step) + { + return + Combine( + View("ElasticIndexRebuildDeploymentStep_Fields_Summary", step).Location("Summary", "Content"), + View("ElasticIndexRebuildDeploymentStep_Fields_Thumbnail", step).Location("Thumbnail", "Content") + ); + } + + public override IDisplayResult Edit(ElasticIndexRebuildDeploymentStep step) + { + return Initialize("ElasticIndexRebuildDeploymentStep_Fields_Edit", async model => + { + model.IncludeAll = step.IncludeAll; + model.IndexNames = step.Indices; + model.AllIndexNames = (await _elasticIndexSettingsService.GetSettingsAsync()).Select(x => x.IndexName).ToArray(); + }).Location("Content"); + } + + public override async Task UpdateAsync(ElasticIndexRebuildDeploymentStep rebuildIndexStep, IUpdateModel updater) + { + rebuildIndexStep.Indices = Array.Empty(); + + await updater.TryUpdateModelAsync(rebuildIndexStep, Prefix, step => step.Indices, step => step.IncludeAll); + + if (rebuildIndexStep.IncludeAll) + { + rebuildIndexStep.Indices = Array.Empty(); + } + + return Edit(rebuildIndexStep); + } + } +} diff --git a/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Drivers/ElasticIndexResetDeploymentStepDriver.cs b/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Drivers/ElasticIndexResetDeploymentStepDriver.cs new file mode 100644 index 00000000000..a61146d6642 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Drivers/ElasticIndexResetDeploymentStepDriver.cs @@ -0,0 +1,55 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using OrchardCore.Deployment; +using OrchardCore.DisplayManagement.Handlers; +using OrchardCore.DisplayManagement.ModelBinding; +using OrchardCore.DisplayManagement.Views; +using OrchardCore.Search.Elasticsearch.Core.Services; +using OrchardCore.Search.Elasticsearch.ViewModels; + +namespace OrchardCore.Search.Elasticsearch.Core.Deployment +{ + public class ElasticIndexResetDeploymentStepDriver : DisplayDriver + { + private readonly ElasticIndexSettingsService _elasticIndexSettingsService; + + public ElasticIndexResetDeploymentStepDriver(ElasticIndexSettingsService elasticIndexSettingsService) + { + _elasticIndexSettingsService = elasticIndexSettingsService; + } + + public override IDisplayResult Display(ElasticIndexResetDeploymentStep step) + { + return + Combine( + View("ElasticIndexResetDeploymentStep_Fields_Summary", step).Location("Summary", "Content"), + View("ElasticIndexResetDeploymentStep_Fields_Thumbnail", step).Location("Thumbnail", "Content") + ); + } + + public override IDisplayResult Edit(ElasticIndexResetDeploymentStep step) + { + return Initialize("ElasticIndexResetDeploymentStep_Fields_Edit", async model => + { + model.IncludeAll = step.IncludeAll; + model.IndexNames = step.Indices; + model.AllIndexNames = (await _elasticIndexSettingsService.GetSettingsAsync()).Select(x => x.IndexName).ToArray(); + }).Location("Content"); + } + + public override async Task UpdateAsync(ElasticIndexResetDeploymentStep resetIndexStep, IUpdateModel updater) + { + resetIndexStep.Indices = Array.Empty(); + + await updater.TryUpdateModelAsync(resetIndexStep, Prefix, step => step.Indices, step => step.IncludeAll); + + if (resetIndexStep.IncludeAll) + { + resetIndexStep.Indices = Array.Empty(); + } + + return Edit(resetIndexStep); + } + } +} diff --git a/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Drivers/ElasticQueryDisplayDriver.cs b/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Drivers/ElasticQueryDisplayDriver.cs new file mode 100644 index 00000000000..d4065e40acc --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Drivers/ElasticQueryDisplayDriver.cs @@ -0,0 +1,76 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Extensions.Localization; +using OrchardCore.DisplayManagement.Handlers; +using OrchardCore.DisplayManagement.ModelBinding; +using OrchardCore.DisplayManagement.Views; +using OrchardCore.Queries; +using OrchardCore.Search.Elasticsearch.Core.Models; +using OrchardCore.Search.Elasticsearch.Core.Services; +using OrchardCore.Search.Elasticsearch.ViewModels; + +namespace OrchardCore.Search.Elasticsearch.Drivers +{ + public class ElasticQueryDisplayDriver : DisplayDriver + { + private readonly ElasticIndexSettingsService _elasticIndexSettingsService; + private readonly IStringLocalizer S; + + public ElasticQueryDisplayDriver( + IStringLocalizer stringLocalizer, + ElasticIndexSettingsService elasticIndexSettingsService) + { + _elasticIndexSettingsService = elasticIndexSettingsService; + S = stringLocalizer; + } + + public override IDisplayResult Display(ElasticQuery query, IUpdateModel updater) + { + return Combine( + Dynamic("ElasticQuery_SummaryAdmin", model => { model.Query = query; }).Location("Content:5"), + Dynamic("ElasticQuery_Buttons_SummaryAdmin", model => { model.Query = query; }).Location("Actions:2") + ); + } + + public override IDisplayResult Edit(ElasticQuery query, IUpdateModel updater) + { + return Initialize("ElasticQuery_Edit", async model => + { + model.Query = query.Template; + model.Index = query.Index; + model.ReturnContentItems = query.ReturnContentItems; + model.Indices = (await _elasticIndexSettingsService.GetSettingsAsync()).Select(x => x.IndexName).ToArray(); + + // Extract query from the query string if we come from the main query editor + if (String.IsNullOrEmpty(query.Template)) + { + await updater.TryUpdateModelAsync(model, "", m => m.Query); + } + }).Location("Content:5"); + } + + public override async Task UpdateAsync(ElasticQuery model, IUpdateModel updater) + { + var viewModel = new ElasticQueryViewModel(); + if (await updater.TryUpdateModelAsync(viewModel, Prefix, m => m.Query, m => m.Index, m => m.ReturnContentItems)) + { + model.Template = viewModel.Query; + model.Index = viewModel.Index; + model.ReturnContentItems = viewModel.ReturnContentItems; + } + + if (String.IsNullOrWhiteSpace(model.Template)) + { + updater.ModelState.AddModelError(nameof(model.Template), S["The query field is required"]); + } + + if (String.IsNullOrWhiteSpace(model.Index)) + { + updater.ModelState.AddModelError(nameof(model.Index), S["The index field is required"]); + } + + return Edit(model, updater); + } + } +} diff --git a/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Drivers/ElasticSettingsDeploymentStepDriver.cs b/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Drivers/ElasticSettingsDeploymentStepDriver.cs new file mode 100644 index 00000000000..53efead30c1 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Drivers/ElasticSettingsDeploymentStepDriver.cs @@ -0,0 +1,24 @@ +using OrchardCore.Deployment; +using OrchardCore.DisplayManagement.Handlers; +using OrchardCore.DisplayManagement.Views; +using OrchardCore.Search.Elasticsearch.Core.Deployment; + +namespace OrchardCore.Search.Elasticsearch.Drivers +{ + public class ElasticSettingsDeploymentStepDriver : DisplayDriver + { + public override IDisplayResult Display(ElasticSettingsDeploymentStep step) + { + return + Combine( + View("ElasticSettingsDeploymentStep_Fields_Summary", step).Location("Summary", "Content"), + View("ElasticSettingsDeploymentStep_Fields_Thumbnail", step).Location("Thumbnail", "Content") + ); + } + + public override IDisplayResult Edit(ElasticSettingsDeploymentStep step) + { + return View("ElasticSettingsDeploymentStep_Fields_Edit", step).Location("Content"); + } + } +} diff --git a/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Drivers/ElasticSettingsDisplayDriver.cs b/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Drivers/ElasticSettingsDisplayDriver.cs new file mode 100644 index 00000000000..206763da8a2 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Drivers/ElasticSettingsDisplayDriver.cs @@ -0,0 +1,75 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using OrchardCore.DisplayManagement.Entities; +using OrchardCore.DisplayManagement.Handlers; +using OrchardCore.DisplayManagement.Views; +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 +{ + public class ElasticSettingsDisplayDriver : SectionDisplayDriver + { + public const string GroupId = "elasticsearch"; + private readonly ElasticIndexSettingsService _elasticIndexSettingsService; + private readonly IHttpContextAccessor _httpContextAccessor; + private readonly IAuthorizationService _authorizationService; + + public ElasticSettingsDisplayDriver( + ElasticIndexSettingsService elasticIndexSettingsService, + IHttpContextAccessor httpContextAccessor, + IAuthorizationService authorizationService + ) + { + _elasticIndexSettingsService = elasticIndexSettingsService; + _httpContextAccessor = httpContextAccessor; + _authorizationService = authorizationService; + } + + 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 => + { + model.SearchIndex = settings.SearchIndex; + model.SearchFields = String.Join(", ", settings.DefaultSearchFields ?? new string[0]); + model.SearchIndexes = (await _elasticIndexSettingsService.GetSettingsAsync()).Select(x => x.IndexName); + model.AllowElasticQueryStringQueryInSearch = settings.AllowElasticQueryStringQueryInSearch; + }).Location("Content:2").OnGroup(GroupId); + } + + public override async Task UpdateAsync(ElasticSettings section, BuildEditorContext context) + { + var user = _httpContextAccessor.HttpContext?.User; + + if (!await _authorizationService.AuthorizeAsync(user, Permissions.ManageElasticIndexes)) + { + return null; + } + + if (context.GroupId == GroupId) + { + var model = new ElasticSettingsViewModel(); + + await context.Updater.TryUpdateModelAsync(model, Prefix); + + section.SearchIndex = model.SearchIndex; + section.DefaultSearchFields = model.SearchFields?.Split(new[] { ',', ' ' }, StringSplitOptions.RemoveEmptyEntries); + section.AllowElasticQueryStringQueryInSearch = model.AllowElasticQueryStringQueryInSearch; + } + + return await EditAsync(section, context); + } + } +} diff --git a/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/GraphQL/ElasticQueryFieldTypeProvider.cs b/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/GraphQL/ElasticQueryFieldTypeProvider.cs new file mode 100644 index 00000000000..f52c8671a7c --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/GraphQL/ElasticQueryFieldTypeProvider.cs @@ -0,0 +1,202 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using GraphQL; +using GraphQL.Types; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using OrchardCore.Apis.GraphQL; +using OrchardCore.Apis.GraphQL.Resolvers; +using OrchardCore.ContentManagement.GraphQL.Queries; +using OrchardCore.Queries; +using OrchardCore.Search.Elasticsearch.Core.Models; + +namespace OrchardCore.Search.Elasticsearch.GraphQL.Queries +{ + public class ElasticQueryFieldTypeProvider : ISchemaBuilder + { + private readonly IHttpContextAccessor _httpContextAccessor; + private readonly ILogger _logger; + + public ElasticQueryFieldTypeProvider(IHttpContextAccessor httpContextAccessor, ILogger logger) + { + _httpContextAccessor = httpContextAccessor; + _logger = logger; + } + + public Task GetIdentifierAsync() + { + var queryManager = _httpContextAccessor.HttpContext.RequestServices.GetService(); + return queryManager.GetIdentifierAsync(); + } + + public async Task BuildAsync(ISchema schema) + { + var queryManager = _httpContextAccessor.HttpContext.RequestServices.GetService(); + + var queries = await queryManager.ListQueriesAsync(); + + foreach (var query in queries.OfType()) + { + if (String.IsNullOrWhiteSpace(query.Schema)) + continue; + + var name = query.Name; + + try + { + var querySchema = JObject.Parse(query.Schema); + if (!querySchema.ContainsKey("type")) + { + _logger.LogError("The Query '{Name}' schema is invalid, the 'type' property was not found.", name); + continue; + } + var type = querySchema["type"].ToString(); + FieldType fieldType; + + var fieldTypeName = querySchema["fieldTypeName"]?.ToString() ?? query.Name; + + if (query.ReturnContentItems && + type.StartsWith("ContentItem/", StringComparison.OrdinalIgnoreCase)) + { + var contentType = type.Remove(0, 12); + fieldType = BuildContentTypeFieldType(schema, contentType, query, fieldTypeName); + } + else + { + fieldType = BuildSchemaBasedFieldType(query, querySchema, fieldTypeName); + } + + if (fieldType != null) + { + schema.Query.AddField(fieldType); + } + } + catch (Exception e) + { + _logger.LogError(e, "The Query '{Name}' has an invalid schema.", name); + } + } + } + + private FieldType BuildSchemaBasedFieldType(ElasticQuery query, JToken querySchema, string fieldTypeName) + { + var properties = querySchema["properties"]; + + if (properties == null) + { + return null; + } + + var typetype = new ObjectGraphType + { + Name = fieldTypeName + }; + + foreach (JProperty child in properties.Children()) + { + var name = child.Name; + var nameLower = name.Replace('.', '_'); + var type = child.Value["type"].ToString(); + var description = child.Value["description"]?.ToString(); + + if (type == "string") + { + var field = typetype.Field( + typeof(StringGraphType), + nameLower, + description: description, + resolve: context => + { + var source = context.Source; + return source[context.FieldDefinition.Metadata["Name"].ToString()].ToObject(); + }); + field.Metadata.Add("Name", name); + } + else if (type == "integer") + { + var field = typetype.Field( + typeof(IntGraphType), + nameLower, + description: description, + resolve: context => + { + var source = context.Source; + return source[context.FieldDefinition.Metadata["Name"].ToString()].ToObject(); + }); + field.Metadata.Add("Name", name); + } + } + + var fieldType = new FieldType + { + Arguments = new QueryArguments( + new QueryArgument { Name = "parameters" } + ), + + Name = fieldTypeName, + Description = "Represents the " + query.Source + " Query : " + query.Name, + ResolvedType = new ListGraphType(typetype), + Resolver = new LockedAsyncFieldResolver(async context => + { + var queryManager = context.RequestServices.GetService(); + var iquery = await queryManager.GetQueryAsync(query.Name); + + var parameters = context.GetArgument("parameters"); + + var queryParameters = parameters != null ? + JsonConvert.DeserializeObject>(parameters) + : new Dictionary(); + + var result = await queryManager.ExecuteQueryAsync(iquery, queryParameters); + return result.Items; + }), + Type = typeof(ListGraphType>) + }; + + return fieldType; + } + + private FieldType BuildContentTypeFieldType(ISchema schema, string contentType, ElasticQuery query, string fieldTypeName) + { + var typetype = schema.Query.Fields.OfType().FirstOrDefault(x => x.Name == contentType); + + if (typetype == null) + { + return null; + } + + var fieldType = new FieldType + { + Arguments = new QueryArguments( + new QueryArgument { Name = "parameters" } + ), + + Name = fieldTypeName, + Description = "Represents the " + query.Source + " Query : " + query.Name, + ResolvedType = typetype.ResolvedType, + Resolver = new LockedAsyncFieldResolver(async context => + { + var queryManager = context.RequestServices.GetService(); + var iquery = await queryManager.GetQueryAsync(query.Name); + + var parameters = context.GetArgument("parameters"); + + var queryParameters = parameters != null ? + JsonConvert.DeserializeObject>(parameters) + : new Dictionary(); + + var result = await queryManager.ExecuteQueryAsync(iquery, queryParameters); + return result.Items; + }), + Type = typetype.Type + }; + + return fieldType; + } + } +} diff --git a/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/GraphQL/Startup.cs b/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/GraphQL/Startup.cs new file mode 100644 index 00000000000..9f4f883774f --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/GraphQL/Startup.cs @@ -0,0 +1,19 @@ +using Microsoft.Extensions.DependencyInjection; +using OrchardCore.Apis.GraphQL; +using OrchardCore.Modules; +using OrchardCore.Search.Elasticsearch.GraphQL.Queries; + +namespace OrchardCore.Search.Elasticsearch.GraphQL +{ + /// + /// These services are registered on the tenant service collection + /// + [RequireFeatures("OrchardCore.Apis.GraphQL")] + public class Startup : StartupBase + { + public override void ConfigureServices(IServiceCollection services) + { + services.AddSingleton(); + } + } +} diff --git a/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Manifest.cs b/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Manifest.cs new file mode 100644 index 00000000000..11176b5388f --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Manifest.cs @@ -0,0 +1,36 @@ +using OrchardCore.Modules.Manifest; + +[assembly: Module( + Name = "Elasticsearch", + Author = ManifestConstants.OrchardCoreTeam, + Website = ManifestConstants.OrchardCoreWebsite, + Version = ManifestConstants.OrchardCoreVersion +)] + +[assembly: Feature( + Id = "OrchardCore.Search.Elasticsearch", + Name = "Elasticsearch", + Description = "Creates Elasticsearch indexes to support search scenarios, introduces a preconfigured container-enabled content type.", + Dependencies = new[] + { + "OrchardCore.Indexing", + "OrchardCore.ContentTypes" + }, + Category = "Search" +)] + +[assembly: Feature( + Id = "OrchardCore.Search.Elasticsearch.Worker", + Name = "Elasticsearch Worker", + Description = "Provides a background task to keep indices in sync with other instances.", + Dependencies = new[] { "OrchardCore.Search.Elasticsearch" }, + Category = "Search" +)] + +[assembly: Feature( + Id = "OrchardCore.Search.Elasticsearch.ContentPicker", + Name = "Elasticsearch Content Picker", + Description = "Provides a Elasticsearch content picker field editor.", + Dependencies = new[] { "OrchardCore.Search.Elasticsearch", "OrchardCore.ContentFields" }, + Category = "Search" +)] diff --git a/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/OrchardCore.Search.Elasticsearch.csproj b/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/OrchardCore.Search.Elasticsearch.csproj new file mode 100644 index 00000000000..ba3e2be38ae --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/OrchardCore.Search.Elasticsearch.csproj @@ -0,0 +1,32 @@ + + + + true + + OrchardCore Elasticsearch + + $(OCCMSDescription) + + Creates Elasticsearch Search indexes to support search scenarios, introduces a preconfigured container-enabled content type. + $(PackageTags) OrchardCoreCMS + + + + + + + + + + + + + + + + + + + + + diff --git a/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Permissions.cs b/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Permissions.cs new file mode 100644 index 00000000000..eef9f790d72 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Permissions.cs @@ -0,0 +1,53 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using OrchardCore.Search.Elasticsearch.Core.Services; +using OrchardCore.Security.Permissions; + +namespace OrchardCore.Search.Elasticsearch +{ + public class Permissions : IPermissionProvider + { + private readonly ElasticIndexSettingsService _elasticIndexSettingsService; + + public static readonly Permission ManageElasticIndexes = new Permission("ManageElasticIndexes", "Manage Elasticsearch Indexes"); + public static readonly Permission QueryElasticApi = new Permission("QueryElasticsearchApi", "Query Elasticsearch Api", new[] { ManageElasticIndexes }); + + public Permissions(ElasticIndexSettingsService elasticIndexSettingsService) + { + _elasticIndexSettingsService = elasticIndexSettingsService; + } + + public async Task> GetPermissionsAsync() + { + var elasticIndexSettings = await _elasticIndexSettingsService.GetSettingsAsync(); + var result = new List(); + foreach (var index in elasticIndexSettings) + { + var permission = new Permission("QueryElasticsearch" + index.IndexName + "Index", "Query Elasticsearch " + index.IndexName + " Index", new[] { ManageElasticIndexes }); + result.Add(permission); + } + + result.Add(ManageElasticIndexes); + result.Add(QueryElasticApi); + + return result; + } + + public IEnumerable GetDefaultStereotypes() + { + return new[] + { + new PermissionStereotype + { + Name = "Administrator", + Permissions = new[] { ManageElasticIndexes } + }, + new PermissionStereotype + { + Name = "Editor", + Permissions = new[] { QueryElasticApi } + } + }; + } + } +} diff --git a/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Properties/AssemblyInfo.cs b/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Properties/AssemblyInfo.cs new file mode 100644 index 00000000000..ea31636f5e8 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Properties/AssemblyInfo.cs @@ -0,0 +1,18 @@ +using System.Reflection; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("OrchardCore.Search.Elasticsearch")] +[assembly: AssemblyTrademark("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("23b628d8-4dcf-4ee1-aea2-8cff6c8b90b0")] diff --git a/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Providers/ElasticSearchProvider.cs b/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Providers/ElasticSearchProvider.cs new file mode 100644 index 00000000000..bbcde75abbe --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Providers/ElasticSearchProvider.cs @@ -0,0 +1,15 @@ +using OrchardCore.Search.Abstractions; + +namespace OrchardCore.Search.Elasticsearch.Providers +{ + /// + /// Creates an Elasticsearch SearchProvider. + /// + internal class ElasticSearchProvider : SearchProvider + { + public ElasticSearchProvider() : base("Elasticsearch") + { + AreaName = GetType().Assembly.GetName().Name; + } + } +} diff --git a/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Startup.cs b/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Startup.cs new file mode 100644 index 00000000000..907551ad5c8 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Startup.cs @@ -0,0 +1,269 @@ +using System; +using System.Linq; +using Elasticsearch.Net; +using Fluid; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Nest; +using OrchardCore.Admin; +using OrchardCore.BackgroundTasks; +using OrchardCore.ContentManagement; +using OrchardCore.ContentTypes.Editors; +using OrchardCore.Deployment; +using OrchardCore.DisplayManagement.Descriptors; +using OrchardCore.DisplayManagement.Handlers; +using OrchardCore.Environment.Shell.Configuration; +using OrchardCore.Modules; +using OrchardCore.Mvc.Core.Utilities; +using OrchardCore.Navigation; +using OrchardCore.Queries; +using OrchardCore.Search.Abstractions; +using OrchardCore.Search.Abstractions.ViewModels; +using OrchardCore.Search.Elasticsearch.Core.Deployment; +using OrchardCore.Search.Elasticsearch.Core.Models; +using OrchardCore.Search.Elasticsearch.Core.Providers; +using OrchardCore.Search.Elasticsearch.Core.Services; +using OrchardCore.Search.Elasticsearch.Drivers; +using OrchardCore.Search.Elasticsearch.Providers; +using OrchardCore.Security.Permissions; +using OrchardCore.Settings; + +namespace OrchardCore.Search.Elasticsearch +{ + public class Startup : StartupBase + { + private const string ConfigSectionName = "OrchardCore_Elasticsearch"; + private readonly AdminOptions _adminOptions; + private readonly IShellConfiguration _shellConfiguration; + private readonly ILogger _logger; + + public Startup(IOptions adminOptions, + IShellConfiguration shellConfiguration, + ILogger logger) + { + _adminOptions = adminOptions.Value; + _shellConfiguration = shellConfiguration; + _logger = logger; + } + + public override void ConfigureServices(IServiceCollection services) + { + var configuration = _shellConfiguration.GetSection(ConfigSectionName); + var elasticConfiguration = configuration.Get(); + + if (CheckOptions(elasticConfiguration, _logger)) + { + services.Configure(o => o.ConfigurationExists = true); + + IConnectionPool pool = null; + var uris = elasticConfiguration.Ports.Select(port => new Uri($"{elasticConfiguration.Url}:{port}")).Distinct(); + + switch (elasticConfiguration.ConnectionType) + { + case "SingleNodeConnectionPool": + pool = new SingleNodeConnectionPool(uris.First()); + break; + + case "CloudConnectionPool": + BasicAuthenticationCredentials credentials = null; + + if (!String.IsNullOrWhiteSpace(elasticConfiguration.Username) && !String.IsNullOrWhiteSpace(elasticConfiguration.Password) && !String.IsNullOrWhiteSpace(elasticConfiguration.CloudId)) + { + credentials = new BasicAuthenticationCredentials(elasticConfiguration.Username, elasticConfiguration.Password); + pool = new CloudConnectionPool(elasticConfiguration.CloudId, credentials); + } + break; + + case "StaticConnectionPool": + pool = new StaticConnectionPool(uris); + break; + + case "SniffingConnectionPool": + pool = new SniffingConnectionPool(uris); + break; + + case "StickyConnectionPool": + pool = new StickyConnectionPool(uris); + break; + + default: + pool = new SingleNodeConnectionPool(uris.First()); + break; + } + + var settings = new ConnectionSettings(pool).ThrowExceptions(); + + if (elasticConfiguration.ConnectionType != "CloudConnectionPool" && !String.IsNullOrWhiteSpace(elasticConfiguration.Username) && !String.IsNullOrWhiteSpace(elasticConfiguration.Password)) + { + settings.BasicAuthentication(elasticConfiguration.Username, elasticConfiguration.Password); + } + + if (!String.IsNullOrWhiteSpace(elasticConfiguration.CertificateFingerprint)) + { + settings.CertificateFingerprint(elasticConfiguration.CertificateFingerprint); + } + + if (elasticConfiguration.EnableApiVersioningHeader) + { + settings.EnableApiVersioningHeader(); + } + + var client = new ElasticClient(settings); + services.AddSingleton(client); + services.Configure(o => + o.Analyzers.Add(new ElasticAnalyzer(ElasticSettings.StandardAnalyzer, new StandardAnalyzer()))); + + try + { + var response = client.Ping(); + + services.Configure(o => + { + o.MemberAccessStrategy.Register(); + o.MemberAccessStrategy.Register(); + o.MemberAccessStrategy.Register(); + }); + + services.AddElasticServices(); + services.AddSingleton(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped, ElasticSettingsDisplayDriver>(); + services.AddScoped, ElasticQueryDisplayDriver>(); + services.AddScoped(); + services.AddScoped(); + } + catch (Exception ex) + { + _logger.LogError(ex, "Elasticsearch is enabled but not active because the connection failed."); + } + } + } + + public override void Configure(IApplicationBuilder app, IEndpointRouteBuilder routes, IServiceProvider serviceProvider) + { + var options = serviceProvider.GetRequiredService>().Value; + + if (!options.ConfigurationExists) + { + return; + } + + var adminControllerName = typeof(AdminController).ControllerName(); + + routes.MapAreaControllerRoute( + name: "Elasticsearch.Index", + areaName: "OrchardCore.Search.Elasticsearch", + pattern: _adminOptions.AdminUrlPrefix + "/elasticsearch/Index", + defaults: new { controller = adminControllerName, action = nameof(AdminController.Index) } + ); + + routes.MapAreaControllerRoute( + name: "Elasticsearch.Delete", + areaName: "OrchardCore.Search.Elasticsearch", + pattern: _adminOptions.AdminUrlPrefix + "/elasticsearch/Delete/{id}", + defaults: new { controller = adminControllerName, action = nameof(AdminController.Delete) } + ); + + routes.MapAreaControllerRoute( + name: "Elasticsearch.Query", + areaName: "OrchardCore.Search.Elasticsearch", + pattern: _adminOptions.AdminUrlPrefix + "/elasticsearch/Query", + defaults: new { controller = adminControllerName, action = nameof(AdminController.Query) } + ); + + routes.MapAreaControllerRoute( + name: "Elasticsearch.Rebuild", + areaName: "OrchardCore.Search.Elasticsearch", + pattern: _adminOptions.AdminUrlPrefix + "/elasticsearch/Rebuild/{id}", + defaults: new { controller = adminControllerName, action = nameof(AdminController.Rebuild) } + ); + + routes.MapAreaControllerRoute( + name: "Elasticsearch.Reset", + areaName: "OrchardCore.Search.Elasticsearch", + pattern: _adminOptions.AdminUrlPrefix + "/elasticsearch/Reset/{id}", + defaults: new { controller = adminControllerName, action = nameof(AdminController.Reset) } + ); + + routes.MapAreaControllerRoute( + name: "Elasticsearch.SyncSettings", + areaName: "OrchardCore.Search.Elasticsearch", + pattern: _adminOptions.AdminUrlPrefix + "/elasticsearch/SyncSettings", + defaults: new { controller = adminControllerName, action = nameof(AdminController.SyncSettings) } + ); + } + + private static bool CheckOptions(ElasticConnectionOptions elasticConnectionOptions, ILogger logger) + { + var optionsAreValid = true; + + if (elasticConnectionOptions == null) + { + logger.LogError("Elasticsearch is enabled but not active because the configuration is missing."); + return false; + } + + if (String.IsNullOrWhiteSpace(elasticConnectionOptions.Url)) + { + logger.LogError("Elasticsearch is enabled but not active because the 'Url' is missing or empty in application configuration."); + optionsAreValid = false; + } + + if (elasticConnectionOptions.Ports.Length == 0) + { + logger.LogError("Elasticsearch is enabled but not active because a port is missing in application configuration."); + optionsAreValid = false; + } + + return optionsAreValid; + } + } + + [RequireFeatures("OrchardCore.Deployment")] + public class DeploymentStartup : StartupBase + { + public override void ConfigureServices(IServiceCollection services) + { + services.AddTransient(); + services.AddSingleton(new DeploymentStepFactory()); + services.AddScoped, ElasticIndexDeploymentStepDriver>(); + + services.AddTransient(); + services.AddSingleton(new DeploymentStepFactory()); + services.AddScoped, ElasticSettingsDeploymentStepDriver>(); + + services.AddTransient(); + services.AddSingleton(new DeploymentStepFactory()); + services.AddScoped, ElasticIndexRebuildDeploymentStepDriver>(); + + services.AddTransient(); + services.AddSingleton(new DeploymentStepFactory()); + services.AddScoped, ElasticIndexResetDeploymentStepDriver>(); + } + } + + [Feature("OrchardCore.Search.Elasticsearch.Worker")] + public class ElasticWorkerStartup : StartupBase + { + public override void ConfigureServices(IServiceCollection services) + { + services.AddSingleton(); + } + } + + [Feature("OrchardCore.Search.Elasticsearch.ContentPicker")] + public class ElasticContentPickerStartup : StartupBase + { + public override void ConfigureServices(IServiceCollection services) + { + services.AddScoped(); + services.AddScoped(); + services.AddShapeAttributes(); + } + } +} diff --git a/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/ViewModels/AdminIndexViewModel.cs b/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/ViewModels/AdminIndexViewModel.cs new file mode 100644 index 00000000000..a41a87cb477 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/ViewModels/AdminIndexViewModel.cs @@ -0,0 +1,38 @@ +using System.Collections.Generic; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.AspNetCore.Mvc.Rendering; + +namespace OrchardCore.Search.Elasticsearch.ViewModels +{ + public class AdminIndexViewModel + { + public IEnumerable Indexes { get; set; } + + public ContentOptions Options { get; set; } = new ContentOptions(); + + [BindNever] + public dynamic Pager { get; set; } + } + + public class ContentOptions + { + public ContentsBulkAction BulkAction { get; set; } + + public string Search { get; set; } + + #region Lists to populate + + [BindNever] + public List ContentsBulkAction { get; set; } + + #endregion Lists to populate + } + + public enum ContentsBulkAction + { + None, + Reset, + Rebuild, + Remove + } +} diff --git a/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/ViewModels/AdminQueryViewModel.cs b/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/ViewModels/AdminQueryViewModel.cs new file mode 100644 index 00000000000..2d3f8067edb --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/ViewModels/AdminQueryViewModel.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.AspNetCore.Mvc.ModelBinding; + +namespace OrchardCore.Search.Elasticsearch.ViewModels +{ + public class AdminQueryViewModel + { + public string DecodedQuery { get; set; } + public string IndexName { get; set; } + public string Parameters { get; set; } + + [BindNever] + public long Count { get; set; } + + [BindNever] + public string[] Indices { get; set; } + + [BindNever] + public TimeSpan Elapsed { get; set; } = TimeSpan.Zero; + + [BindNever] + public IEnumerable> Documents { get; set; } = Enumerable.Empty>(); + + [BindNever] + public IEnumerable> Fields { get; set; } = Enumerable.Empty>(); + } +} diff --git a/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/ViewModels/ElasticApiQueryViewModel.cs b/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/ViewModels/ElasticApiQueryViewModel.cs new file mode 100644 index 00000000000..fd7870aa8ae --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/ViewModels/ElasticApiQueryViewModel.cs @@ -0,0 +1,9 @@ +namespace OrchardCore.Search.Elasticsearch.ViewModels +{ + public class ElasticApiQueryViewModel + { + public string IndexName { set; get; } + public string Query { set; get; } + public string Parameters { set; get; } + } +} diff --git a/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/ViewModels/ElasticContentIndexSettingsViewModel.cs b/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/ViewModels/ElasticContentIndexSettingsViewModel.cs new file mode 100644 index 00000000000..7cae6a8180e --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/ViewModels/ElasticContentIndexSettingsViewModel.cs @@ -0,0 +1,9 @@ +using OrchardCore.Search.Elasticsearch.Core.Models; + +namespace OrchardCore.Search.Elasticsearch.ViewModels +{ + public class ElasticContentIndexSettingsViewModel + { + public ElasticContentIndexSettings ElasticContentIndexSettings { get; set; } + } +} diff --git a/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/ViewModels/ElasticIndexDeploymentStepViewModel.cs b/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/ViewModels/ElasticIndexDeploymentStepViewModel.cs new file mode 100644 index 00000000000..9cde8bb3479 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/ViewModels/ElasticIndexDeploymentStepViewModel.cs @@ -0,0 +1,9 @@ +namespace OrchardCore.Search.Elasticsearch.ViewModels +{ + public class ElasticIndexDeploymentStepViewModel + { + public bool IncludeAll { get; set; } + public string[] IndexNames { get; set; } + public string[] AllIndexNames { get; set; } + } +} diff --git a/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/ViewModels/ElasticIndexRebuildDeploymentStepViewModel.cs b/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/ViewModels/ElasticIndexRebuildDeploymentStepViewModel.cs new file mode 100644 index 00000000000..a0876412b4d --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/ViewModels/ElasticIndexRebuildDeploymentStepViewModel.cs @@ -0,0 +1,9 @@ +namespace OrchardCore.Search.Elasticsearch.ViewModels +{ + public class ElasticIndexRebuildDeploymentStepViewModel + { + public bool IncludeAll { get; set; } + public string[] IndexNames { get; set; } + public string[] AllIndexNames { get; set; } + } +} diff --git a/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/ViewModels/ElasticIndexResetDeploymentStepViewModel.cs b/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/ViewModels/ElasticIndexResetDeploymentStepViewModel.cs new file mode 100644 index 00000000000..056cbae9d82 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/ViewModels/ElasticIndexResetDeploymentStepViewModel.cs @@ -0,0 +1,9 @@ +namespace OrchardCore.Search.Elasticsearch.ViewModels +{ + public class ElasticIndexResetDeploymentStepViewModel + { + public bool IncludeAll { get; set; } + public string[] IndexNames { get; set; } + public string[] AllIndexNames { get; set; } + } +} diff --git a/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/ViewModels/ElasticIndexSettingsViewModel.cs b/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/ViewModels/ElasticIndexSettingsViewModel.cs new file mode 100644 index 00000000000..017ef230c5e --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/ViewModels/ElasticIndexSettingsViewModel.cs @@ -0,0 +1,33 @@ +using System.Collections.Generic; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.AspNetCore.Mvc.Rendering; + +namespace OrchardCore.Search.Elasticsearch.ViewModels +{ + public class ElasticIndexSettingsViewModel + { + public string IndexName { get; set; } + + public string AnalyzerName { get; set; } + + public bool IndexLatest { get; set; } + + public string Culture { get; set; } + + public string[] IndexedContentTypes { get; set; } + + public bool IsCreate { get; set; } + + public bool StoreSourceData { get; set; } + + #region List to populate + + [BindNever] + public IEnumerable Analyzers { get; set; } + + [BindNever] + public IEnumerable Cultures { get; set; } + + #endregion List to populate + } +} diff --git a/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/ViewModels/ElasticQueryViewModel.cs b/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/ViewModels/ElasticQueryViewModel.cs new file mode 100644 index 00000000000..dc5b7ef7e88 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/ViewModels/ElasticQueryViewModel.cs @@ -0,0 +1,10 @@ +namespace OrchardCore.Search.Elasticsearch.ViewModels +{ + public class ElasticQueryViewModel + { + public string[] Indices { get; set; } + public string Index { get; set; } + public string Query { get; set; } + public bool ReturnContentItems { get; set; } + } +} diff --git a/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/ViewModels/ElasticSettingsViewModel.cs b/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/ViewModels/ElasticSettingsViewModel.cs new file mode 100644 index 00000000000..5e6383c8951 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/ViewModels/ElasticSettingsViewModel.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; + +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; } + } +} diff --git a/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/ViewModels/IndexViewModel.cs b/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/ViewModels/IndexViewModel.cs new file mode 100644 index 00000000000..c639a91e979 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/ViewModels/IndexViewModel.cs @@ -0,0 +1,11 @@ +using System; + +namespace OrchardCore.Search.Elasticsearch.ViewModels +{ + public class IndexViewModel + { + public string Name { get; set; } + public string AnalyzerName { get; set; } + public DateTime LastUpdateUtc { get; set; } + } +} diff --git a/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/ViewModels/MappingsViewModel.cs b/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/ViewModels/MappingsViewModel.cs new file mode 100644 index 00000000000..01c91601820 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/ViewModels/MappingsViewModel.cs @@ -0,0 +1,8 @@ +namespace OrchardCore.Search.Elasticsearch.ViewModels +{ + internal class MappingsViewModel + { + public string IndexName { get; set; } + public string Mappings { get; set; } + } +} diff --git a/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/ViewModels/QueryIndexViewModel.cs b/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/ViewModels/QueryIndexViewModel.cs new file mode 100644 index 00000000000..c474dde5b57 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/ViewModels/QueryIndexViewModel.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.AspNetCore.Mvc.ModelBinding; + +namespace OrchardCore.Search.Elasticsearch.ViewModels +{ + public class QueryIndexViewModel + { + public string Query { get; set; } + public string IndexName { get; set; } + + [BindNever] + public TimeSpan Duration { get; set; } + + [BindNever] + public IEnumerable> Documents { get; set; } = Enumerable.Empty>(); + } +} diff --git a/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Views/Admin/Edit.cshtml b/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Views/Admin/Edit.cshtml new file mode 100644 index 00000000000..61b66dda2f0 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Views/Admin/Edit.cshtml @@ -0,0 +1,75 @@ +@model ElasticIndexSettingsViewModel +@if (Model.IsCreate) +{ + @RenderTitleSegments(T["Create Elasticsearch Index"]) +} +else +{ + @RenderTitleSegments(T["Edit Elasticsearch Index"]) +} + + + + + @T["Index Name"] + + @T["Should be all lowercase. The shell name will be automatically prepended."]@T["See documentation"]. + + + + + @T["Analyzer Name"] + + + + + + @T["Culture"] + + @T["The content culture that it will index."] + + + + + @T["Content Types"] + @T["The content types to index. Choose at least one."] + @if (Model.IsCreate) + { + @await Component.InvokeAsync("SelectContentTypes", new { htmlName = Html.NameFor(m => m.IndexedContentTypes) }) + } + else + { + @await Component.InvokeAsync("SelectContentTypes", new { selectedContentTypes = Model.IndexedContentTypes, htmlName = Html.NameFor(m => m.IndexedContentTypes) }) + } + + + + + @T["Index options"] + + + @T["Index latest version"] + @T["Check to index draft if it exists, otherwise only the published version is indexed."] + + + + + @T["Store source data"] + @T["Check to store source data in the index."] @T["See documentation"] + + + + + + @if (Model.IsCreate) + { + @T["Create"] + @T["Cancel"] + } + else + { + @T["Save"] + @T["Cancel"] + } + + diff --git a/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Views/Admin/Index.cshtml b/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Views/Admin/Index.cshtml new file mode 100644 index 00000000000..1b117b51a4c --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Views/Admin/Index.cshtml @@ -0,0 +1,163 @@ +@model AdminIndexViewModel +@{ + int startIndex = (Model.Pager.Page - 1) * (Model.Pager.PageSize) + 1; + int endIndex = startIndex + Model.Indexes.Count() - 1; +} + +@RenderTitleSegments(T["Elasticsearch Indices"]) + +@* the form is necessary to generate and antiforgery token for the delete action *@ + + + + + + + + + + + + + + + + @T["Synchronize Settings"] + @T["Add Index"] + + + + + + @if (Model.Indexes.Any()) + { + + + + + + + @T.Plural(Model.Indexes.Count(), "1 item", "{0} items")@T.Plural((int)Model.Pager.TotalItemCount, " / {0} item in total", " / {0} items in total") + + + + + + + @T["Actions"] + + + @foreach (var item in Model.Options.ContentsBulkAction) + { + @item.Text + } + + + + + + @foreach (var entry in Model.Indexes) + { + + + + + + + + @entry.Name + @entry.AnalyzerName + + + @T["Mappings"] + @T["Query"] + @T["Reset"] + @T["Rebuild"] + @T["Edit"] + + @T["Delete"] + + Toggle Dropdown + + + @T["Force Delete"] + + + + + + } + } + else + { + + + @T["Nothing here! There are no indices for the moment."] + + + } + + + +@await DisplayAsync(Model.Pager) + + diff --git a/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Views/Admin/Mappings.cshtml b/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Views/Admin/Mappings.cshtml new file mode 100644 index 00000000000..362b344c330 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Views/Admin/Mappings.cshtml @@ -0,0 +1,61 @@ +@model MappingsViewModel + +@T["Elasticsearch index mappings"] + + + + + @T["Mappings for: "]@Model.IndexName + + + + + @T["The Elasticsearch index mapping. For reference only."] + + + + + \ No newline at end of file diff --git a/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Views/Admin/Query.cshtml b/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Views/Admin/Query.cshtml new file mode 100644 index 00000000000..5a87a7b9b4f --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Views/Admin/Query.cshtml @@ -0,0 +1,156 @@ +@model OrchardCore.Search.Elasticsearch.ViewModels.AdminQueryViewModel +@using OrchardCore.ContentManagement; +@inject IContentManager ContentManager + +@{ + var matchAllQuery = Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes(@"{ + ""query"": { + ""match_all"": { } + } +}")); + +} + + + + + +@RenderTitleSegments(T["Elasticsearch Query"]) + + + + + @T["Index"] + + @foreach (var index in Model.Indices) + { + @index + } + + @T["The Elasticsearch index to search on"] + + + + @T["Template"] + + @T["You can use the Match All query to search all documents.", Html.Raw(Url.Action("Query", "Admin", new { area = "OrchardCore.Search.Elasticsearch", Query = matchAllQuery, IndexName = Model.IndexName }))] + @T["The search query uses the Elasticsearch format. Some documentation can be found here https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl.html"] + + + + @T["Parameters"] + + @T["An optional Json object containing the parameter values for this query."] + + + + @T["Query"] + + + + + @if (Model.Elapsed != TimeSpan.Zero) + { + @T["Found {0} result(s) in {1} ms", Model.Count.ToString(), Model.Elapsed.TotalMilliseconds.ToString()] + } + +@if (Model.Documents.Any() || Model.Fields.Any()) +{ + + @T["Create"] + +} +@if (Model.Documents.Any()) +{ + var fieldNames = Model.Documents.SelectMany(d => d.Keys.Distinct()).Distinct().ToList(); + + + @T["The results displayed in this table are coming from the _source data."] + + + + + + # + @foreach (var name in fieldNames) + { + @name + } + + + + @{ + int row = 1; + } + @foreach (var document in Model.Documents) + { + + @(row++) + @foreach (var name in fieldNames) + { + @(document.GetValueOrDefault(name)?.ToString()?? String.Empty) + } + + } + + + +} +@if (Model.Fields.Any()) +{ + var fields = Model.Fields.SelectMany(d => d.Distinct()).ToList(); + var fieldNames = fields.OrderBy(x => x.Key).Select(d => d.Key).Distinct().ToList(); + + + @T["The results displayed in this table are coming from the field(s) data."] + + + + + + # + @foreach (var name in fieldNames) + { + @name + } + + + + @{ + int row = 1; + } + @foreach (var hit in Model.Fields) + { + + @(row++) + + @for (int i = 0; i < fieldNames.Count; i++) + { + var hitValues = (string[])hit.FirstOrDefault(x => x.Key == fieldNames[i]).Value; + + if (hitValues != null) + { + var value = hitValues.First(); + var item = !String.IsNullOrEmpty(value) ? value : "NULL"; + + + @item + + } + else + { + NULL + } + } + + } + + + +} + diff --git a/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Views/ContentPickerFieldElasticEditorSettings.Edit.cshtml b/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Views/ContentPickerFieldElasticEditorSettings.Edit.cshtml new file mode 100644 index 00000000000..b1bc0144fe5 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Views/ContentPickerFieldElasticEditorSettings.Edit.cshtml @@ -0,0 +1,16 @@ +@model OrchardCore.Search.Elasticsearch.Core.Models.ContentPickerFieldElasticEditorSettings + + + + + @T["Index"] + + @foreach (var index in Model.Indices) + { + @index + } + + + @T["The Elasticsearch index to query for content items"] + + diff --git a/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Views/ElasticContentIndexSettings.Edit.cshtml b/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Views/ElasticContentIndexSettings.Edit.cshtml new file mode 100644 index 00000000000..4f56c038bd2 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Views/ElasticContentIndexSettings.Edit.cshtml @@ -0,0 +1,11 @@ +@model ElasticContentIndexSettingsViewModel + +Elasticsearch @T["Index settings"] + + + + + @T["Include this element in the index"] + @T["Check to include the value of this element in the index."] + + diff --git a/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Views/ElasticQuery.Buttons.SummaryAdmin.cshtml b/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Views/ElasticQuery.Buttons.SummaryAdmin.cshtml new file mode 100644 index 00000000000..24501cf5ce3 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Views/ElasticQuery.Buttons.SummaryAdmin.cshtml @@ -0,0 +1,12 @@ +@model dynamic + +@{ + var base64Query = Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes((string)Model.Query.Template ?? "")); +} + +@T["Run"] diff --git a/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Views/ElasticQuery.Edit.cshtml b/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Views/ElasticQuery.Edit.cshtml new file mode 100644 index 00000000000..834674b2fc1 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Views/ElasticQuery.Edit.cshtml @@ -0,0 +1,36 @@ +@model ElasticQueryViewModel + + + + + + + @T["Index"] + + @foreach (var index in Model.Indices) + { + @index + } + + @T["The Elasticsearch index to search on"] + + + + + + @T["Return Content Items"] + @T["Check to return the corresponding content items."] + + + + + @T["Query"] + + @T["The search query uses the Elasticsearch format. Some documentation can be found here https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl.html"] + + + diff --git a/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Views/ElasticQuery.SummaryAdmin.cshtml b/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Views/ElasticQuery.SummaryAdmin.cshtml new file mode 100644 index 00000000000..8f41a140062 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Views/ElasticQuery.SummaryAdmin.cshtml @@ -0,0 +1 @@ +@T["Elasticsearch query"] diff --git a/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Views/ElasticSettings.Edit.cshtml b/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Views/ElasticSettings.Edit.cshtml new file mode 100644 index 00000000000..7bcf021a454 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Views/ElasticSettings.Edit.cshtml @@ -0,0 +1,35 @@ +@model ElasticSettingsViewModel + +@if (Model.SearchIndexes.Any()) +{ + + @T["Default search index"] + + @foreach (var index in Model.SearchIndexes) + { + @index + } + + + @T["The default index to use for the search page."] + +} +else +{ + @T["You need to create at least an index to set as the Search index."] +} + + + @T["Default searched fields"] + + + @T["A comma separated list of fields to use for search pages. The default value is Content.ContentItem.FullText."] + + + + + + @T["Allow Elasticsearch \"query string query\" in search forms"] + @T["Whether search queries should be allowed to use Elasticsearch \"query string query\" syntax."] @T["See documentation"] + + diff --git a/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Views/Items/ElasticIndexDeploymentStep.Fields.Edit.cshtml b/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Views/Items/ElasticIndexDeploymentStep.Fields.Edit.cshtml new file mode 100644 index 00000000000..e8679028b84 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Views/Items/ElasticIndexDeploymentStep.Fields.Edit.cshtml @@ -0,0 +1,61 @@ +@model ElasticIndexDeploymentStepViewModel + +@{ + var indexNames = Model.IndexNames; + var allIndexNames = Model.AllIndexNames; +} + +@T["Elasticsearch Search Indexes"] + + + + + + + + @T["Include all search indexes."] + + + + + + + + + + @T["The search indexes to add as part of the plan."] + + + + + + @foreach (var indexName in allIndexNames) + { + var checkd = indexNames?.Contains(indexName); + + + + + + @indexName + + + + } + + + + + + diff --git a/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Views/Items/ElasticIndexDeploymentStep.Fields.Summary.cshtml b/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Views/Items/ElasticIndexDeploymentStep.Fields.Summary.cshtml new file mode 100644 index 00000000000..298bf06095d --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Views/Items/ElasticIndexDeploymentStep.Fields.Summary.cshtml @@ -0,0 +1,27 @@ +@using OrchardCore.DisplayManagement.Views +@using OrchardCore.Search.Elasticsearch.Core.Deployment + +@model ShapeViewModel + +@{ + var includeAll = Model.Value.IncludeAll; + var indexNames = Model.Value.IndexNames; +} + +@T["Elasticsearch Search Indexes"] + +@if (includeAll) +{ + @T["All"] +} +else if (indexNames?.Length > 0) +{ + foreach (var indexName in indexNames) + { + @indexName + } +} +else +{ + @T["No index selected."] +} diff --git a/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Views/Items/ElasticIndexDeploymentStep.Fields.Thumbnail.cshtml b/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Views/Items/ElasticIndexDeploymentStep.Fields.Thumbnail.cshtml new file mode 100644 index 00000000000..89b8b3d359a --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Views/Items/ElasticIndexDeploymentStep.Fields.Thumbnail.cshtml @@ -0,0 +1,4 @@ +@model dynamic + +@T["Elasticsearch Search Indexes"] +@T["Exports all or specified search indexes."] diff --git a/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Views/Items/ElasticIndexRebuildDeploymentStep.Fields.Edit.cshtml b/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Views/Items/ElasticIndexRebuildDeploymentStep.Fields.Edit.cshtml new file mode 100644 index 00000000000..9fa694b207f --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Views/Items/ElasticIndexRebuildDeploymentStep.Fields.Edit.cshtml @@ -0,0 +1,51 @@ +@model ElasticIndexRebuildDeploymentStepViewModel + +@T["Rebuild Elasticsearch Indices"] + + + + + + + + @T["Include all Elasticsearch indices."] + + + + + + + + + + @T["The Elasticsearch indices to rebuild as part of the plan."] + + + + + + @foreach (var indexName in Model.AllIndexNames) + { + var isChecked = Model.IndexNames?.Contains(indexName); + + + + + + @indexName + + + + } + + + + + + diff --git a/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Views/Items/ElasticIndexRebuildDeploymentStep.Fields.Summary.cshtml b/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Views/Items/ElasticIndexRebuildDeploymentStep.Fields.Summary.cshtml new file mode 100644 index 00000000000..bfcf85c53c0 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Views/Items/ElasticIndexRebuildDeploymentStep.Fields.Summary.cshtml @@ -0,0 +1,27 @@ +@using OrchardCore.DisplayManagement.Views +@using OrchardCore.Search.Elasticsearch.Core.Deployment + +@model ShapeViewModel + +@{ + var includeAll = Model.Value.IncludeAll; + var indices = Model.Value.Indices; +} + +@T["Rebuild Elasticsearch Indices"] + +@if (includeAll) +{ + @T["All"] +} +else if (indices?.Length > 0) +{ + foreach (var indexName in indices) + { + @indexName + } +} +else +{ + @T["No index selected."] +} diff --git a/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Views/Items/ElasticIndexRebuildDeploymentStep.Fields.Thumbnail.cshtml b/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Views/Items/ElasticIndexRebuildDeploymentStep.Fields.Thumbnail.cshtml new file mode 100644 index 00000000000..12c1b9782f6 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Views/Items/ElasticIndexRebuildDeploymentStep.Fields.Thumbnail.cshtml @@ -0,0 +1,4 @@ +@model dynamic + +@T["Rebuild Elasticsearch Indices"] +@T["Rebuild all or specified Elasticsearch indices."] diff --git a/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Views/Items/ElasticIndexResetDeploymentStep.Fields.Edit.cshtml b/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Views/Items/ElasticIndexResetDeploymentStep.Fields.Edit.cshtml new file mode 100644 index 00000000000..8f581cded58 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Views/Items/ElasticIndexResetDeploymentStep.Fields.Edit.cshtml @@ -0,0 +1,51 @@ +@model ElasticIndexResetDeploymentStepViewModel + +@T["Reset Elasticsearch Indices"] + + + + + + + + @T["Include all Elasticsearch indices."] + + + + + + + + + + @T["The Elasticsearch Indices to reset as part of the plan."] + + + + + + @foreach (var indexName in Model.AllIndexNames) + { + var isChecked = Model.IndexNames?.Contains(indexName); + + + + + + @indexName + + + + } + + + + + + diff --git a/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Views/Items/ElasticIndexResetDeploymentStep.Fields.Summary.cshtml b/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Views/Items/ElasticIndexResetDeploymentStep.Fields.Summary.cshtml new file mode 100644 index 00000000000..0b6b0138df3 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Views/Items/ElasticIndexResetDeploymentStep.Fields.Summary.cshtml @@ -0,0 +1,27 @@ +@using OrchardCore.DisplayManagement.Views +@using OrchardCore.Search.Elasticsearch.Core.Deployment + +@model ShapeViewModel + +@{ + var includeAll = Model.Value.IncludeAll; + var indices = Model.Value.Indices; +} + +@T["Reset Elasticsearch Indices"] + +@if (includeAll) +{ + @T["All"] +} +else if (indices?.Length > 0) +{ + foreach (var indexName in indices) + { + @indexName + } +} +else +{ + @T["No index selected."] +} diff --git a/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Views/Items/ElasticIndexResetDeploymentStep.Fields.Thumbnail.cshtml b/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Views/Items/ElasticIndexResetDeploymentStep.Fields.Thumbnail.cshtml new file mode 100644 index 00000000000..f007652ce15 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Views/Items/ElasticIndexResetDeploymentStep.Fields.Thumbnail.cshtml @@ -0,0 +1,4 @@ +@model dynamic + +@T["Reset Elasticsearch Indices"] +@T["Reset all or specified Elasticsearch indices."] diff --git a/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Views/Items/ElasticSettingsDeploymentStep.Fields.Edit.cshtml b/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Views/Items/ElasticSettingsDeploymentStep.Fields.Edit.cshtml new file mode 100644 index 00000000000..7f63fd4a698 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Views/Items/ElasticSettingsDeploymentStep.Fields.Edit.cshtml @@ -0,0 +1,3 @@ +@model dynamic + +@T["Elasticsearch Settings"] diff --git a/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Views/Items/ElasticSettingsDeploymentStep.Fields.Summary.cshtml b/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Views/Items/ElasticSettingsDeploymentStep.Fields.Summary.cshtml new file mode 100644 index 00000000000..100371395b3 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Views/Items/ElasticSettingsDeploymentStep.Fields.Summary.cshtml @@ -0,0 +1,5 @@ +@model dynamic + +@T["Elasticsearch Settings"] + +@T["Adds Elasticsearch settings to the plan."] diff --git a/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Views/Items/ElasticSettingsDeploymentStep.Fields.Thumbnail.cshtml b/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Views/Items/ElasticSettingsDeploymentStep.Fields.Thumbnail.cshtml new file mode 100644 index 00000000000..6324057c80a --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Views/Items/ElasticSettingsDeploymentStep.Fields.Thumbnail.cshtml @@ -0,0 +1,4 @@ +@model dynamic + +@T["Elasticsearch Settings"] +@T["Exports Elasticsearch settings."] diff --git a/src/OrchardCore.Modules/OrchardCore.Lucene/Views/NavigationItemText-search.Id.cshtml b/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Views/NavigationItemText-Elasticsearch.Id.cshtml similarity index 100% rename from src/OrchardCore.Modules/OrchardCore.Lucene/Views/NavigationItemText-search.Id.cshtml rename to src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Views/NavigationItemText-Elasticsearch.Id.cshtml diff --git a/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Views/Query-Elasticsearch.Link.cshtml b/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Views/Query-Elasticsearch.Link.cshtml new file mode 100644 index 00000000000..01e89160339 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Views/Query-Elasticsearch.Link.cshtml @@ -0,0 +1,16 @@ +@model dynamic +@using OrchardCore.Search.Elasticsearch.Core.Services +@inject ElasticIndexSettingsService ElasticIndexSettingsService + +@{ + var disabled = !(await ElasticIndexSettingsService.GetSettingsAsync()).Any(); +} + + + @T["Elasticsearch"] + @T["Queries an Elasticsearch index."] + + + diff --git a/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Views/_ViewImports.cshtml b/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Views/_ViewImports.cshtml new file mode 100644 index 00000000000..b8b85ae30dc --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Views/_ViewImports.cshtml @@ -0,0 +1,13 @@ +@* + For more information on enabling MVC for empty projects, visit http://go.microsoft.com/fwlink/?LinkID=397860 + +*@ + +@inherits OrchardCore.DisplayManagement.Razor.RazorPage +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers +@addTagHelper *, OrchardCore.DisplayManagement +@addTagHelper *, OrchardCore.ResourceManagement +@using Microsoft.Extensions.Localization +@using Microsoft.AspNetCore.Mvc.Localization +@using OrchardCore.Search.Elasticsearch.ViewModels +@using OrchardCore.ContentManagement.Display diff --git a/src/OrchardCore.Modules/OrchardCore.Lucene/AdminMenu.cs b/src/OrchardCore.Modules/OrchardCore.Search.Lucene/AdminMenu.cs similarity index 79% rename from src/OrchardCore.Modules/OrchardCore.Lucene/AdminMenu.cs rename to src/OrchardCore.Modules/OrchardCore.Search.Lucene/AdminMenu.cs index d9ad832171d..ba0a718f70b 100644 --- a/src/OrchardCore.Modules/OrchardCore.Lucene/AdminMenu.cs +++ b/src/OrchardCore.Modules/OrchardCore.Search.Lucene/AdminMenu.cs @@ -1,10 +1,10 @@ using System; using System.Threading.Tasks; using Microsoft.Extensions.Localization; -using OrchardCore.Lucene.Drivers; +using OrchardCore.Search.Lucene.Drivers; using OrchardCore.Navigation; -namespace OrchardCore.Lucene +namespace OrchardCore.Search.Lucene { public class AdminMenu : INavigationProvider { @@ -27,17 +27,17 @@ public Task BuildNavigationAsync(string name, NavigationBuilder builder) .AddClass("search").Id("search") .Add(S["Indexing"], S["Indexing"].PrefixPosition(), import => import .Add(S["Lucene Indices"], S["Lucene Indices"].PrefixPosition(), indexes => indexes - .Action("Index", "Admin", new { area = "OrchardCore.Lucene" }) - .Permission(Permissions.ManageIndexes) + .Action("Index", "Admin", new { area = "OrchardCore.Search.Lucene" }) + .Permission(Permissions.ManageLuceneIndexes) .LocalNav()) .Add(S["Run Lucene Query"], S["Run Lucene Query"].PrefixPosition(), queries => queries - .Action("Query", "Admin", new { area = "OrchardCore.Lucene" }) - .Permission(Permissions.ManageIndexes) + .Action("Query", "Admin", new { area = "OrchardCore.Search.Lucene" }) + .Permission(Permissions.ManageLuceneIndexes) .LocalNav())) .Add(S["Settings"], settings => settings - .Add(S["Search"], S["Search"].PrefixPosition(), entry => entry + .Add(S["Lucene"], S["Lucene"].PrefixPosition(), entry => entry .Action("Index", "Admin", new { area = "OrchardCore.Settings", groupId = LuceneSettingsDisplayDriver.GroupId }) - .Permission(Permissions.ManageIndexes) + .Permission(Permissions.ManageLuceneIndexes) .LocalNav() ))); diff --git a/src/OrchardCore.Modules/OrchardCore.Lucene/Controllers/AdminController.cs b/src/OrchardCore.Modules/OrchardCore.Search.Lucene/Controllers/AdminController.cs similarity index 95% rename from src/OrchardCore.Modules/OrchardCore.Lucene/Controllers/AdminController.cs rename to src/OrchardCore.Modules/OrchardCore.Search.Lucene/Controllers/AdminController.cs index 0f3618a7a18..ac6b595eed0 100644 --- a/src/OrchardCore.Modules/OrchardCore.Lucene/Controllers/AdminController.cs +++ b/src/OrchardCore.Modules/OrchardCore.Search.Lucene/Controllers/AdminController.cs @@ -21,15 +21,16 @@ using OrchardCore.DisplayManagement; using OrchardCore.DisplayManagement.Notify; using OrchardCore.Liquid; -using OrchardCore.Lucene.Model; -using OrchardCore.Lucene.Services; -using OrchardCore.Lucene.ViewModels; using OrchardCore.Mvc.Utilities; using OrchardCore.Navigation; using OrchardCore.Routing; +using OrchardCore.Search.Lucene.Model; +using OrchardCore.Search.Lucene.Services; +using OrchardCore.Search.Lucene.ViewModels; +using OrchardCore.Settings; using YesSql; -namespace OrchardCore.Lucene.Controllers +namespace OrchardCore.Search.Lucene.Controllers { public class AdminController : Controller { @@ -92,7 +93,7 @@ public AdminController( public async Task Index(ContentOptions options, PagerParameters pagerParameters) { - if (!await _authorizationService.AuthorizeAsync(User, Permissions.ManageIndexes)) + if (!await _authorizationService.AuthorizeAsync(User, Permissions.ManageLuceneIndexes)) { return Forbid(); } @@ -146,7 +147,7 @@ public async Task Edit(string indexName = null) var IsCreate = String.IsNullOrWhiteSpace(indexName); var settings = new LuceneIndexSettings(); - if (!await _authorizationService.AuthorizeAsync(User, Permissions.ManageIndexes)) + if (!await _authorizationService.AuthorizeAsync(User, Permissions.ManageLuceneIndexes)) { return Forbid(); } @@ -173,7 +174,8 @@ public async Task Edit(string indexName = null) Analyzers = _luceneAnalyzerManager.GetAnalyzers() .Select(x => new SelectListItem { Text = x.Name, Value = x.Name }), IndexedContentTypes = IsCreate ? _contentDefinitionManager.ListTypeDefinitions() - .Select(x => x.Name).ToArray() : settings.IndexedContentTypes + .Select(x => x.Name).ToArray() : settings.IndexedContentTypes, + StoreSourceData = !IsCreate && settings.StoreSourceData }; return View(model); @@ -182,7 +184,7 @@ public async Task Edit(string indexName = null) [HttpPost, ActionName("Edit")] public async Task EditPost(LuceneIndexSettingsViewModel model, string[] indexedContentTypes) { - if (!await _authorizationService.AuthorizeAsync(User, Permissions.ManageIndexes)) + if (!await _authorizationService.AuthorizeAsync(User, Permissions.ManageLuceneIndexes)) { return Forbid(); } @@ -217,7 +219,7 @@ public async Task EditPost(LuceneIndexSettingsViewModel model, str { try { - var settings = new LuceneIndexSettings { IndexName = model.IndexName, AnalyzerName = model.AnalyzerName, IndexLatest = model.IndexLatest, IndexedContentTypes = indexedContentTypes, Culture = model.Culture ?? "" }; + var settings = new LuceneIndexSettings { IndexName = model.IndexName, AnalyzerName = model.AnalyzerName, IndexLatest = model.IndexLatest, IndexedContentTypes = indexedContentTypes, Culture = model.Culture ?? "", StoreSourceData = model.StoreSourceData }; // We call Rebuild in order to reset the index state cursor too in case the same index // name was also used previously. @@ -236,7 +238,7 @@ public async Task EditPost(LuceneIndexSettingsViewModel model, str { try { - var settings = new LuceneIndexSettings { IndexName = model.IndexName, AnalyzerName = model.AnalyzerName, IndexLatest = model.IndexLatest, IndexedContentTypes = indexedContentTypes, Culture = model.Culture ?? "" }; + var settings = new LuceneIndexSettings { IndexName = model.IndexName, AnalyzerName = model.AnalyzerName, IndexLatest = model.IndexLatest, IndexedContentTypes = indexedContentTypes, Culture = model.Culture ?? "", StoreSourceData = model.StoreSourceData }; await _luceneIndexingService.UpdateIndexAsync(settings); } @@ -256,7 +258,7 @@ public async Task EditPost(LuceneIndexSettingsViewModel model, str [HttpPost] public async Task Reset(string id) { - if (!await _authorizationService.AuthorizeAsync(User, Permissions.ManageIndexes)) + if (!await _authorizationService.AuthorizeAsync(User, Permissions.ManageLuceneIndexes)) { return Forbid(); } @@ -272,7 +274,7 @@ public async Task Reset(string id) } else { - _luceneIndexingService.ResetIndex(id); + _luceneIndexingService.ResetIndexAsync(id); await _luceneIndexingService.ProcessContentItemsAsync(id); } @@ -285,7 +287,7 @@ public async Task Reset(string id) [HttpPost] public async Task Rebuild(string id) { - if (!await _authorizationService.AuthorizeAsync(User, Permissions.ManageIndexes)) + if (!await _authorizationService.AuthorizeAsync(User, Permissions.ManageLuceneIndexes)) { return Forbid(); } @@ -310,7 +312,7 @@ public async Task Rebuild(string id) [HttpPost] public async Task Delete(LuceneIndexSettingsViewModel model) { - if (!await _authorizationService.AuthorizeAsync(User, Permissions.ManageIndexes)) + if (!await _authorizationService.AuthorizeAsync(User, Permissions.ManageLuceneIndexes)) { return Forbid(); } @@ -348,7 +350,7 @@ public Task Query(string indexName, string query) [HttpPost] public async Task Query(AdminQueryViewModel model) { - if (!await _authorizationService.AuthorizeAsync(User, Permissions.ManageIndexes)) + if (!await _authorizationService.AuthorizeAsync(User, Permissions.ManageLuceneIndexes)) { return Forbid(); } @@ -421,7 +423,7 @@ await _luceneIndexManager.SearchAsync(model.IndexName, async searcher => [FormValueRequired("submit.BulkAction")] public async Task IndexPost(ViewModels.ContentOptions options, IEnumerable itemIds) { - if (!await _authorizationService.AuthorizeAsync(User, Permissions.ManageIndexes)) + if (!await _authorizationService.AuthorizeAsync(User, Permissions.ManageLuceneIndexes)) { return Forbid(); } @@ -449,7 +451,7 @@ public async Task IndexPost(ViewModels.ContentOptions options, IEn return NotFound(); } - _luceneIndexingService.ResetIndex(item.IndexName); + _luceneIndexingService.ResetIndexAsync(item.IndexName); await _luceneIndexingService.ProcessContentItemsAsync(item.IndexName); await _notifier.SuccessAsync(H["Index {0} reset successfully.", item.IndexName]); diff --git a/src/OrchardCore.Modules/OrchardCore.Lucene/Controllers/ApiController.cs b/src/OrchardCore.Modules/OrchardCore.Search.Lucene/Controllers/ApiController.cs similarity index 97% rename from src/OrchardCore.Modules/OrchardCore.Lucene/Controllers/ApiController.cs rename to src/OrchardCore.Modules/OrchardCore.Search.Lucene/Controllers/ApiController.cs index 3cc0375b8e4..fdaedba29e3 100644 --- a/src/OrchardCore.Modules/OrchardCore.Lucene/Controllers/ApiController.cs +++ b/src/OrchardCore.Modules/OrchardCore.Search.Lucene/Controllers/ApiController.cs @@ -3,9 +3,9 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Newtonsoft.Json; -using OrchardCore.Lucene.Model; +using OrchardCore.Search.Lucene.Model; -namespace OrchardCore.Lucene.Controllers +namespace OrchardCore.Search.Lucene.Controllers { [Route("api/lucene")] [ApiController] diff --git a/src/OrchardCore.Modules/OrchardCore.Lucene/Controllers/SearchController.cs b/src/OrchardCore.Modules/OrchardCore.Search.Lucene/Controllers/SearchController.cs similarity index 90% rename from src/OrchardCore.Modules/OrchardCore.Lucene/Controllers/SearchController.cs rename to src/OrchardCore.Modules/OrchardCore.Search.Lucene/Controllers/SearchController.cs index de5a1f91e62..f77243587cf 100644 --- a/src/OrchardCore.Modules/OrchardCore.Lucene/Controllers/SearchController.cs +++ b/src/OrchardCore.Modules/OrchardCore.Search.Lucene/Controllers/SearchController.cs @@ -11,26 +11,26 @@ using OrchardCore.ContentManagement.Records; using OrchardCore.DisplayManagement; using OrchardCore.Entities; -using OrchardCore.Lucene.Model; -using OrchardCore.Lucene.Services; using OrchardCore.Navigation; using OrchardCore.Search.Abstractions.ViewModels; +using OrchardCore.Search.Lucene.Model; +using OrchardCore.Search.Lucene.Services; using OrchardCore.Security.Permissions; using OrchardCore.Settings; using YesSql; using YesSql.Services; -namespace OrchardCore.Lucene.Controllers +namespace OrchardCore.Search.Lucene.Controllers { public class SearchController : Controller { private readonly IAuthorizationService _authorizationService; private readonly ISiteService _siteService; - private readonly LuceneIndexManager _luceneIndexProvider; + private readonly LuceneIndexManager _luceneIndexManager; private readonly LuceneIndexingService _luceneIndexingService; private readonly LuceneIndexSettingsService _luceneIndexSettingsService; private readonly LuceneAnalyzerManager _luceneAnalyzerManager; - private readonly ISearchQueryService _searchQueryService; + private readonly ILuceneSearchQueryService _searchQueryService; private readonly ISession _session; private readonly IStringLocalizer S; private readonly IEnumerable _permissionProviders; @@ -40,11 +40,11 @@ public class SearchController : Controller public SearchController( IAuthorizationService authorizationService, ISiteService siteService, - LuceneIndexManager luceneIndexProvider, + LuceneIndexManager luceneIndexManager, LuceneIndexingService luceneIndexingService, LuceneIndexSettingsService luceneIndexSettingsService, LuceneAnalyzerManager luceneAnalyzerManager, - ISearchQueryService searchQueryService, + ILuceneSearchQueryService searchQueryService, ISession session, IStringLocalizer stringLocalizer, IEnumerable permissionProviders, @@ -54,7 +54,7 @@ ILogger logger { _authorizationService = authorizationService; _siteService = siteService; - _luceneIndexProvider = luceneIndexProvider; + _luceneIndexManager = luceneIndexManager; _luceneIndexingService = luceneIndexingService; _luceneIndexSettingsService = luceneIndexSettingsService; _luceneAnalyzerManager = luceneAnalyzerManager; @@ -69,7 +69,7 @@ ILogger logger [HttpGet] public async Task Search(SearchIndexViewModel viewModel, PagerSlimParameters pagerParameters) { - var permissionsProvider = _permissionProviders.FirstOrDefault(x => x.GetType().FullName == "OrchardCore.Lucene.Permissions"); + var permissionsProvider = _permissionProviders.FirstOrDefault(x => x.GetType().FullName == "OrchardCore.Search.Lucene.Permissions"); var permissions = await permissionsProvider.GetPermissionsAsync(); var siteSettings = await _siteService.GetSiteSettingsAsync(); @@ -86,13 +86,13 @@ public async Task Search(SearchIndexViewModel viewModel, PagerSli else { _logger.LogInformation("Couldn't execute search. The search index doesn't exist."); - return BadRequest("Search is not configured."); + return BadRequest("Lucene search is not configured."); } - if (searchIndex != null && !_luceneIndexProvider.Exists(searchIndex)) + if (searchIndex != null && !_luceneIndexManager.Exists(searchIndex)) { _logger.LogInformation("Couldn't execute search. The search index doesn't exist."); - return BadRequest("Search is not configured."); + return BadRequest("Lucene search is not configured."); } var luceneSettings = await _luceneIndexingService.GetLuceneSettingsAsync(); @@ -100,7 +100,7 @@ public async Task Search(SearchIndexViewModel viewModel, PagerSli if (luceneSettings == null || luceneSettings?.DefaultSearchFields == null) { _logger.LogInformation("Couldn't execute search. No Lucene settings was defined."); - return BadRequest("Search is not configured."); + return BadRequest("Lucene search is not configured."); } var luceneIndexSettings = await _luceneIndexSettingsService.GetSettingsAsync(searchIndex); @@ -108,7 +108,7 @@ public async Task Search(SearchIndexViewModel viewModel, PagerSli if (luceneIndexSettings == null) { _logger.LogInformation($"Couldn't execute search. No Lucene index settings was defined for ({searchIndex}) index."); - return BadRequest($"Search index ({searchIndex}) is not configured."); + return BadRequest($"Lucene search index ({searchIndex}) is not configured."); } if (string.IsNullOrWhiteSpace(viewModel.Terms)) diff --git a/src/OrchardCore.Modules/OrchardCore.Lucene/Deployment/LuceneIndexDeploymentSource.cs b/src/OrchardCore.Modules/OrchardCore.Search.Lucene/Deployment/LuceneIndexDeploymentSource.cs similarity index 95% rename from src/OrchardCore.Modules/OrchardCore.Lucene/Deployment/LuceneIndexDeploymentSource.cs rename to src/OrchardCore.Modules/OrchardCore.Search.Lucene/Deployment/LuceneIndexDeploymentSource.cs index d25b2f53aa6..44274cf6f42 100644 --- a/src/OrchardCore.Modules/OrchardCore.Lucene/Deployment/LuceneIndexDeploymentSource.cs +++ b/src/OrchardCore.Modules/OrchardCore.Search.Lucene/Deployment/LuceneIndexDeploymentSource.cs @@ -3,9 +3,9 @@ using System.Threading.Tasks; using Newtonsoft.Json.Linq; using OrchardCore.Deployment; -using OrchardCore.Lucene.Model; +using OrchardCore.Search.Lucene.Model; -namespace OrchardCore.Lucene.Deployment +namespace OrchardCore.Search.Lucene.Deployment { public class LuceneIndexDeploymentSource : IDeploymentSource { diff --git a/src/OrchardCore.Modules/OrchardCore.Lucene/Deployment/LuceneIndexDeploymentStep.cs b/src/OrchardCore.Modules/OrchardCore.Search.Lucene/Deployment/LuceneIndexDeploymentStep.cs similarity index 89% rename from src/OrchardCore.Modules/OrchardCore.Lucene/Deployment/LuceneIndexDeploymentStep.cs rename to src/OrchardCore.Modules/OrchardCore.Search.Lucene/Deployment/LuceneIndexDeploymentStep.cs index e016fa28028..3ee686c9946 100644 --- a/src/OrchardCore.Modules/OrchardCore.Lucene/Deployment/LuceneIndexDeploymentStep.cs +++ b/src/OrchardCore.Modules/OrchardCore.Search.Lucene/Deployment/LuceneIndexDeploymentStep.cs @@ -1,6 +1,6 @@ using OrchardCore.Deployment; -namespace OrchardCore.Lucene.Deployment +namespace OrchardCore.Search.Lucene.Deployment { /// /// Adds layers to a . diff --git a/src/OrchardCore.Modules/OrchardCore.Lucene/Deployment/LuceneIndexDeploymentStepDriver.cs b/src/OrchardCore.Modules/OrchardCore.Search.Lucene/Deployment/LuceneIndexDeploymentStepDriver.cs similarity index 95% rename from src/OrchardCore.Modules/OrchardCore.Lucene/Deployment/LuceneIndexDeploymentStepDriver.cs rename to src/OrchardCore.Modules/OrchardCore.Search.Lucene/Deployment/LuceneIndexDeploymentStepDriver.cs index 0ec01f8f4e0..aeaf4ddaa88 100644 --- a/src/OrchardCore.Modules/OrchardCore.Lucene/Deployment/LuceneIndexDeploymentStepDriver.cs +++ b/src/OrchardCore.Modules/OrchardCore.Search.Lucene/Deployment/LuceneIndexDeploymentStepDriver.cs @@ -5,9 +5,9 @@ using OrchardCore.DisplayManagement.Handlers; using OrchardCore.DisplayManagement.ModelBinding; using OrchardCore.DisplayManagement.Views; -using OrchardCore.Lucene.ViewModels; +using OrchardCore.Search.Lucene.ViewModels; -namespace OrchardCore.Lucene.Deployment +namespace OrchardCore.Search.Lucene.Deployment { public class LuceneIndexDeploymentStepDriver : DisplayDriver { diff --git a/src/OrchardCore.Modules/OrchardCore.Lucene/Deployment/LuceneIndexRebuildDeploymentSource.cs b/src/OrchardCore.Modules/OrchardCore.Search.Lucene/Deployment/LuceneIndexRebuildDeploymentSource.cs similarity index 95% rename from src/OrchardCore.Modules/OrchardCore.Lucene/Deployment/LuceneIndexRebuildDeploymentSource.cs rename to src/OrchardCore.Modules/OrchardCore.Search.Lucene/Deployment/LuceneIndexRebuildDeploymentSource.cs index a560f88287f..f95d00fa02f 100644 --- a/src/OrchardCore.Modules/OrchardCore.Lucene/Deployment/LuceneIndexRebuildDeploymentSource.cs +++ b/src/OrchardCore.Modules/OrchardCore.Search.Lucene/Deployment/LuceneIndexRebuildDeploymentSource.cs @@ -3,7 +3,7 @@ using Newtonsoft.Json.Linq; using OrchardCore.Deployment; -namespace OrchardCore.Lucene.Deployment +namespace OrchardCore.Search.Lucene.Deployment { public class LuceneIndexRebuildDeploymentSource : IDeploymentSource { diff --git a/src/OrchardCore.Modules/OrchardCore.Lucene/Deployment/LuceneIndexRebuildDeploymentStep.cs b/src/OrchardCore.Modules/OrchardCore.Search.Lucene/Deployment/LuceneIndexRebuildDeploymentStep.cs similarity index 90% rename from src/OrchardCore.Modules/OrchardCore.Lucene/Deployment/LuceneIndexRebuildDeploymentStep.cs rename to src/OrchardCore.Modules/OrchardCore.Search.Lucene/Deployment/LuceneIndexRebuildDeploymentStep.cs index caf33029d12..1cb69648465 100644 --- a/src/OrchardCore.Modules/OrchardCore.Lucene/Deployment/LuceneIndexRebuildDeploymentStep.cs +++ b/src/OrchardCore.Modules/OrchardCore.Search.Lucene/Deployment/LuceneIndexRebuildDeploymentStep.cs @@ -1,6 +1,6 @@ using OrchardCore.Deployment; -namespace OrchardCore.Lucene.Deployment +namespace OrchardCore.Search.Lucene.Deployment { /// /// Adds rebuild Lucene index task to a . diff --git a/src/OrchardCore.Modules/OrchardCore.Lucene/Deployment/LuceneIndexRebuildDeploymentStepDriver.cs b/src/OrchardCore.Modules/OrchardCore.Search.Lucene/Deployment/LuceneIndexRebuildDeploymentStepDriver.cs similarity index 95% rename from src/OrchardCore.Modules/OrchardCore.Lucene/Deployment/LuceneIndexRebuildDeploymentStepDriver.cs rename to src/OrchardCore.Modules/OrchardCore.Search.Lucene/Deployment/LuceneIndexRebuildDeploymentStepDriver.cs index 9661dd55044..1d4e339d4a9 100644 --- a/src/OrchardCore.Modules/OrchardCore.Lucene/Deployment/LuceneIndexRebuildDeploymentStepDriver.cs +++ b/src/OrchardCore.Modules/OrchardCore.Search.Lucene/Deployment/LuceneIndexRebuildDeploymentStepDriver.cs @@ -5,9 +5,9 @@ using OrchardCore.DisplayManagement.Handlers; using OrchardCore.DisplayManagement.ModelBinding; using OrchardCore.DisplayManagement.Views; -using OrchardCore.Lucene.ViewModels; +using OrchardCore.Search.Lucene.ViewModels; -namespace OrchardCore.Lucene.Deployment +namespace OrchardCore.Search.Lucene.Deployment { public class LuceneIndexRebuildDeploymentStepDriver : DisplayDriver { diff --git a/src/OrchardCore.Modules/OrchardCore.Lucene/Deployment/LuceneIndexResetDeploymentSource.cs b/src/OrchardCore.Modules/OrchardCore.Search.Lucene/Deployment/LuceneIndexResetDeploymentSource.cs similarity index 95% rename from src/OrchardCore.Modules/OrchardCore.Lucene/Deployment/LuceneIndexResetDeploymentSource.cs rename to src/OrchardCore.Modules/OrchardCore.Search.Lucene/Deployment/LuceneIndexResetDeploymentSource.cs index 9fe316cf552..56583e63c4d 100644 --- a/src/OrchardCore.Modules/OrchardCore.Lucene/Deployment/LuceneIndexResetDeploymentSource.cs +++ b/src/OrchardCore.Modules/OrchardCore.Search.Lucene/Deployment/LuceneIndexResetDeploymentSource.cs @@ -3,7 +3,7 @@ using Newtonsoft.Json.Linq; using OrchardCore.Deployment; -namespace OrchardCore.Lucene.Deployment +namespace OrchardCore.Search.Lucene.Deployment { public class LuceneIndexResetDeploymentSource : IDeploymentSource { diff --git a/src/OrchardCore.Modules/OrchardCore.Lucene/Deployment/LuceneIndexResetDeploymentStep.cs b/src/OrchardCore.Modules/OrchardCore.Search.Lucene/Deployment/LuceneIndexResetDeploymentStep.cs similarity index 90% rename from src/OrchardCore.Modules/OrchardCore.Lucene/Deployment/LuceneIndexResetDeploymentStep.cs rename to src/OrchardCore.Modules/OrchardCore.Search.Lucene/Deployment/LuceneIndexResetDeploymentStep.cs index b96c3fdb5e2..80a878f2163 100644 --- a/src/OrchardCore.Modules/OrchardCore.Lucene/Deployment/LuceneIndexResetDeploymentStep.cs +++ b/src/OrchardCore.Modules/OrchardCore.Search.Lucene/Deployment/LuceneIndexResetDeploymentStep.cs @@ -1,6 +1,6 @@ using OrchardCore.Deployment; -namespace OrchardCore.Lucene.Deployment +namespace OrchardCore.Search.Lucene.Deployment { /// /// Adds reset Lucene index task to a . diff --git a/src/OrchardCore.Modules/OrchardCore.Lucene/Deployment/LuceneIndexResetDeploymentStepDriver.cs b/src/OrchardCore.Modules/OrchardCore.Search.Lucene/Deployment/LuceneIndexResetDeploymentStepDriver.cs similarity index 95% rename from src/OrchardCore.Modules/OrchardCore.Lucene/Deployment/LuceneIndexResetDeploymentStepDriver.cs rename to src/OrchardCore.Modules/OrchardCore.Search.Lucene/Deployment/LuceneIndexResetDeploymentStepDriver.cs index 88b2d404842..f2df2708033 100644 --- a/src/OrchardCore.Modules/OrchardCore.Lucene/Deployment/LuceneIndexResetDeploymentStepDriver.cs +++ b/src/OrchardCore.Modules/OrchardCore.Search.Lucene/Deployment/LuceneIndexResetDeploymentStepDriver.cs @@ -5,9 +5,9 @@ using OrchardCore.DisplayManagement.Handlers; using OrchardCore.DisplayManagement.ModelBinding; using OrchardCore.DisplayManagement.Views; -using OrchardCore.Lucene.ViewModels; +using OrchardCore.Search.Lucene.ViewModels; -namespace OrchardCore.Lucene.Deployment +namespace OrchardCore.Search.Lucene.Deployment { public class LuceneIndexResetDeploymentStepDriver : DisplayDriver { diff --git a/src/OrchardCore.Modules/OrchardCore.Lucene/Deployment/LuceneSettingsDeploymentSource.cs b/src/OrchardCore.Modules/OrchardCore.Search.Lucene/Deployment/LuceneSettingsDeploymentSource.cs similarity index 95% rename from src/OrchardCore.Modules/OrchardCore.Lucene/Deployment/LuceneSettingsDeploymentSource.cs rename to src/OrchardCore.Modules/OrchardCore.Search.Lucene/Deployment/LuceneSettingsDeploymentSource.cs index f8809caf614..8e7f021e4eb 100644 --- a/src/OrchardCore.Modules/OrchardCore.Lucene/Deployment/LuceneSettingsDeploymentSource.cs +++ b/src/OrchardCore.Modules/OrchardCore.Search.Lucene/Deployment/LuceneSettingsDeploymentSource.cs @@ -2,7 +2,7 @@ using Newtonsoft.Json.Linq; using OrchardCore.Deployment; -namespace OrchardCore.Lucene.Deployment +namespace OrchardCore.Search.Lucene.Deployment { public class LuceneSettingsDeploymentSource : IDeploymentSource { diff --git a/src/OrchardCore.Modules/OrchardCore.Lucene/Deployment/LuceneSettingsDeploymentStep.cs b/src/OrchardCore.Modules/OrchardCore.Search.Lucene/Deployment/LuceneSettingsDeploymentStep.cs similarity index 86% rename from src/OrchardCore.Modules/OrchardCore.Lucene/Deployment/LuceneSettingsDeploymentStep.cs rename to src/OrchardCore.Modules/OrchardCore.Search.Lucene/Deployment/LuceneSettingsDeploymentStep.cs index 026880d5a55..711f79457e6 100644 --- a/src/OrchardCore.Modules/OrchardCore.Lucene/Deployment/LuceneSettingsDeploymentStep.cs +++ b/src/OrchardCore.Modules/OrchardCore.Search.Lucene/Deployment/LuceneSettingsDeploymentStep.cs @@ -1,6 +1,6 @@ using OrchardCore.Deployment; -namespace OrchardCore.Lucene.Deployment +namespace OrchardCore.Search.Lucene.Deployment { /// /// Adds layers to a . diff --git a/src/OrchardCore.Modules/OrchardCore.Lucene/Deployment/LuceneSettingsDeploymentStepDriver.cs b/src/OrchardCore.Modules/OrchardCore.Search.Lucene/Deployment/LuceneSettingsDeploymentStepDriver.cs similarity index 94% rename from src/OrchardCore.Modules/OrchardCore.Lucene/Deployment/LuceneSettingsDeploymentStepDriver.cs rename to src/OrchardCore.Modules/OrchardCore.Search.Lucene/Deployment/LuceneSettingsDeploymentStepDriver.cs index 63b93c7e91b..29b5accea18 100644 --- a/src/OrchardCore.Modules/OrchardCore.Lucene/Deployment/LuceneSettingsDeploymentStepDriver.cs +++ b/src/OrchardCore.Modules/OrchardCore.Search.Lucene/Deployment/LuceneSettingsDeploymentStepDriver.cs @@ -2,7 +2,7 @@ using OrchardCore.DisplayManagement.Handlers; using OrchardCore.DisplayManagement.Views; -namespace OrchardCore.Lucene.Deployment +namespace OrchardCore.Search.Lucene.Deployment { public class LuceneSettingsDeploymentStepDriver : DisplayDriver { diff --git a/src/OrchardCore.Modules/OrchardCore.Lucene/Drivers/LuceneQueryDisplayDriver.cs b/src/OrchardCore.Modules/OrchardCore.Search.Lucene/Drivers/LuceneQueryDisplayDriver.cs similarity index 97% rename from src/OrchardCore.Modules/OrchardCore.Lucene/Drivers/LuceneQueryDisplayDriver.cs rename to src/OrchardCore.Modules/OrchardCore.Search.Lucene/Drivers/LuceneQueryDisplayDriver.cs index aab3927f19f..dca3ad9221b 100644 --- a/src/OrchardCore.Modules/OrchardCore.Lucene/Drivers/LuceneQueryDisplayDriver.cs +++ b/src/OrchardCore.Modules/OrchardCore.Search.Lucene/Drivers/LuceneQueryDisplayDriver.cs @@ -5,10 +5,10 @@ using OrchardCore.DisplayManagement.Handlers; using OrchardCore.DisplayManagement.ModelBinding; using OrchardCore.DisplayManagement.Views; -using OrchardCore.Lucene.ViewModels; +using OrchardCore.Search.Lucene.ViewModels; using OrchardCore.Queries; -namespace OrchardCore.Lucene.Drivers +namespace OrchardCore.Search.Lucene.Drivers { public class LuceneQueryDisplayDriver : DisplayDriver { diff --git a/src/OrchardCore.Modules/OrchardCore.Lucene/Drivers/LuceneSettingsDisplayDriver.cs b/src/OrchardCore.Modules/OrchardCore.Search.Lucene/Drivers/LuceneSettingsDisplayDriver.cs similarity index 91% rename from src/OrchardCore.Modules/OrchardCore.Lucene/Drivers/LuceneSettingsDisplayDriver.cs rename to src/OrchardCore.Modules/OrchardCore.Search.Lucene/Drivers/LuceneSettingsDisplayDriver.cs index 36dbba4be7f..b0c668c5180 100644 --- a/src/OrchardCore.Modules/OrchardCore.Lucene/Drivers/LuceneSettingsDisplayDriver.cs +++ b/src/OrchardCore.Modules/OrchardCore.Search.Lucene/Drivers/LuceneSettingsDisplayDriver.cs @@ -6,15 +6,15 @@ using OrchardCore.DisplayManagement.Entities; using OrchardCore.DisplayManagement.Handlers; using OrchardCore.DisplayManagement.Views; -using OrchardCore.Lucene.Model; -using OrchardCore.Lucene.ViewModels; +using OrchardCore.Search.Lucene.Model; +using OrchardCore.Search.Lucene.ViewModels; using OrchardCore.Settings; -namespace OrchardCore.Lucene.Drivers +namespace OrchardCore.Search.Lucene.Drivers { public class LuceneSettingsDisplayDriver : SectionDisplayDriver { - public const string GroupId = "search"; + public const string GroupId = "lucene"; private readonly LuceneIndexSettingsService _luceneIndexSettingsService; private readonly IHttpContextAccessor _httpContextAccessor; private readonly IAuthorizationService _authorizationService; @@ -34,7 +34,7 @@ public override async Task EditAsync(LuceneSettings settings, Bu { var user = _httpContextAccessor.HttpContext?.User; - if (!await _authorizationService.AuthorizeAsync(user, Permissions.ManageIndexes)) + if (!await _authorizationService.AuthorizeAsync(user, Permissions.ManageLuceneIndexes)) { return null; } @@ -52,7 +52,7 @@ public override async Task UpdateAsync(LuceneSettings section, B { var user = _httpContextAccessor.HttpContext?.User; - if (!await _authorizationService.AuthorizeAsync(user, Permissions.ManageIndexes)) + if (!await _authorizationService.AuthorizeAsync(user, Permissions.ManageLuceneIndexes)) { return null; } diff --git a/src/OrchardCore.Modules/OrchardCore.Lucene/GraphQL/LuceneQueryFieldTypeProvider.cs b/src/OrchardCore.Modules/OrchardCore.Search.Lucene/GraphQL/LuceneQueryFieldTypeProvider.cs similarity index 99% rename from src/OrchardCore.Modules/OrchardCore.Lucene/GraphQL/LuceneQueryFieldTypeProvider.cs rename to src/OrchardCore.Modules/OrchardCore.Search.Lucene/GraphQL/LuceneQueryFieldTypeProvider.cs index 238ab12db2d..d2bcdf74c12 100644 --- a/src/OrchardCore.Modules/OrchardCore.Lucene/GraphQL/LuceneQueryFieldTypeProvider.cs +++ b/src/OrchardCore.Modules/OrchardCore.Search.Lucene/GraphQL/LuceneQueryFieldTypeProvider.cs @@ -12,7 +12,7 @@ using OrchardCore.Apis.GraphQL; using OrchardCore.Apis.GraphQL.Resolvers; using OrchardCore.ContentManagement.GraphQL.Queries; -using OrchardCore.Lucene; +using OrchardCore.Search.Lucene; namespace OrchardCore.Queries.Lucene.GraphQL.Queries { @@ -85,6 +85,7 @@ public async Task BuildAsync(ISchema schema) private FieldType BuildSchemaBasedFieldType(LuceneQuery query, JToken querySchema, string fieldTypeName) { var properties = querySchema["properties"]; + if (properties == null) { return null; @@ -162,6 +163,7 @@ private FieldType BuildSchemaBasedFieldType(LuceneQuery query, JToken querySchem private FieldType BuildContentTypeFieldType(ISchema schema, string contentType, LuceneQuery query, string fieldTypeName) { var typetype = schema.Query.Fields.OfType().FirstOrDefault(x => x.Name == contentType); + if (typetype == null) { return null; diff --git a/src/OrchardCore.Modules/OrchardCore.Lucene/GraphQL/Startup.cs b/src/OrchardCore.Modules/OrchardCore.Search.Lucene/GraphQL/Startup.cs similarity index 92% rename from src/OrchardCore.Modules/OrchardCore.Lucene/GraphQL/Startup.cs rename to src/OrchardCore.Modules/OrchardCore.Search.Lucene/GraphQL/Startup.cs index 0cdb4289f4c..431ea5cca7e 100644 --- a/src/OrchardCore.Modules/OrchardCore.Lucene/GraphQL/Startup.cs +++ b/src/OrchardCore.Modules/OrchardCore.Search.Lucene/GraphQL/Startup.cs @@ -3,7 +3,7 @@ using OrchardCore.Modules; using OrchardCore.Queries.Lucene.GraphQL.Queries; -namespace OrchardCore.Lucene.GraphQL +namespace OrchardCore.Search.Lucene.GraphQL { /// /// These services are registered on the tenant service collection diff --git a/src/OrchardCore.Modules/OrchardCore.Lucene/Handler/LuceneIndexingContentHandler.cs b/src/OrchardCore.Modules/OrchardCore.Search.Lucene/Handler/LuceneIndexingContentHandler.cs similarity index 95% rename from src/OrchardCore.Modules/OrchardCore.Lucene/Handler/LuceneIndexingContentHandler.cs rename to src/OrchardCore.Modules/OrchardCore.Search.Lucene/Handler/LuceneIndexingContentHandler.cs index c6b8967bb39..2c994315f77 100644 --- a/src/OrchardCore.Modules/OrchardCore.Lucene/Handler/LuceneIndexingContentHandler.cs +++ b/src/OrchardCore.Modules/OrchardCore.Search.Lucene/Handler/LuceneIndexingContentHandler.cs @@ -12,8 +12,9 @@ using OrchardCore.Environment.Shell.Scope; using OrchardCore.Indexing; using OrchardCore.Modules; +using OrchardCore.Search.Lucene.Model; -namespace OrchardCore.Lucene.Handlers +namespace OrchardCore.Search.Lucene.Handlers { public class LuceneIndexingContentHandler : ContentHandlerBase { @@ -106,7 +107,7 @@ private static async Task IndexingAsync(ShellScope scope, IEnumerable x.BuildIndexAsync(buildIndexContext), logger); await luceneIndexManager.DeleteDocumentsAsync(indexSettings.IndexName, new string[] { contentItem.ContentItemId }); diff --git a/src/OrchardCore.Modules/OrchardCore.Lucene/Manifest.cs b/src/OrchardCore.Modules/OrchardCore.Search.Lucene/Manifest.cs similarity index 69% rename from src/OrchardCore.Modules/OrchardCore.Lucene/Manifest.cs rename to src/OrchardCore.Modules/OrchardCore.Search.Lucene/Manifest.cs index 8a7d5f3a799..b3f4792bf06 100644 --- a/src/OrchardCore.Modules/OrchardCore.Lucene/Manifest.cs +++ b/src/OrchardCore.Modules/OrchardCore.Search.Lucene/Manifest.cs @@ -8,7 +8,7 @@ )] [assembly: Feature( - Id = "OrchardCore.Lucene", + Id = "OrchardCore.Search.Lucene", Name = "Lucene", Description = "Creates Lucene indexes to support search scenarios, introduces a preconfigured container-enabled content type.", Dependencies = new[] @@ -16,21 +16,21 @@ "OrchardCore.Indexing", "OrchardCore.ContentTypes" }, - Category = "Content Management" + Category = "Search" )] [assembly: Feature( - Id = "OrchardCore.Lucene.Worker", + Id = "OrchardCore.Search.Lucene.Worker", Name = "Lucene Worker", Description = "Provides a background task to keep local indices in sync with other instances.", - Dependencies = new[] { "OrchardCore.Lucene" }, - Category = "Content Management" + Dependencies = new[] { "OrchardCore.Search.Lucene" }, + Category = "Search" )] [assembly: Feature( - Id = "OrchardCore.Lucene.ContentPicker", + Id = "OrchardCore.Search.Lucene.ContentPicker", Name = "Lucene Content Picker", Description = "Provides a Lucene content picker field editor.", - Dependencies = new[] { "OrchardCore.Lucene", "OrchardCore.ContentFields" }, - Category = "Content Management" + Dependencies = new[] { "OrchardCore.Search.Lucene", "OrchardCore.ContentFields" }, + Category = "Search" )] diff --git a/src/OrchardCore.Modules/OrchardCore.Search.Lucene/Migrations.cs b/src/OrchardCore.Modules/OrchardCore.Search.Lucene/Migrations.cs new file mode 100644 index 00000000000..fa423eada52 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Search.Lucene/Migrations.cs @@ -0,0 +1,202 @@ +using System; +using Dapper; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json.Linq; +using OrchardCore.ContentManagement.Metadata; +using OrchardCore.Data; +using OrchardCore.Data.Migration; +using OrchardCore.Environment.Shell; +using OrchardCore.Environment.Shell.Scope; +using OrchardCore.Search.Lucene.Model; +using YesSql; + +namespace OrchardCore.Search.Lucene +{ + public class Migrations : DataMigration + { + private readonly IContentDefinitionManager _contentDefinitionManager; + + public Migrations(IContentDefinitionManager contentDefinitionManager) + { + _contentDefinitionManager = contentDefinitionManager; + } + + public int Create() + { + var contentTypeDefinitions = _contentDefinitionManager.LoadTypeDefinitions(); + + foreach (var contentTypeDefinition in contentTypeDefinitions) + { + foreach (var partDefinition in contentTypeDefinition.Parts) + { + _contentDefinitionManager.AlterPartDefinition(partDefinition.Name, partBuilder => + { + if (partDefinition.Settings.TryGetValue("ContentIndexSettings", out var existingPartSettings) && + !partDefinition.Settings.ContainsKey(nameof(LuceneContentIndexSettings))) + { + var included = existingPartSettings["Included"]; + var analyzed = existingPartSettings["Analyzed"]; + + if (included != null) + { + if (analyzed != null) + { + if ((bool)included) + { + existingPartSettings["Keyword"] = true; + } + } + else + { + if ((bool)included && !(bool)analyzed) + { + existingPartSettings["Keyword"] = true; + } + } + } + + // We remove unecessary properties from old releases. + existingPartSettings["Analyzed"]?.Parent.Remove(); + existingPartSettings["Tokenized"]?.Parent.Remove(); + existingPartSettings["Template"]?.Parent.Remove(); + + partDefinition.Settings.Add(new JProperty(nameof(LuceneContentIndexSettings), existingPartSettings)); + } + + partDefinition.Settings.Remove("ContentIndexSettings"); + }); + } + } + + var partDefinitions = _contentDefinitionManager.LoadPartDefinitions(); + + foreach (var partDefinition in partDefinitions) + { + _contentDefinitionManager.AlterPartDefinition(partDefinition.Name, partBuilder => + { + if (partDefinition.Settings.TryGetValue("ContentIndexSettings", out var existingPartSettings) && + !partDefinition.Settings.ContainsKey(nameof(LuceneContentIndexSettings))) + { + var included = existingPartSettings["Included"]; + var analyzed = existingPartSettings["Analyzed"]; + + if (included != null) + { + if (analyzed != null) + { + if ((bool)included) + { + existingPartSettings["Keyword"] = true; + } + } + else + { + if ((bool)included && !(bool)analyzed) + { + existingPartSettings["Keyword"] = true; + } + } + } + + // We remove unecessary properties from old releases. + existingPartSettings["Analyzed"]?.Parent.Remove(); + existingPartSettings["Tokenized"]?.Parent.Remove(); + existingPartSettings["Template"]?.Parent.Remove(); + + partDefinition.Settings.Add(new JProperty(nameof(LuceneContentIndexSettings), existingPartSettings)); + } + + partDefinition.Settings.Remove("ContentIndexSettings"); + + foreach (var fieldDefinition in partDefinition.Fields) + { + if (fieldDefinition.Settings.TryGetValue("ContentIndexSettings", out var existingFieldSettings) + && !fieldDefinition.Settings.TryGetValue(nameof(LuceneContentIndexSettings), out var existingLuceneFieldSettings)) + { + var included = existingFieldSettings["Included"]; + var analyzed = existingFieldSettings["Analyzed"]; + + if (included != null) + { + if (analyzed == null) + { + if ((bool)included) + { + existingFieldSettings["Keyword"] = true; + } + } + else + { + if ((bool)included && !(bool)analyzed) + { + existingFieldSettings["Keyword"] = true; + } + } + } + + // We remove unecessary properties from old releases. + existingFieldSettings["Analyzed"]?.Parent.Remove(); + existingFieldSettings["Tokenized"]?.Parent.Remove(); + existingFieldSettings["Template"]?.Parent.Remove(); + + fieldDefinition.Settings.Add(new JProperty(nameof(LuceneContentIndexSettings), existingFieldSettings)); + } + + fieldDefinition.Settings.Remove("ContentIndexSettings"); + } + }); + } + + // Defer this until after the subsequent migrations have succeeded as the schema has changed. + ShellScope.AddDeferredTask(async scope => + { + var session = scope.ServiceProvider.GetRequiredService(); + var dbConnectionAccessor = scope.ServiceProvider.GetService(); + var shellSettings = scope.ServiceProvider.GetService(); + var logger = scope.ServiceProvider.GetService>(); + var tablePrefix = session.Store.Configuration.TablePrefix; + var documentTableName = session.Store.Configuration.TableNameConvention.GetDocumentTable(); + + var table = $"{session.Store.Configuration.TablePrefix}{documentTableName}"; + + using (var connection = dbConnectionAccessor.CreateConnection()) + { + await connection.OpenAsync(); + + using (var transaction = connection.BeginTransaction(session.Store.Configuration.IsolationLevel)) + { + var dialect = session.Store.Configuration.SqlDialect; + + try + { + if (logger.IsEnabled(LogLevel.Debug)) + { + logger.LogDebug("Updating Lucene indices settings and queries"); + } + + var updateCmd = $"UPDATE {dialect.QuoteForTableName(table)} SET Content = REPLACE(content, '\"$type\":\"OrchardCore.Lucene.LuceneQuery, OrchardCore.Lucene\"', '\"$type\":\"OrchardCore.Search.Lucene.LuceneQuery, OrchardCore.Search.Lucene\"') WHERE [Type] = 'OrchardCore.Queries.Services.QueriesDocument, OrchardCore.Queries'"; + + await transaction.Connection.ExecuteAsync(updateCmd, null, transaction); + + updateCmd = $"UPDATE {dialect.QuoteForTableName(table)} SET [Type] = 'OrchardCore.Search.Lucene.Model.LuceneIndexSettingsDocument, OrchardCore.Search.Lucene' WHERE [Type] = 'OrchardCore.Lucene.Model.LuceneIndexSettingsDocument, OrchardCore.Lucene'"; + + await transaction.Connection.ExecuteAsync(updateCmd, null, transaction); + + transaction.Commit(); + } + catch (Exception e) + { + transaction.Rollback(); + logger.LogError(e, "An error occurred while updating Lucene indices settings and queries"); + + throw; + } + } + } + }); + + return 1; + } + } +} diff --git a/src/OrchardCore.Modules/OrchardCore.Search.Lucene/Model/LuceneContentIndexSettings.cs b/src/OrchardCore.Modules/OrchardCore.Search.Lucene/Model/LuceneContentIndexSettings.cs new file mode 100644 index 00000000000..ef7494bb135 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Search.Lucene/Model/LuceneContentIndexSettings.cs @@ -0,0 +1,31 @@ +using OrchardCore.Indexing; + +namespace OrchardCore.Search.Lucene.Model +{ + /// + /// Represents the indexing settings for a content part or a field. + /// + public class LuceneContentIndexSettings : IContentIndexSettings + { + public bool Included { get; set; } + public bool Stored { get; set; } + public bool Keyword { get; set; } + + public DocumentIndexOptions ToOptions() + { + var options = DocumentIndexOptions.None; + + if (Stored) + { + options |= DocumentIndexOptions.Store; + } + + if (Keyword) + { + options |= DocumentIndexOptions.Keyword; + } + + return options; + } + } +} diff --git a/src/OrchardCore.Modules/OrchardCore.Lucene/Model/LuceneIndexSettings.cs b/src/OrchardCore.Modules/OrchardCore.Search.Lucene/Model/LuceneIndexSettings.cs similarity index 86% rename from src/OrchardCore.Modules/OrchardCore.Lucene/Model/LuceneIndexSettings.cs rename to src/OrchardCore.Modules/OrchardCore.Search.Lucene/Model/LuceneIndexSettings.cs index d9bb0cd7c53..0f176658703 100644 --- a/src/OrchardCore.Modules/OrchardCore.Lucene/Model/LuceneIndexSettings.cs +++ b/src/OrchardCore.Modules/OrchardCore.Search.Lucene/Model/LuceneIndexSettings.cs @@ -2,7 +2,7 @@ using Newtonsoft.Json; using OrchardCore.Data.Documents; -namespace OrchardCore.Lucene.Model +namespace OrchardCore.Search.Lucene.Model { public class LuceneIndexSettings { @@ -16,6 +16,8 @@ public class LuceneIndexSettings public string[] IndexedContentTypes { get; set; } public string Culture { get; set; } + + public bool StoreSourceData { get; set; } } public class LuceneIndexSettingsDocument : Document diff --git a/src/OrchardCore.Modules/OrchardCore.Lucene/Model/LuceneQueryModel.cs b/src/OrchardCore.Modules/OrchardCore.Search.Lucene/Model/LuceneQueryModel.cs similarity index 81% rename from src/OrchardCore.Modules/OrchardCore.Lucene/Model/LuceneQueryModel.cs rename to src/OrchardCore.Modules/OrchardCore.Search.Lucene/Model/LuceneQueryModel.cs index fb86fdbe03b..58ee62a75e1 100644 --- a/src/OrchardCore.Modules/OrchardCore.Lucene/Model/LuceneQueryModel.cs +++ b/src/OrchardCore.Modules/OrchardCore.Search.Lucene/Model/LuceneQueryModel.cs @@ -1,4 +1,4 @@ -namespace OrchardCore.Lucene.Model +namespace OrchardCore.Search.Lucene.Model { public class LuceneQueryModel { diff --git a/src/OrchardCore.Modules/OrchardCore.Lucene/Model/LuceneSettings.cs b/src/OrchardCore.Modules/OrchardCore.Search.Lucene/Model/LuceneSettings.cs similarity index 92% rename from src/OrchardCore.Modules/OrchardCore.Lucene/Model/LuceneSettings.cs rename to src/OrchardCore.Modules/OrchardCore.Search.Lucene/Model/LuceneSettings.cs index 3ebdce357d7..2975f8b4a4e 100644 --- a/src/OrchardCore.Modules/OrchardCore.Lucene/Model/LuceneSettings.cs +++ b/src/OrchardCore.Modules/OrchardCore.Search.Lucene/Model/LuceneSettings.cs @@ -1,7 +1,7 @@ using Lucene.Net.Util; using OrchardCore.Contents.Indexing; -namespace OrchardCore.Lucene.Model +namespace OrchardCore.Search.Lucene.Model { public class LuceneSettings { diff --git a/src/OrchardCore.Modules/OrchardCore.Lucene/OrchardCore.Lucene.csproj b/src/OrchardCore.Modules/OrchardCore.Search.Lucene/OrchardCore.Search.Lucene.csproj similarity index 94% rename from src/OrchardCore.Modules/OrchardCore.Lucene/OrchardCore.Lucene.csproj rename to src/OrchardCore.Modules/OrchardCore.Search.Lucene/OrchardCore.Search.Lucene.csproj index 2583dfe9e3f..85d650345f4 100644 --- a/src/OrchardCore.Modules/OrchardCore.Lucene/OrchardCore.Lucene.csproj +++ b/src/OrchardCore.Modules/OrchardCore.Search.Lucene/OrchardCore.Search.Lucene.csproj @@ -3,7 +3,7 @@ true - OrchardCore Lucene + OrchardCore Lucene Search $(OCCMSDescription) Creates Lucene indexes to support search scenarios, introduces a preconfigured container-enabled content type. @@ -29,12 +29,12 @@ - + diff --git a/src/OrchardCore.Modules/OrchardCore.Lucene/Permissions.cs b/src/OrchardCore.Modules/OrchardCore.Search.Lucene/Permissions.cs similarity index 79% rename from src/OrchardCore.Modules/OrchardCore.Lucene/Permissions.cs rename to src/OrchardCore.Modules/OrchardCore.Search.Lucene/Permissions.cs index 8bc10cd3edf..2d2a53cdb52 100644 --- a/src/OrchardCore.Modules/OrchardCore.Lucene/Permissions.cs +++ b/src/OrchardCore.Modules/OrchardCore.Search.Lucene/Permissions.cs @@ -2,14 +2,14 @@ using System.Threading.Tasks; using OrchardCore.Security.Permissions; -namespace OrchardCore.Lucene +namespace OrchardCore.Search.Lucene { public class Permissions : IPermissionProvider { private readonly LuceneIndexSettingsService _luceneIndexSettingsService; - public static readonly Permission ManageIndexes = new Permission("ManageIndexes", "Manage Indexes"); - public static readonly Permission QueryLuceneApi = new Permission("QueryLuceneApi", "Query Lucene Api", new[] { ManageIndexes }); + public static readonly Permission ManageLuceneIndexes = new Permission("ManageLuceneIndexes", "Manage Lucene Indexes"); + public static readonly Permission QueryLuceneApi = new Permission("QueryLuceneApi", "Query Lucene Api", new[] { ManageLuceneIndexes }); public Permissions(LuceneIndexSettingsService luceneIndexSettingsService) { @@ -22,11 +22,11 @@ public async Task> GetPermissionsAsync() var result = new List(); foreach (var index in luceneIndexSettings) { - var permission = new Permission("QueryLucene" + index.IndexName + "Index", "Query Lucene " + index.IndexName + " Index", new[] { ManageIndexes }); + var permission = new Permission("QueryLucene" + index.IndexName + "Index", "Query Lucene " + index.IndexName + " Index", new[] { ManageLuceneIndexes }); result.Add(permission); } - result.Add(ManageIndexes); + result.Add(ManageLuceneIndexes); result.Add(QueryLuceneApi); return result; @@ -39,7 +39,7 @@ public IEnumerable GetDefaultStereotypes() new PermissionStereotype { Name = "Administrator", - Permissions = new[] { ManageIndexes } + Permissions = new[] { ManageLuceneIndexes } }, new PermissionStereotype { diff --git a/src/OrchardCore.Modules/OrchardCore.Search.Lucene/Properties/AssemblyInfo.cs b/src/OrchardCore.Modules/OrchardCore.Search.Lucene/Properties/AssemblyInfo.cs new file mode 100644 index 00000000000..8712a34fe03 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Search.Lucene/Properties/AssemblyInfo.cs @@ -0,0 +1,18 @@ +using System.Reflection; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("OrchardCore.Search.Lucene")] +[assembly: AssemblyTrademark("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("23b628d8-4dcf-4ee1-aea2-8cff6c8b80b0")] diff --git a/src/OrchardCore.Modules/OrchardCore.Lucene/Recipes/LuceneIndexRebuildStep.cs b/src/OrchardCore.Modules/OrchardCore.Search.Lucene/Recipes/LuceneIndexRebuildStep.cs similarity index 97% rename from src/OrchardCore.Modules/OrchardCore.Lucene/Recipes/LuceneIndexRebuildStep.cs rename to src/OrchardCore.Modules/OrchardCore.Search.Lucene/Recipes/LuceneIndexRebuildStep.cs index 884dfcf2b27..b8533a6b1f8 100644 --- a/src/OrchardCore.Modules/OrchardCore.Lucene/Recipes/LuceneIndexRebuildStep.cs +++ b/src/OrchardCore.Modules/OrchardCore.Search.Lucene/Recipes/LuceneIndexRebuildStep.cs @@ -6,7 +6,7 @@ using OrchardCore.Recipes.Models; using OrchardCore.Recipes.Services; -namespace OrchardCore.Lucene.Recipes +namespace OrchardCore.Search.Lucene.Recipes { /// /// This recipe step rebuilds a lucene index. diff --git a/src/OrchardCore.Modules/OrchardCore.Lucene/Recipes/LuceneIndexResetStep.cs b/src/OrchardCore.Modules/OrchardCore.Search.Lucene/Recipes/LuceneIndexResetStep.cs similarity index 97% rename from src/OrchardCore.Modules/OrchardCore.Lucene/Recipes/LuceneIndexResetStep.cs rename to src/OrchardCore.Modules/OrchardCore.Search.Lucene/Recipes/LuceneIndexResetStep.cs index 5f8113f5ae3..9c0eaf54ea1 100644 --- a/src/OrchardCore.Modules/OrchardCore.Lucene/Recipes/LuceneIndexResetStep.cs +++ b/src/OrchardCore.Modules/OrchardCore.Search.Lucene/Recipes/LuceneIndexResetStep.cs @@ -6,7 +6,7 @@ using OrchardCore.Recipes.Models; using OrchardCore.Recipes.Services; -namespace OrchardCore.Lucene.Recipes +namespace OrchardCore.Search.Lucene.Recipes { /// /// This recipe step resets a lucene index. @@ -42,7 +42,7 @@ await HttpBackgroundJob.ExecuteAfterEndOfRequestAsync("lucene-index-reset", asyn } else { - luceneIndexingService.ResetIndex(indexName); + luceneIndexingService.ResetIndexAsync(indexName); } await luceneIndexingService.ProcessContentItemsAsync(indexName); } diff --git a/src/OrchardCore.Modules/OrchardCore.Lucene/Recipes/LuceneIndexStep.cs b/src/OrchardCore.Modules/OrchardCore.Search.Lucene/Recipes/LuceneIndexStep.cs similarity index 95% rename from src/OrchardCore.Modules/OrchardCore.Lucene/Recipes/LuceneIndexStep.cs rename to src/OrchardCore.Modules/OrchardCore.Search.Lucene/Recipes/LuceneIndexStep.cs index a7ecc5520c4..83f5ed7954c 100644 --- a/src/OrchardCore.Modules/OrchardCore.Lucene/Recipes/LuceneIndexStep.cs +++ b/src/OrchardCore.Modules/OrchardCore.Search.Lucene/Recipes/LuceneIndexStep.cs @@ -3,11 +3,11 @@ using System.Linq; using System.Threading.Tasks; using Newtonsoft.Json.Linq; -using OrchardCore.Lucene.Model; +using OrchardCore.Search.Lucene.Model; using OrchardCore.Recipes.Models; using OrchardCore.Recipes.Services; -namespace OrchardCore.Lucene.Recipes +namespace OrchardCore.Search.Lucene.Recipes { /// /// This recipe step creates a lucene index. diff --git a/src/OrchardCore.Modules/OrchardCore.Lucene/Services/ISearchQueryService.cs b/src/OrchardCore.Modules/OrchardCore.Search.Lucene/Services/ILuceneSearchQueryService.cs similarity index 69% rename from src/OrchardCore.Modules/OrchardCore.Lucene/Services/ISearchQueryService.cs rename to src/OrchardCore.Modules/OrchardCore.Search.Lucene/Services/ILuceneSearchQueryService.cs index a8ec2fd2304..32d8ac4be00 100644 --- a/src/OrchardCore.Modules/OrchardCore.Lucene/Services/ISearchQueryService.cs +++ b/src/OrchardCore.Modules/OrchardCore.Search.Lucene/Services/ILuceneSearchQueryService.cs @@ -2,9 +2,9 @@ using System.Threading.Tasks; using Lucene.Net.Search; -namespace OrchardCore.Lucene.Services +namespace OrchardCore.Search.Lucene.Services { - public interface ISearchQueryService + public interface ILuceneSearchQueryService { Task> ExecuteQueryAsync(Query query, string indexName, int start = 0, int end = 20); } diff --git a/src/OrchardCore.Modules/OrchardCore.Lucene/Services/IndexingBackgroundTask.cs b/src/OrchardCore.Modules/OrchardCore.Search.Lucene/Services/IndexingBackgroundTask.cs similarity index 77% rename from src/OrchardCore.Modules/OrchardCore.Lucene/Services/IndexingBackgroundTask.cs rename to src/OrchardCore.Modules/OrchardCore.Search.Lucene/Services/IndexingBackgroundTask.cs index c44c99cfa3a..629148bf77f 100644 --- a/src/OrchardCore.Modules/OrchardCore.Lucene/Services/IndexingBackgroundTask.cs +++ b/src/OrchardCore.Modules/OrchardCore.Search.Lucene/Services/IndexingBackgroundTask.cs @@ -4,13 +4,13 @@ using Microsoft.Extensions.DependencyInjection; using OrchardCore.BackgroundTasks; -namespace OrchardCore.Lucene +namespace OrchardCore.Search.Lucene { /// - /// This background task will index content items using. + /// This background task will index content items using Lucene. /// /// - /// This services is only registered from OrchardCore.Lucene.Worker feature. + /// This services is only registered from OrchardCore.Search.Lucene.Worker feature. /// [BackgroundTask(Schedule = "* * * * *", Description = "Update lucene indexes.")] public class IndexingBackgroundTask : IBackgroundTask diff --git a/src/OrchardCore.Modules/OrchardCore.Lucene/Services/LuceneAnalyzer.cs b/src/OrchardCore.Modules/OrchardCore.Search.Lucene/Services/LuceneAnalyzer.cs similarity index 92% rename from src/OrchardCore.Modules/OrchardCore.Lucene/Services/LuceneAnalyzer.cs rename to src/OrchardCore.Modules/OrchardCore.Search.Lucene/Services/LuceneAnalyzer.cs index 8f4ef3c05f3..1ab4662d7d3 100644 --- a/src/OrchardCore.Modules/OrchardCore.Lucene/Services/LuceneAnalyzer.cs +++ b/src/OrchardCore.Modules/OrchardCore.Search.Lucene/Services/LuceneAnalyzer.cs @@ -1,7 +1,7 @@ using System; using Lucene.Net.Analysis; -namespace OrchardCore.Lucene.Services +namespace OrchardCore.Search.Lucene.Services { public class LuceneAnalyzer : ILuceneAnalyzer { diff --git a/src/OrchardCore.Modules/OrchardCore.Lucene/Services/LuceneAnalyzerManager.cs b/src/OrchardCore.Modules/OrchardCore.Search.Lucene/Services/LuceneAnalyzerManager.cs similarity index 96% rename from src/OrchardCore.Modules/OrchardCore.Lucene/Services/LuceneAnalyzerManager.cs rename to src/OrchardCore.Modules/OrchardCore.Search.Lucene/Services/LuceneAnalyzerManager.cs index 0c26469e72e..55135882eb2 100644 --- a/src/OrchardCore.Modules/OrchardCore.Lucene/Services/LuceneAnalyzerManager.cs +++ b/src/OrchardCore.Modules/OrchardCore.Search.Lucene/Services/LuceneAnalyzerManager.cs @@ -3,7 +3,7 @@ using Lucene.Net.Analysis; using Microsoft.Extensions.Options; -namespace OrchardCore.Lucene.Services +namespace OrchardCore.Search.Lucene.Services { /// /// Coordinates implementations provided by diff --git a/src/OrchardCore.Modules/OrchardCore.Lucene/Services/LuceneContentPickerResultProvider.cs b/src/OrchardCore.Modules/OrchardCore.Search.Lucene/Services/LuceneContentPickerResultProvider.cs similarity index 73% rename from src/OrchardCore.Modules/OrchardCore.Lucene/Services/LuceneContentPickerResultProvider.cs rename to src/OrchardCore.Modules/OrchardCore.Search.Lucene/Services/LuceneContentPickerResultProvider.cs index f6ce2ab9351..2b3c0c5e73f 100644 --- a/src/OrchardCore.Modules/OrchardCore.Lucene/Services/LuceneContentPickerResultProvider.cs +++ b/src/OrchardCore.Modules/OrchardCore.Search.Lucene/Services/LuceneContentPickerResultProvider.cs @@ -1,20 +1,21 @@ +using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Lucene.Net.Index; using Lucene.Net.Search; using OrchardCore.ContentManagement; -using OrchardCore.Lucene.Settings; +using OrchardCore.Search.Lucene.Settings; -namespace OrchardCore.Lucene.Services +namespace OrchardCore.Search.Lucene.Services { public class LuceneContentPickerResultProvider : IContentPickerResultProvider { - private readonly LuceneIndexManager _luceneIndexProvider; + private readonly LuceneIndexManager _luceneIndexManager; - public LuceneContentPickerResultProvider(LuceneIndexManager luceneIndexProvider) + public LuceneContentPickerResultProvider(LuceneIndexManager luceneIndexManager) { - _luceneIndexProvider = luceneIndexProvider; + _luceneIndexManager = luceneIndexManager; } public string Name => "Lucene"; @@ -24,29 +25,30 @@ public async Task> Search(ContentPickerSearchCo var indexName = "Search"; var fieldSettings = searchContext.PartFieldDefinition?.GetSettings(); - if (!string.IsNullOrWhiteSpace(fieldSettings?.Index)) + + if (!String.IsNullOrWhiteSpace(fieldSettings?.Index)) { indexName = fieldSettings.Index; } - if (!_luceneIndexProvider.Exists(indexName)) + if (!_luceneIndexManager.Exists(indexName)) { return new List(); } var results = new List(); - await _luceneIndexProvider.SearchAsync(indexName, searcher => + await _luceneIndexManager.SearchAsync(indexName, searcher => { Query query = null; - if (string.IsNullOrWhiteSpace(searchContext.Query)) + if (String.IsNullOrWhiteSpace(searchContext.Query)) { query = new MatchAllDocsQuery(); } else { - query = new WildcardQuery(new Term("Content.ContentItem.DisplayText.Analyzed", searchContext.Query.ToLowerInvariant() + "*")); + query = new WildcardQuery(new Term("Content.ContentItem.DisplayText.Normalized", searchContext.Query.ToLowerInvariant() + "*")); } var filter = new FieldCacheTermsFilter("Content.ContentItem.ContentType", searchContext.ContentTypes.ToArray()); @@ -60,8 +62,8 @@ await _luceneIndexProvider.SearchAsync(indexName, searcher => results.Add(new ContentPickerResult { ContentItemId = doc.GetField("ContentItemId").GetStringValue(), - DisplayText = doc.GetField("Content.ContentItem.DisplayText").GetStringValue(), - HasPublished = doc.GetField("Content.ContentItem.Published").GetStringValue() == "true" ? true : false + DisplayText = doc.GetField("Content.ContentItem.DisplayText.keyword").GetStringValue(), + HasPublished = doc.GetField("Content.ContentItem.Published").GetStringValue().ToLower() == "true" }); } diff --git a/src/OrchardCore.Modules/OrchardCore.Lucene/Services/LuceneIndexInitializerService.cs b/src/OrchardCore.Modules/OrchardCore.Search.Lucene/Services/LuceneIndexInitializerService.cs similarity index 98% rename from src/OrchardCore.Modules/OrchardCore.Lucene/Services/LuceneIndexInitializerService.cs rename to src/OrchardCore.Modules/OrchardCore.Search.Lucene/Services/LuceneIndexInitializerService.cs index 3166efb7956..d45bcd3838e 100644 --- a/src/OrchardCore.Modules/OrchardCore.Lucene/Services/LuceneIndexInitializerService.cs +++ b/src/OrchardCore.Modules/OrchardCore.Search.Lucene/Services/LuceneIndexInitializerService.cs @@ -5,7 +5,7 @@ using OrchardCore.Environment.Shell.Scope; using OrchardCore.Modules; -namespace OrchardCore.Lucene +namespace OrchardCore.Search.Lucene { public class LuceneIndexInitializerService : IModularTenantEvents { diff --git a/src/OrchardCore.Modules/OrchardCore.Lucene/Services/LuceneIndexManager.cs b/src/OrchardCore.Modules/OrchardCore.Search.Lucene/Services/LuceneIndexManager.cs similarity index 82% rename from src/OrchardCore.Modules/OrchardCore.Lucene/Services/LuceneIndexManager.cs rename to src/OrchardCore.Modules/OrchardCore.Search.Lucene/Services/LuceneIndexManager.cs index 77463d38257..d03ea9934ec 100644 --- a/src/OrchardCore.Modules/OrchardCore.Lucene/Services/LuceneIndexManager.cs +++ b/src/OrchardCore.Modules/OrchardCore.Search.Lucene/Services/LuceneIndexManager.cs @@ -14,16 +14,17 @@ using Lucene.Net.Store; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; +using OrchardCore.Contents.Indexing; using OrchardCore.Environment.Shell; using OrchardCore.Indexing; -using OrchardCore.Lucene.Model; -using OrchardCore.Lucene.Services; using OrchardCore.Modules; +using OrchardCore.Search.Lucene.Model; +using OrchardCore.Search.Lucene.Services; using Spatial4n.Context; using Directory = System.IO.Directory; using LDirectory = Lucene.Net.Store.Directory; -namespace OrchardCore.Lucene +namespace OrchardCore.Search.Lucene { /// /// Provides methods to manage physical Lucene indices. @@ -139,11 +140,13 @@ public bool Exists(string indexName) public async Task StoreDocumentsAsync(string indexName, IEnumerable indexDocuments) { + var luceneIndexSettings = await _luceneIndexSettingsService.GetSettingsAsync(indexName); + await WriteAsync(indexName, writer => { foreach (var indexDocument in indexDocuments) { - writer.AddDocument(CreateLuceneDocument(indexDocument)); + writer.AddDocument(CreateLuceneDocument(indexDocument, luceneIndexSettings)); } writer.Commit(); @@ -191,14 +194,21 @@ public IReadOnlyDictionary GetTimestamps() return new ReadOnlyDictionary(_timestamps); } - private Document CreateLuceneDocument(DocumentIndex documentIndex) + private Document CreateLuceneDocument(DocumentIndex documentIndex, LuceneIndexSettings indexSettings) { var doc = new Document { - // Always store the content item id - new StringField("ContentItemId", documentIndex.ContentItemId.ToString(), Field.Store.YES) + // Always store the content item id and version id + new StoredField("ContentItemId", documentIndex.ContentItemId.ToString()), + new StoredField("ContentItemVersionId", documentIndex.ContentItemVersionId.ToString()) }; + if (indexSettings.StoreSourceData) + { + doc.Add(new StoredField(IndexingConstants.SourceKey + "ContentItemId", documentIndex.ContentItemId.ToString())); + doc.Add(new StoredField(IndexingConstants.SourceKey + "ContentItemVersionId", documentIndex.ContentItemVersionId.ToString())); + } + foreach (var entry in documentIndex.Entries) { var store = entry.Options.HasFlag(DocumentIndexOptions.Store) @@ -210,6 +220,11 @@ private Document CreateLuceneDocument(DocumentIndex documentIndex) case DocumentIndex.Types.Boolean: // Store "true"/"false" for booleans doc.Add(new StringField(entry.Name, Convert.ToString(entry.Value).ToLowerInvariant(), store)); + + if (indexSettings.StoreSourceData) + { + doc.Add(new StoredField(IndexingConstants.SourceKey + entry.Name, Convert.ToString(entry.Value).ToLowerInvariant())); + } break; case DocumentIndex.Types.DateTime: @@ -223,6 +238,18 @@ private Document CreateLuceneDocument(DocumentIndex documentIndex) { doc.Add(new StringField(entry.Name, DateTools.DateToString(((DateTime)entry.Value).ToUniversalTime(), DateResolution.SECOND), store)); } + + if (indexSettings.StoreSourceData) + { + if (entry.Value is DateTimeOffset) + { + doc.Add(new StoredField(IndexingConstants.SourceKey + entry.Name, DateTools.DateToString(((DateTimeOffset)entry.Value).UtcDateTime, DateResolution.SECOND))); + } + else + { + doc.Add(new StoredField(IndexingConstants.SourceKey + entry.Name, DateTools.DateToString(((DateTime)entry.Value).ToUniversalTime(), DateResolution.SECOND))); + } + } } else { @@ -234,6 +261,11 @@ private Document CreateLuceneDocument(DocumentIndex documentIndex) if (entry.Value != null && Int64.TryParse(entry.Value.ToString(), out var value)) { doc.Add(new Int64Field(entry.Name, value, store)); + + if (indexSettings.StoreSourceData) + { + doc.Add(new StoredField(IndexingConstants.SourceKey + entry.Name, value)); + } } else { @@ -246,6 +278,11 @@ private Document CreateLuceneDocument(DocumentIndex documentIndex) if (entry.Value != null) { doc.Add(new DoubleField(entry.Name, Convert.ToDouble(entry.Value), store)); + + if (indexSettings.StoreSourceData) + { + doc.Add(new StoredField(IndexingConstants.SourceKey + entry.Name, Convert.ToDouble(entry.Value))); + } } else { @@ -256,30 +293,46 @@ private Document CreateLuceneDocument(DocumentIndex documentIndex) case DocumentIndex.Types.Text: if (entry.Value != null && !String.IsNullOrEmpty(Convert.ToString(entry.Value))) { - if (entry.Options.HasFlag(DocumentIndexOptions.Analyze)) + var stringValue = Convert.ToString(entry.Value); + + if (entry.Options.HasFlag(DocumentIndexOptions.Keyword)) { - doc.Add(new TextField(entry.Name, Convert.ToString(entry.Value), store)); + doc.Add(new StringField(entry.Name, stringValue, store)); } else { - doc.Add(new StringField(entry.Name, Convert.ToString(entry.Value), store)); + doc.Add(new TextField(entry.Name, stringValue, store)); + } + + // This is for ElasticSearch Queries compatibility since a keyword field is always indexed + // by default when indexing without explicit mapping in ElasticSearch. + // Keyword ignore above 256 chars by default. + if (store == Field.Store.NO && !entry.Options.HasFlag(DocumentIndexOptions.Keyword) && stringValue.Length <= 256) + { + doc.Add(new StringField($"{entry.Name}.keyword", stringValue, Field.Store.NO)); + } + + if (indexSettings.StoreSourceData) + { + doc.Add(new StoredField(IndexingConstants.SourceKey + entry.Name, stringValue)); } } else { - if (entry.Options.HasFlag(DocumentIndexOptions.Analyze)) + if (entry.Options.HasFlag(DocumentIndexOptions.Keyword)) { - doc.Add(new TextField(entry.Name, "NULL", store)); + doc.Add(new StringField(entry.Name, "NULL", store)); } else { - doc.Add(new StringField(entry.Name, "NULL", store)); + doc.Add(new TextField(entry.Name, "NULL", store)); } } break; case DocumentIndex.Types.GeoPoint: var strategy = new RecursivePrefixTreeStrategy(_grid, entry.Name); + if (entry.Value != null && entry.Value is DocumentIndex.GeoPoint point) { var geoPoint = _ctx.MakePoint((double)point.Longitude, (double)point.Latitude); @@ -288,14 +341,16 @@ private Document CreateLuceneDocument(DocumentIndex documentIndex) doc.Add(field); } - if (entry.Options.HasFlag(DocumentIndexOptions.Store)) + doc.Add(new StoredField(strategy.FieldName, $"{point.Latitude},{point.Longitude}")); + + if (indexSettings.StoreSourceData) { - doc.Add(new StoredField(strategy.FieldName, $"{point.Latitude},{point.Longitude}")); + doc.Add(new StoredField(IndexingConstants.SourceKey + strategy.FieldName, $"{point.Latitude},{point.Longitude}")); } } else { - doc.Add(new StringField(strategy.FieldName, "NULL", store)); + doc.Add(new StoredField(strategy.FieldName, "NULL")); } break; } diff --git a/src/OrchardCore.Modules/OrchardCore.Lucene/Services/LuceneIndexSettingsService.cs b/src/OrchardCore.Modules/OrchardCore.Search.Lucene/Services/LuceneIndexSettingsService.cs similarity index 97% rename from src/OrchardCore.Modules/OrchardCore.Lucene/Services/LuceneIndexSettingsService.cs rename to src/OrchardCore.Modules/OrchardCore.Search.Lucene/Services/LuceneIndexSettingsService.cs index de7e94b4096..b4a9cd4b1a0 100644 --- a/src/OrchardCore.Modules/OrchardCore.Lucene/Services/LuceneIndexSettingsService.cs +++ b/src/OrchardCore.Modules/OrchardCore.Search.Lucene/Services/LuceneIndexSettingsService.cs @@ -1,9 +1,9 @@ using System.Collections.Generic; using System.Threading.Tasks; using OrchardCore.Documents; -using OrchardCore.Lucene.Model; +using OrchardCore.Search.Lucene.Model; -namespace OrchardCore.Lucene +namespace OrchardCore.Search.Lucene { public class LuceneIndexSettingsService { diff --git a/src/OrchardCore.Modules/OrchardCore.Lucene/Services/LuceneIndexingService.cs b/src/OrchardCore.Modules/OrchardCore.Search.Lucene/Services/LuceneIndexingService.cs similarity index 89% rename from src/OrchardCore.Modules/OrchardCore.Lucene/Services/LuceneIndexingService.cs rename to src/OrchardCore.Modules/OrchardCore.Search.Lucene/Services/LuceneIndexingService.cs index 3c94e29b0af..75e353ff60f 100644 --- a/src/OrchardCore.Modules/OrchardCore.Lucene/Services/LuceneIndexingService.cs +++ b/src/OrchardCore.Modules/OrchardCore.Search.Lucene/Services/LuceneIndexingService.cs @@ -9,11 +9,11 @@ using OrchardCore.Entities; using OrchardCore.Environment.Shell; using OrchardCore.Indexing; -using OrchardCore.Lucene.Model; +using OrchardCore.Search.Lucene.Model; using OrchardCore.Modules; using OrchardCore.Settings; -namespace OrchardCore.Lucene +namespace OrchardCore.Search.Lucene { /// /// This class provides services to update all the Lucene indices. It is non-rentrant so that calls @@ -123,8 +123,13 @@ await shellScope.UsingAsync(async scope => .Select(x => x.ContentItemId) .ToArray(); - var allPublished = await contentManager.GetAsync(updatedContentItemIds); - var allLatest = await contentManager.GetAsync(updatedContentItemIds, latest: true); + var allPublished = new Dictionary(); + var allLatest = new Dictionary(); + + var allPublishedContentItems = await contentManager.GetAsync(updatedContentItemIds); + allPublished = allPublishedContentItems.DistinctBy(x => x.ContentItemId).ToDictionary(k => k.ContentItemId, v => v); + var allLatestContentItems = await contentManager.GetAsync(updatedContentItemIds, latest: true); + allLatest = allLatestContentItems.DistinctBy(x => x.ContentItemId).ToDictionary(k => k.ContentItemVersionId, v => v); // Group all DocumentIndex by index to batch update them var updatedDocumentsByIndex = new Dictionary>(); @@ -152,20 +157,22 @@ await shellScope.UsingAsync(async scope => if (needPublished) { - var contentItem = await contentManager.GetAsync(task.ContentItemId); + allPublished.TryGetValue(task.ContentItemId, out var contentItem); + if (contentItem != null) { - publishedIndexContext = new BuildIndexContext(new DocumentIndex(task.ContentItemId), contentItem, new string[] { contentItem.ContentType }); + publishedIndexContext = new BuildIndexContext(new DocumentIndex(task.ContentItemId, contentItem.ContentItemVersionId), contentItem, new string[] { contentItem.ContentType }, new LuceneContentIndexSettings()); await indexHandlers.InvokeAsync(x => x.BuildIndexAsync(publishedIndexContext), _logger); } } if (needLatest) { - var contentItem = await contentManager.GetAsync(task.ContentItemId, VersionOptions.Latest); + allLatest.TryGetValue(task.ContentItemId, out var contentItem); + if (contentItem != null) { - latestIndexContext = new BuildIndexContext(new DocumentIndex(task.ContentItemId), contentItem, new string[] { contentItem.ContentType }); + latestIndexContext = new BuildIndexContext(new DocumentIndex(task.ContentItemId, contentItem.ContentItemVersionId), contentItem, new string[] { contentItem.ContentType }, new LuceneContentIndexSettings()); await indexHandlers.InvokeAsync(x => x.BuildIndexAsync(latestIndexContext), _logger); } } @@ -269,7 +276,7 @@ public Task DeleteIndexAsync(string indexName) /// Restarts the indexing process from the beginning in order to update /// current content items. It doesn't delete existing entries from the index. /// - public void ResetIndex(string indexName) + public void ResetIndexAsync(string indexName) { _indexingState.SetLastTaskId(indexName, 0); _indexingState.Update(); @@ -287,7 +294,7 @@ public async Task RebuildIndexAsync(string indexName) await _indexManager.CreateIndexAsync(indexName); - ResetIndex(indexName); + ResetIndexAsync(indexName); } public async Task GetLuceneSettingsAsync() diff --git a/src/OrchardCore.Modules/OrchardCore.Lucene/Services/LuceneIndexingState.cs b/src/OrchardCore.Modules/OrchardCore.Search.Lucene/Services/LuceneIndexingState.cs similarity index 98% rename from src/OrchardCore.Modules/OrchardCore.Lucene/Services/LuceneIndexingState.cs rename to src/OrchardCore.Modules/OrchardCore.Search.Lucene/Services/LuceneIndexingState.cs index 83e02a34bbe..5fef37770eb 100644 --- a/src/OrchardCore.Modules/OrchardCore.Lucene/Services/LuceneIndexingState.cs +++ b/src/OrchardCore.Modules/OrchardCore.Search.Lucene/Services/LuceneIndexingState.cs @@ -3,7 +3,7 @@ using Newtonsoft.Json.Linq; using OrchardCore.Environment.Shell; -namespace OrchardCore.Lucene +namespace OrchardCore.Search.Lucene { /// /// This class persists the indexing state, a cursor, on the filesystem alongside the index itself. diff --git a/src/OrchardCore.Modules/OrchardCore.Lucene/Services/LuceneQuery.cs b/src/OrchardCore.Modules/OrchardCore.Search.Lucene/Services/LuceneQuery.cs similarity index 92% rename from src/OrchardCore.Modules/OrchardCore.Lucene/Services/LuceneQuery.cs rename to src/OrchardCore.Modules/OrchardCore.Search.Lucene/Services/LuceneQuery.cs index 670bcc0a6f6..7813444c46a 100644 --- a/src/OrchardCore.Modules/OrchardCore.Lucene/Services/LuceneQuery.cs +++ b/src/OrchardCore.Modules/OrchardCore.Search.Lucene/Services/LuceneQuery.cs @@ -1,7 +1,7 @@ using OrchardCore.ContentManagement; using OrchardCore.Queries; -namespace OrchardCore.Lucene +namespace OrchardCore.Search.Lucene { public class LuceneQuery : Query { diff --git a/src/OrchardCore.Modules/OrchardCore.Lucene/Services/LuceneQuerySource.cs b/src/OrchardCore.Modules/OrchardCore.Search.Lucene/Services/LuceneQuerySource.cs similarity index 90% rename from src/OrchardCore.Modules/OrchardCore.Lucene/Services/LuceneQuerySource.cs rename to src/OrchardCore.Modules/OrchardCore.Search.Lucene/Services/LuceneQuerySource.cs index a0be2e058ca..787d99380ae 100644 --- a/src/OrchardCore.Modules/OrchardCore.Lucene/Services/LuceneQuerySource.cs +++ b/src/OrchardCore.Modules/OrchardCore.Search.Lucene/Services/LuceneQuerySource.cs @@ -9,17 +9,17 @@ using OrchardCore.ContentManagement; using OrchardCore.ContentManagement.Records; using OrchardCore.Liquid; -using OrchardCore.Lucene.Model; -using OrchardCore.Lucene.Services; using OrchardCore.Queries; +using OrchardCore.Search.Lucene.Model; +using OrchardCore.Search.Lucene.Services; using YesSql; using YesSql.Services; -namespace OrchardCore.Lucene +namespace OrchardCore.Search.Lucene { public class LuceneQuerySource : IQuerySource { - private readonly LuceneIndexManager _luceneIndexProvider; + private readonly LuceneIndexManager _luceneIndexManager; private readonly LuceneIndexSettingsService _luceneIndexSettingsService; private readonly LuceneAnalyzerManager _luceneAnalyzerManager; private readonly ILuceneQueryService _queryService; @@ -29,7 +29,7 @@ public class LuceneQuerySource : IQuerySource private readonly TemplateOptions _templateOptions; public LuceneQuerySource( - LuceneIndexManager luceneIndexProvider, + LuceneIndexManager luceneIndexManager, LuceneIndexSettingsService luceneIndexSettingsService, LuceneAnalyzerManager luceneAnalyzerManager, ILuceneQueryService queryService, @@ -38,7 +38,7 @@ public LuceneQuerySource( JavaScriptEncoder javaScriptEncoder, IOptions templateOptions) { - _luceneIndexProvider = luceneIndexProvider; + _luceneIndexManager = luceneIndexManager; _luceneIndexSettingsService = luceneIndexSettingsService; _luceneAnalyzerManager = luceneAnalyzerManager; _queryService = queryService; @@ -60,7 +60,7 @@ public async Task ExecuteQueryAsync(Query query, IDictionary + await _luceneIndexManager.SearchAsync(luceneQuery.Index, async searcher => { var tokenizedContent = await _liquidTemplateManager.RenderStringAsync(luceneQuery.Template, _javaScriptEncoder, parameters.Select(x => new KeyValuePair(x.Key, FluidValue.Create(x.Value, _templateOptions)))); @@ -77,7 +77,7 @@ await _luceneIndexProvider.SearchAsync(luceneQuery.Index, async searcher => luceneQueryResults.Items = new List(); // Load corresponding content item versions - var indexedContentItemVersionIds = docs.TopDocs.ScoreDocs.Select(x => searcher.Doc(x.Doc).Get("Content.ContentItem.ContentItemVersionId")).ToArray(); + var indexedContentItemVersionIds = docs.TopDocs.ScoreDocs.Select(x => searcher.Doc(x.Doc).Get("ContentItemVersionId")).ToArray(); var dbContentItems = await _session.Query(x => x.ContentItemVersionId.IsIn(indexedContentItemVersionIds)).ListAsync(); // Reorder the result to preserve the one from the lucene query diff --git a/src/OrchardCore.Modules/OrchardCore.Search.Lucene/Services/LuceneSearchProvider.cs b/src/OrchardCore.Modules/OrchardCore.Search.Lucene/Services/LuceneSearchProvider.cs new file mode 100644 index 00000000000..cad8e091704 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Search.Lucene/Services/LuceneSearchProvider.cs @@ -0,0 +1,15 @@ +using OrchardCore.Search.Abstractions; + +namespace OrchardCore.Search.Lucene.Services +{ + ///
@entry.AnalyzerName
@T["Found {0} result(s) in {1} ms", Model.Count.ToString(), Model.Elapsed.TotalMilliseconds.ToString()]
@T["The results displayed in this table are coming from the _source data."]
@T["The results displayed in this table are coming from the field(s) data."]
Content.ContentItem.FullText
@T["Exports all or specified search indexes."]
@T["Rebuild all or specified Elasticsearch indices."]
@T["Reset all or specified Elasticsearch indices."]
@T["Exports Elasticsearch settings."]
@T["Queries an Elasticsearch index."]