From 55819e669ce74274c4a92a17c65d16b0cb749434 Mon Sep 17 00:00:00 2001 From: Jasmin Savard Date: Mon, 18 Oct 2021 16:05:13 -0400 Subject: [PATCH] ElasticSearch first commit --- OrchardCore.sln | 21 + src/OrchardCore.Build/Dependencies.props | 2 +- .../OrchardCore.Cms.Web.csproj | 2 +- src/OrchardCore.Cms.Web/appsettings.json | 7 +- .../Migrations/dashboard-widgets.recipe.json | 2 +- .../Indexing/ContentItemIndexCoordinator.cs | 10 +- .../Migrations/Widgets/migration.recipe.json | 2 +- .../Indexing/BagPartIndexHandler.cs | 2 +- .../OrchardCore.Lucene/AdminMenu.cs | 2 +- .../Drivers/LuceneSettingsDisplayDriver.cs | 2 +- .../Handler/LuceneIndexingContentHandler.cs | 3 +- .../Model/LuceneContentIndexSettings.cs} | 8 +- .../Services/LuceneIndexingService.cs | 4 +- .../Settings/ContentIndexSettingsViewModel.cs | 9 - ...tentPartFieldIndexSettingsDisplayDriver.cs | 11 +- ...ntentTypePartIndexSettingsDisplayDriver.cs | 11 +- .../LuceneContentIndexSettingsViewModel.cs | 9 + .../Views/Admin/Index.cshtml | 2 +- .../Views/ContentIndexSettings.Edit.cshtml | 24 - .../LuceneContentIndexSettings.Edit.cshtml | 25 + .../content-menu-updatefrom1.recipe.json | 2 +- .../Migrations/menu.recipe.json | 2 +- .../OrchardCore.Search.Elastic/AdminMenu.cs | 48 ++ .../ElasticConnectionOptions.cs | 21 + .../Controllers/AdminController.cs | 502 ++++++++++++++++++ .../Controllers/ApiController.cs | 101 ++++ .../Controllers/SearchController.cs | 220 ++++++++ .../ElasticIndexDeploymentSource.cs | 50 ++ .../Deployment/ElasticIndexDeploymentStep.cs | 19 + .../ElasticIndexDeploymentStepDriver.cs | 58 ++ .../ElasticSettingsDeploymentSource.cs | 34 ++ .../ElasticSettingsDeploymentStep.cs | 15 + .../ElasticSettingsDeploymentStepDriver.cs | 23 + .../Drivers/ElasticQueryDisplayDriver.cs | 74 +++ .../Drivers/ElasticSettingsDisplayDriver.cs | 74 +++ .../GraphQL/ElasticQueryFieldTypeProvider.cs | 198 +++++++ .../GraphQL/Startup.cs | 19 + .../Handler/ElasticIndexingContentHandler.cs | 121 +++++ .../OrchardCore.Search.Elastic/Manifest.cs | 28 + .../Model/ElasticContentIndexSettings.cs | 14 + .../Model/ElasticIndexSettings.cs | 25 + .../Model/ElasticQueryModel.cs | 9 + .../Model/ElasticSettings.cs | 17 + .../OrchardCore.Search.Elastic.csproj | 46 ++ .../OrchardCore.Search.Elastic/Permissions.cs | 52 ++ .../Properties/AssemblyInfo.cs | 18 + .../Recipes/ElasticIndexStep.cs | 57 ++ .../Services/ElasticAnalyzer.cs | 29 + .../Services/ElasticAnalyzerManager.cs | 41 ++ .../ElasticContentPickerResultProvider.cs | 74 +++ .../Services/ElasticIndexManager.cs | 250 +++++++++ .../Services/ElasticIndexSettingsService.cs | 94 ++++ .../Services/ElasticIndexingBackgroundTask.cs | 25 + .../Services/ElasticIndexingService.cs | 305 +++++++++++ .../Services/ElasticIndexingState.cs | 73 +++ .../Services/ElasticQuery.cs | 15 + .../Services/ElasticQuerySource.cs | 99 ++++ .../Services/ISearchQueryService.cs | 11 + .../Services/SearchQueryService.cs | 27 + ...tentPartFieldIndexSettingsDisplayDriver.cs | 53 ++ ...ContentPickerFieldElasticEditorSettings.cs | 12 + ...tPickerFieldElasticEditorSettingsDriver.cs | 47 ++ ...ntentTypePartIndexSettingsDisplayDriver.cs | 53 ++ .../ElasticContentIndexSettingsViewModel.cs | 9 + .../Settings/TypeIndexSettings.cs | 24 + .../ElasticContentPickerShapeProvider.cs | 31 ++ .../Shapes/SearchShapes.cs | 53 ++ .../OrchardCore.Search.Elastic/Startup.cs | 194 +++++++ .../ViewModels/AdminIndexViewModel.cs | 38 ++ .../ViewModels/AdminQueryViewModel.cs | 26 + .../ElasticIndexDeploymentStepViewModel.cs | 9 + .../ElasticIndexSettingsViewModel.cs | 31 ++ .../ViewModels/ElasticQueryViewModel.cs | 10 + .../ViewModels/ElasticSettingsViewModel.cs | 13 + .../ViewModels/IndexViewModel.cs | 11 + .../ViewModels/QueryIndexViewModel.cs | 19 + .../Views/Admin/Edit.cshtml | 69 +++ .../Views/Admin/Index.cshtml | 149 ++++++ .../Views/Admin/Query.cshtml | 95 ++++ ...ckerFieldElasticEditorSettings.Edit.cshtml | 16 + .../ElasticContentIndexSettings.Edit.cshtml | 12 + .../ElasticQuery.Buttons.SummaryAdmin.cshtml | 12 + .../Views/ElasticQuery.Edit.cshtml | 36 ++ .../Views/ElasticQuery.SummaryAdmin.cshtml | 1 + .../Views/ElasticSettings.Edit.cshtml | 35 ++ ...sticIndexDeploymentStep.Fields.Edit.cshtml | 62 +++ ...cIndexDeploymentStep.Fields.Summary.cshtml | 27 + ...ndexDeploymentStep.Fields.Thumbnail.cshtml | 4 + ...cSettingsDeploymentStep.Fields.Edit.cshtml | 3 + ...ttingsDeploymentStep.Fields.Summary.cshtml | 5 + ...ingsDeploymentStep.Fields.Thumbnail.cshtml | 4 + ...NavigationItemText-elasticsearch.Id.cshtml | 1 + .../Views/Query-ElasticSearch.Link.cshtml | 16 + .../Views/Search-Form.cshtml | 10 + .../Views/Search-Results.cshtml | 18 + .../Views/Search/Search.cshtml | 9 + .../Views/_ViewImports.cshtml | 14 + .../Migrations/socialmetasettings.recipe.json | 14 +- .../TheBlogTheme/Recipes/blog.recipe.json | 12 +- ...rdCore.Application.Cms.Core.Targets.csproj | 2 +- .../BuildFieldIndexContext.cs | 2 +- .../BuildIndexContext.cs | 5 +- .../BuildPartIndexContext.cs | 6 +- .../ContentFieldIndexHandler.cs | 2 +- .../ContentPartIndexHandler.cs | 2 +- .../IContentFieldIndexHandler.cs | 2 +- .../IContentIndexSettings.cs | 12 + .../IContentPartIndexHandler.cs | 2 +- .../OrchardCore.Lucene.Abstractions.csproj | 1 - .../ElasticOptions.cs | 9 + .../ElasticQueryContext.cs | 27 + .../ElasticQueryResults.cs | 11 + .../ElasticTopDocs.cs | 10 + .../IElasticAnalyzer.cs | 13 + .../IElasticQueryProvider.cs | 10 + .../IElasticQueryService.cs | 12 + .../Indexing/Index.cs | 30 ++ .../Indexing/Mapping.cs | 15 + .../Indexing/Property.cs | 57 ++ .../Indexing/Types.cs | 21 + ...ardCore.Search.Elastic.Abstractions.csproj | 25 + .../ElasticQueryService.cs | 89 ++++ .../OrchardCore.Search.Elastic.Core.csproj | 26 + .../ServiceCollectionExtensions.cs | 17 + .../Apis/Lucene/Recipes/luceneQueryTest.json | 2 +- 125 files changed, 4626 insertions(+), 89 deletions(-) rename src/{OrchardCore/OrchardCore.Indexing.Abstractions/ContentIndexSettings.cs => OrchardCore.Modules/OrchardCore.Lucene/Model/LuceneContentIndexSettings.cs} (82%) delete mode 100644 src/OrchardCore.Modules/OrchardCore.Lucene/Settings/ContentIndexSettingsViewModel.cs create mode 100644 src/OrchardCore.Modules/OrchardCore.Lucene/Settings/LuceneContentIndexSettingsViewModel.cs delete mode 100644 src/OrchardCore.Modules/OrchardCore.Lucene/Views/ContentIndexSettings.Edit.cshtml create mode 100644 src/OrchardCore.Modules/OrchardCore.Lucene/Views/LuceneContentIndexSettings.Edit.cshtml create mode 100644 src/OrchardCore.Modules/OrchardCore.Search.Elastic/AdminMenu.cs create mode 100644 src/OrchardCore.Modules/OrchardCore.Search.Elastic/Configurations/ElasticConnectionOptions.cs create mode 100644 src/OrchardCore.Modules/OrchardCore.Search.Elastic/Controllers/AdminController.cs create mode 100644 src/OrchardCore.Modules/OrchardCore.Search.Elastic/Controllers/ApiController.cs create mode 100644 src/OrchardCore.Modules/OrchardCore.Search.Elastic/Controllers/SearchController.cs create mode 100644 src/OrchardCore.Modules/OrchardCore.Search.Elastic/Deployment/ElasticIndexDeploymentSource.cs create mode 100644 src/OrchardCore.Modules/OrchardCore.Search.Elastic/Deployment/ElasticIndexDeploymentStep.cs create mode 100644 src/OrchardCore.Modules/OrchardCore.Search.Elastic/Deployment/ElasticIndexDeploymentStepDriver.cs create mode 100644 src/OrchardCore.Modules/OrchardCore.Search.Elastic/Deployment/ElasticSettingsDeploymentSource.cs create mode 100644 src/OrchardCore.Modules/OrchardCore.Search.Elastic/Deployment/ElasticSettingsDeploymentStep.cs create mode 100644 src/OrchardCore.Modules/OrchardCore.Search.Elastic/Deployment/ElasticSettingsDeploymentStepDriver.cs create mode 100644 src/OrchardCore.Modules/OrchardCore.Search.Elastic/Drivers/ElasticQueryDisplayDriver.cs create mode 100644 src/OrchardCore.Modules/OrchardCore.Search.Elastic/Drivers/ElasticSettingsDisplayDriver.cs create mode 100644 src/OrchardCore.Modules/OrchardCore.Search.Elastic/GraphQL/ElasticQueryFieldTypeProvider.cs create mode 100644 src/OrchardCore.Modules/OrchardCore.Search.Elastic/GraphQL/Startup.cs create mode 100644 src/OrchardCore.Modules/OrchardCore.Search.Elastic/Handler/ElasticIndexingContentHandler.cs create mode 100644 src/OrchardCore.Modules/OrchardCore.Search.Elastic/Manifest.cs create mode 100644 src/OrchardCore.Modules/OrchardCore.Search.Elastic/Model/ElasticContentIndexSettings.cs create mode 100644 src/OrchardCore.Modules/OrchardCore.Search.Elastic/Model/ElasticIndexSettings.cs create mode 100644 src/OrchardCore.Modules/OrchardCore.Search.Elastic/Model/ElasticQueryModel.cs create mode 100644 src/OrchardCore.Modules/OrchardCore.Search.Elastic/Model/ElasticSettings.cs create mode 100644 src/OrchardCore.Modules/OrchardCore.Search.Elastic/OrchardCore.Search.Elastic.csproj create mode 100644 src/OrchardCore.Modules/OrchardCore.Search.Elastic/Permissions.cs create mode 100644 src/OrchardCore.Modules/OrchardCore.Search.Elastic/Properties/AssemblyInfo.cs create mode 100644 src/OrchardCore.Modules/OrchardCore.Search.Elastic/Recipes/ElasticIndexStep.cs create mode 100644 src/OrchardCore.Modules/OrchardCore.Search.Elastic/Services/ElasticAnalyzer.cs create mode 100644 src/OrchardCore.Modules/OrchardCore.Search.Elastic/Services/ElasticAnalyzerManager.cs create mode 100644 src/OrchardCore.Modules/OrchardCore.Search.Elastic/Services/ElasticContentPickerResultProvider.cs create mode 100644 src/OrchardCore.Modules/OrchardCore.Search.Elastic/Services/ElasticIndexManager.cs create mode 100644 src/OrchardCore.Modules/OrchardCore.Search.Elastic/Services/ElasticIndexSettingsService.cs create mode 100644 src/OrchardCore.Modules/OrchardCore.Search.Elastic/Services/ElasticIndexingBackgroundTask.cs create mode 100644 src/OrchardCore.Modules/OrchardCore.Search.Elastic/Services/ElasticIndexingService.cs create mode 100644 src/OrchardCore.Modules/OrchardCore.Search.Elastic/Services/ElasticIndexingState.cs create mode 100644 src/OrchardCore.Modules/OrchardCore.Search.Elastic/Services/ElasticQuery.cs create mode 100644 src/OrchardCore.Modules/OrchardCore.Search.Elastic/Services/ElasticQuerySource.cs create mode 100644 src/OrchardCore.Modules/OrchardCore.Search.Elastic/Services/ISearchQueryService.cs create mode 100644 src/OrchardCore.Modules/OrchardCore.Search.Elastic/Services/SearchQueryService.cs create mode 100644 src/OrchardCore.Modules/OrchardCore.Search.Elastic/Settings/ContentPartFieldIndexSettingsDisplayDriver.cs create mode 100644 src/OrchardCore.Modules/OrchardCore.Search.Elastic/Settings/ContentPickerFieldElasticEditorSettings.cs create mode 100644 src/OrchardCore.Modules/OrchardCore.Search.Elastic/Settings/ContentPickerFieldElasticEditorSettingsDriver.cs create mode 100644 src/OrchardCore.Modules/OrchardCore.Search.Elastic/Settings/ContentTypePartIndexSettingsDisplayDriver.cs create mode 100644 src/OrchardCore.Modules/OrchardCore.Search.Elastic/Settings/ElasticContentIndexSettingsViewModel.cs create mode 100644 src/OrchardCore.Modules/OrchardCore.Search.Elastic/Settings/TypeIndexSettings.cs create mode 100644 src/OrchardCore.Modules/OrchardCore.Search.Elastic/Shapes/ElasticContentPickerShapeProvider.cs create mode 100644 src/OrchardCore.Modules/OrchardCore.Search.Elastic/Shapes/SearchShapes.cs create mode 100644 src/OrchardCore.Modules/OrchardCore.Search.Elastic/Startup.cs create mode 100644 src/OrchardCore.Modules/OrchardCore.Search.Elastic/ViewModels/AdminIndexViewModel.cs create mode 100644 src/OrchardCore.Modules/OrchardCore.Search.Elastic/ViewModels/AdminQueryViewModel.cs create mode 100644 src/OrchardCore.Modules/OrchardCore.Search.Elastic/ViewModels/ElasticIndexDeploymentStepViewModel.cs create mode 100644 src/OrchardCore.Modules/OrchardCore.Search.Elastic/ViewModels/ElasticIndexSettingsViewModel.cs create mode 100644 src/OrchardCore.Modules/OrchardCore.Search.Elastic/ViewModels/ElasticQueryViewModel.cs create mode 100644 src/OrchardCore.Modules/OrchardCore.Search.Elastic/ViewModels/ElasticSettingsViewModel.cs create mode 100644 src/OrchardCore.Modules/OrchardCore.Search.Elastic/ViewModels/IndexViewModel.cs create mode 100644 src/OrchardCore.Modules/OrchardCore.Search.Elastic/ViewModels/QueryIndexViewModel.cs create mode 100644 src/OrchardCore.Modules/OrchardCore.Search.Elastic/Views/Admin/Edit.cshtml create mode 100644 src/OrchardCore.Modules/OrchardCore.Search.Elastic/Views/Admin/Index.cshtml create mode 100644 src/OrchardCore.Modules/OrchardCore.Search.Elastic/Views/Admin/Query.cshtml create mode 100644 src/OrchardCore.Modules/OrchardCore.Search.Elastic/Views/ContentPickerFieldElasticEditorSettings.Edit.cshtml create mode 100644 src/OrchardCore.Modules/OrchardCore.Search.Elastic/Views/ElasticContentIndexSettings.Edit.cshtml create mode 100644 src/OrchardCore.Modules/OrchardCore.Search.Elastic/Views/ElasticQuery.Buttons.SummaryAdmin.cshtml create mode 100644 src/OrchardCore.Modules/OrchardCore.Search.Elastic/Views/ElasticQuery.Edit.cshtml create mode 100644 src/OrchardCore.Modules/OrchardCore.Search.Elastic/Views/ElasticQuery.SummaryAdmin.cshtml create mode 100644 src/OrchardCore.Modules/OrchardCore.Search.Elastic/Views/ElasticSettings.Edit.cshtml create mode 100644 src/OrchardCore.Modules/OrchardCore.Search.Elastic/Views/Items/ElasticIndexDeploymentStep.Fields.Edit.cshtml create mode 100644 src/OrchardCore.Modules/OrchardCore.Search.Elastic/Views/Items/ElasticIndexDeploymentStep.Fields.Summary.cshtml create mode 100644 src/OrchardCore.Modules/OrchardCore.Search.Elastic/Views/Items/ElasticIndexDeploymentStep.Fields.Thumbnail.cshtml create mode 100644 src/OrchardCore.Modules/OrchardCore.Search.Elastic/Views/Items/ElasticSettingsDeploymentStep.Fields.Edit.cshtml create mode 100644 src/OrchardCore.Modules/OrchardCore.Search.Elastic/Views/Items/ElasticSettingsDeploymentStep.Fields.Summary.cshtml create mode 100644 src/OrchardCore.Modules/OrchardCore.Search.Elastic/Views/Items/ElasticSettingsDeploymentStep.Fields.Thumbnail.cshtml create mode 100644 src/OrchardCore.Modules/OrchardCore.Search.Elastic/Views/NavigationItemText-elasticsearch.Id.cshtml create mode 100644 src/OrchardCore.Modules/OrchardCore.Search.Elastic/Views/Query-ElasticSearch.Link.cshtml create mode 100644 src/OrchardCore.Modules/OrchardCore.Search.Elastic/Views/Search-Form.cshtml create mode 100644 src/OrchardCore.Modules/OrchardCore.Search.Elastic/Views/Search-Results.cshtml create mode 100644 src/OrchardCore.Modules/OrchardCore.Search.Elastic/Views/Search/Search.cshtml create mode 100644 src/OrchardCore.Modules/OrchardCore.Search.Elastic/Views/_ViewImports.cshtml create mode 100644 src/OrchardCore/OrchardCore.Indexing.Abstractions/IContentIndexSettings.cs create mode 100644 src/OrchardCore/OrchardCore.Search.Elastic.Abstractions/ElasticOptions.cs create mode 100644 src/OrchardCore/OrchardCore.Search.Elastic.Abstractions/ElasticQueryContext.cs create mode 100644 src/OrchardCore/OrchardCore.Search.Elastic.Abstractions/ElasticQueryResults.cs create mode 100644 src/OrchardCore/OrchardCore.Search.Elastic.Abstractions/ElasticTopDocs.cs create mode 100644 src/OrchardCore/OrchardCore.Search.Elastic.Abstractions/IElasticAnalyzer.cs create mode 100644 src/OrchardCore/OrchardCore.Search.Elastic.Abstractions/IElasticQueryProvider.cs create mode 100644 src/OrchardCore/OrchardCore.Search.Elastic.Abstractions/IElasticQueryService.cs create mode 100644 src/OrchardCore/OrchardCore.Search.Elastic.Abstractions/Indexing/Index.cs create mode 100644 src/OrchardCore/OrchardCore.Search.Elastic.Abstractions/Indexing/Mapping.cs create mode 100644 src/OrchardCore/OrchardCore.Search.Elastic.Abstractions/Indexing/Property.cs create mode 100644 src/OrchardCore/OrchardCore.Search.Elastic.Abstractions/Indexing/Types.cs create mode 100644 src/OrchardCore/OrchardCore.Search.Elastic.Abstractions/OrchardCore.Search.Elastic.Abstractions.csproj create mode 100644 src/OrchardCore/OrchardCore.Search.Elastic.Core/ElasticQueryService.cs create mode 100644 src/OrchardCore/OrchardCore.Search.Elastic.Core/OrchardCore.Search.Elastic.Core.csproj create mode 100644 src/OrchardCore/OrchardCore.Search.Elastic.Core/ServiceCollectionExtensions.cs diff --git a/OrchardCore.sln b/OrchardCore.sln index a6dd92af158..d45777a8435 100644 --- a/OrchardCore.sln +++ b/OrchardCore.sln @@ -400,6 +400,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.Elastic.Abstractions", "src\OrchardCore\OrchardCore.Search.Elastic.Abstractions\OrchardCore.Search.Elastic.Abstractions.csproj", "{E6A90BFD-AB5C-4AED-A5AB-799136AB1521}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OrchardCore.Search.Elastic.Core", "src\OrchardCore\OrchardCore.Search.Elastic.Core\OrchardCore.Search.Elastic.Core.csproj", "{848C919B-6829-4758-9E59-51F13D25ABBC}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OrchardCore.Search.Elastic", "src\OrchardCore.Modules\OrchardCore.Search.Elastic\OrchardCore.Search.Elastic.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}" @@ -1100,6 +1106,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 @@ -1310,6 +1328,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} diff --git a/src/OrchardCore.Build/Dependencies.props b/src/OrchardCore.Build/Dependencies.props index a76029d57c3..08f391a41c8 100644 --- a/src/OrchardCore.Build/Dependencies.props +++ b/src/OrchardCore.Build/Dependencies.props @@ -33,6 +33,7 @@ + @@ -54,5 +55,4 @@ - diff --git a/src/OrchardCore.Cms.Web/OrchardCore.Cms.Web.csproj b/src/OrchardCore.Cms.Web/OrchardCore.Cms.Web.csproj index 2f0ffb5621d..b9c8a31f57c 100644 --- a/src/OrchardCore.Cms.Web/OrchardCore.Cms.Web.csproj +++ b/src/OrchardCore.Cms.Web/OrchardCore.Cms.Web.csproj @@ -1,4 +1,4 @@ - + diff --git a/src/OrchardCore.Cms.Web/appsettings.json b/src/OrchardCore.Cms.Web/appsettings.json index f8f037f1340..56c4c7ac64c 100644 --- a/src/OrchardCore.Cms.Web/appsettings.json +++ b/src/OrchardCore.Cms.Web/appsettings.json @@ -81,7 +81,10 @@ // "ChangePasswordUrl": "ChangePassword", // "ExternalLoginsUrl": "ExternalLogins" //}, - + // Provides Elastic Connection + "OrchardCore_Elastic": { + "Url": "http://localhost:9200" + } // WARNING: AutoSetup section given as an example for Development only, for Production use "Environment Variables" instead //"OrchardCore_AutoSetup": { // "AutoSetupPath": "", @@ -118,4 +121,4 @@ // "Url": "/health/live" //} } -} +} \ No newline at end of file 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..5b3170de0bc 100644 --- a/src/OrchardCore.Modules/OrchardCore.AdminDashboard/Migrations/dashboard-widgets.recipe.json +++ b/src/OrchardCore.Modules/OrchardCore.AdminDashboard/Migrations/dashboard-widgets.recipe.json @@ -51,7 +51,7 @@ }, "GraphQLContentTypePartSettings": {}, "HtmlBodyPartSettings": {}, - "ContentIndexSettings": {} + "LuceneContentIndexSettings": {} } }, { diff --git a/src/OrchardCore.Modules/OrchardCore.Contents/Indexing/ContentItemIndexCoordinator.cs b/src/OrchardCore.Modules/OrchardCore.Contents/Indexing/ContentItemIndexCoordinator.cs index cef19857349..4ae33e2e3f5 100644 --- a/src/OrchardCore.Modules/OrchardCore.Contents/Indexing/ContentItemIndexCoordinator.cs +++ b/src/OrchardCore.Modules/OrchardCore.Contents/Indexing/ContentItemIndexCoordinator.cs @@ -1,8 +1,10 @@ using System.Collections.Generic; +using System.Reflection; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using OrchardCore.ContentManagement; using OrchardCore.ContentManagement.Metadata; +using OrchardCore.ContentManagement.Metadata.Models; using OrchardCore.Indexing; using OrchardCore.Modules; @@ -49,7 +51,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(); + MethodInfo ContentTypePartDefinitionMethod = typeof(ContentTypePartDefinition).GetMethod("GetSettings"); + MethodInfo 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 +67,9 @@ await _partIndexHandlers.InvokeAsync((handler, part, contentTypePartDefinition, foreach (var contentPartFieldDefinition in contentTypePartDefinition.PartDefinition.Fields) { - var partFieldIndexSettings = contentPartFieldDefinition.GetSettings(); + MethodInfo ContentPartFieldDefinitionMethod = typeof(ContentPartFieldDefinition).GetMethod("GetSettings"); + MethodInfo ContentPartFieldDefinitionGeneric = ContentPartFieldDefinitionMethod.MakeGenericMethod(context.Settings.GetType()); + var partFieldIndexSettings = (IContentIndexSettings)ContentPartFieldDefinitionGeneric.Invoke(contentPartFieldDefinition, null); if (!partFieldIndexSettings.Included) { 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..4009f9049c9 100644 --- a/src/OrchardCore.Modules/OrchardCore.Facebook/Migrations/Widgets/migration.recipe.json +++ b/src/OrchardCore.Modules/OrchardCore.Facebook/Migrations/Widgets/migration.recipe.json @@ -246,7 +246,7 @@ "FacebookPluginPartSettings": { "Liquid": "
\">\r\n
" }, - "ContentIndexSettings": {} + "LuceneContentIndexSettings": {} } } ] 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.Lucene/AdminMenu.cs b/src/OrchardCore.Modules/OrchardCore.Lucene/AdminMenu.cs index d9ad832171d..59b9ce2f4b1 100644 --- a/src/OrchardCore.Modules/OrchardCore.Lucene/AdminMenu.cs +++ b/src/OrchardCore.Modules/OrchardCore.Lucene/AdminMenu.cs @@ -35,7 +35,7 @@ public Task BuildNavigationAsync(string name, NavigationBuilder builder) .Permission(Permissions.ManageIndexes) .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) .LocalNav() diff --git a/src/OrchardCore.Modules/OrchardCore.Lucene/Drivers/LuceneSettingsDisplayDriver.cs b/src/OrchardCore.Modules/OrchardCore.Lucene/Drivers/LuceneSettingsDisplayDriver.cs index 36dbba4be7f..66495cdf1a0 100644 --- a/src/OrchardCore.Modules/OrchardCore.Lucene/Drivers/LuceneSettingsDisplayDriver.cs +++ b/src/OrchardCore.Modules/OrchardCore.Lucene/Drivers/LuceneSettingsDisplayDriver.cs @@ -14,7 +14,7 @@ namespace OrchardCore.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; diff --git a/src/OrchardCore.Modules/OrchardCore.Lucene/Handler/LuceneIndexingContentHandler.cs b/src/OrchardCore.Modules/OrchardCore.Lucene/Handler/LuceneIndexingContentHandler.cs index c6b8967bb39..2506e382a3b 100644 --- a/src/OrchardCore.Modules/OrchardCore.Lucene/Handler/LuceneIndexingContentHandler.cs +++ b/src/OrchardCore.Modules/OrchardCore.Lucene/Handler/LuceneIndexingContentHandler.cs @@ -11,6 +11,7 @@ using OrchardCore.ContentPreview; using OrchardCore.Environment.Shell.Scope; using OrchardCore.Indexing; +using OrchardCore.Lucene.Model; using OrchardCore.Modules; namespace OrchardCore.Lucene.Handlers @@ -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/OrchardCore.Indexing.Abstractions/ContentIndexSettings.cs b/src/OrchardCore.Modules/OrchardCore.Lucene/Model/LuceneContentIndexSettings.cs similarity index 82% rename from src/OrchardCore/OrchardCore.Indexing.Abstractions/ContentIndexSettings.cs rename to src/OrchardCore.Modules/OrchardCore.Lucene/Model/LuceneContentIndexSettings.cs index 69040ebb27b..0896400459d 100644 --- a/src/OrchardCore/OrchardCore.Indexing.Abstractions/ContentIndexSettings.cs +++ b/src/OrchardCore.Modules/OrchardCore.Lucene/Model/LuceneContentIndexSettings.cs @@ -1,9 +1,11 @@ -namespace OrchardCore.Indexing +using OrchardCore.Indexing; + +namespace OrchardCore.Lucene.Model { /// /// Represents the indexing settings for a content part or a field. /// - public class ContentIndexSettings + public class LuceneContentIndexSettings : IContentIndexSettings { public bool Included { get; set; } public bool Stored { get; set; } @@ -26,4 +28,4 @@ public DocumentIndexOptions ToOptions() return options; } } -} +} \ No newline at end of file diff --git a/src/OrchardCore.Modules/OrchardCore.Lucene/Services/LuceneIndexingService.cs b/src/OrchardCore.Modules/OrchardCore.Lucene/Services/LuceneIndexingService.cs index 3c94e29b0af..e1084e35119 100644 --- a/src/OrchardCore.Modules/OrchardCore.Lucene/Services/LuceneIndexingService.cs +++ b/src/OrchardCore.Modules/OrchardCore.Lucene/Services/LuceneIndexingService.cs @@ -155,7 +155,7 @@ await shellScope.UsingAsync(async scope => var contentItem = await contentManager.GetAsync(task.ContentItemId); if (contentItem != null) { - publishedIndexContext = new BuildIndexContext(new DocumentIndex(task.ContentItemId), contentItem, new string[] { contentItem.ContentType }); + publishedIndexContext = new BuildIndexContext(new DocumentIndex(task.ContentItemId), contentItem, new string[] { contentItem.ContentType }, new LuceneContentIndexSettings()); await indexHandlers.InvokeAsync(x => x.BuildIndexAsync(publishedIndexContext), _logger); } } @@ -165,7 +165,7 @@ await shellScope.UsingAsync(async scope => var contentItem = await contentManager.GetAsync(task.ContentItemId, VersionOptions.Latest); if (contentItem != null) { - latestIndexContext = new BuildIndexContext(new DocumentIndex(task.ContentItemId), contentItem, new string[] { contentItem.ContentType }); + latestIndexContext = new BuildIndexContext(new DocumentIndex(task.ContentItemId), contentItem, new string[] { contentItem.ContentType }, new LuceneContentIndexSettings()); await indexHandlers.InvokeAsync(x => x.BuildIndexAsync(latestIndexContext), _logger); } } 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/ContentPartFieldIndexSettingsDisplayDriver.cs b/src/OrchardCore.Modules/OrchardCore.Lucene/Settings/ContentPartFieldIndexSettingsDisplayDriver.cs index a1364fc1a04..c9212463308 100644 --- a/src/OrchardCore.Modules/OrchardCore.Lucene/Settings/ContentPartFieldIndexSettingsDisplayDriver.cs +++ b/src/OrchardCore.Modules/OrchardCore.Lucene/Settings/ContentPartFieldIndexSettingsDisplayDriver.cs @@ -6,6 +6,7 @@ using OrchardCore.DisplayManagement.ModelBinding; using OrchardCore.DisplayManagement.Views; using OrchardCore.Indexing; +using OrchardCore.Lucene.Model; namespace OrchardCore.Lucene.Settings { @@ -27,9 +28,9 @@ public override async Task EditAsync(ContentPartFieldDefinition return null; } - return Initialize("ContentIndexSettings_Edit", model => + return Initialize("LuceneContentIndexSettings_Edit", model => { - model.ContentIndexSettings = contentPartFieldDefinition.GetSettings(); + model.LuceneContentIndexSettings = contentPartFieldDefinition.GetSettings(); }).Location("Content:10"); } @@ -40,13 +41,13 @@ public override async Task UpdateAsync(ContentPartFieldDefinitio return null; } - var model = new ContentIndexSettingsViewModel(); + var model = new LuceneContentIndexSettingsViewModel(); await context.Updater.TryUpdateModelAsync(model, Prefix); - context.Builder.WithSettings(model.ContentIndexSettings); + context.Builder.WithSettings(model.LuceneContentIndexSettings); return await EditAsync(contentPartFieldDefinition, context.Updater); } } -} +} \ No newline at end of file diff --git a/src/OrchardCore.Modules/OrchardCore.Lucene/Settings/ContentTypePartIndexSettingsDisplayDriver.cs b/src/OrchardCore.Modules/OrchardCore.Lucene/Settings/ContentTypePartIndexSettingsDisplayDriver.cs index 1b8b1990665..02c26dde0ce 100644 --- a/src/OrchardCore.Modules/OrchardCore.Lucene/Settings/ContentTypePartIndexSettingsDisplayDriver.cs +++ b/src/OrchardCore.Modules/OrchardCore.Lucene/Settings/ContentTypePartIndexSettingsDisplayDriver.cs @@ -6,6 +6,7 @@ using OrchardCore.DisplayManagement.ModelBinding; using OrchardCore.DisplayManagement.Views; using OrchardCore.Indexing; +using OrchardCore.Lucene.Model; namespace OrchardCore.Lucene.Settings { @@ -27,9 +28,9 @@ public override async Task EditAsync(ContentTypePartDefinition c return null; } - return Initialize("ContentIndexSettings_Edit", model => + return Initialize("LuceneContentIndexSettings_Edit", model => { - model.ContentIndexSettings = contentTypePartDefinition.GetSettings(); + model.LuceneContentIndexSettings = contentTypePartDefinition.GetSettings(); }).Location("Content:10"); } @@ -40,13 +41,13 @@ public override async Task UpdateAsync(ContentTypePartDefinition return null; } - var model = new ContentIndexSettingsViewModel(); + var model = new LuceneContentIndexSettingsViewModel(); await context.Updater.TryUpdateModelAsync(model, Prefix); - context.Builder.WithSettings(model.ContentIndexSettings); + context.Builder.WithSettings(model.LuceneContentIndexSettings); return await EditAsync(contentTypePartDefinition, context.Updater); } } -} +} \ No newline at end of file diff --git a/src/OrchardCore.Modules/OrchardCore.Lucene/Settings/LuceneContentIndexSettingsViewModel.cs b/src/OrchardCore.Modules/OrchardCore.Lucene/Settings/LuceneContentIndexSettingsViewModel.cs new file mode 100644 index 00000000000..107cde2059a --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Lucene/Settings/LuceneContentIndexSettingsViewModel.cs @@ -0,0 +1,9 @@ +using OrchardCore.Lucene.Model; + +namespace OrchardCore.Lucene.Settings +{ + public class LuceneContentIndexSettingsViewModel + { + public LuceneContentIndexSettings LuceneContentIndexSettings { get; set; } + } +} diff --git a/src/OrchardCore.Modules/OrchardCore.Lucene/Views/Admin/Index.cshtml b/src/OrchardCore.Modules/OrchardCore.Lucene/Views/Admin/Index.cshtml index 07622cf847f..38ecb96aa5f 100644 --- a/src/OrchardCore.Modules/OrchardCore.Lucene/Views/Admin/Index.cshtml +++ b/src/OrchardCore.Modules/OrchardCore.Lucene/Views/Admin/Index.cshtml @@ -4,7 +4,7 @@ int endIndex = startIndex + Model.Indexes.Count() - 1; } -

@RenderTitleSegments(T["Indices"])

+

@RenderTitleSegments(T["Lucene Indices"])

@* the form is necessary to generate and antiforgery token for the delete action *@
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 87f73c4c615..00000000000 --- a/src/OrchardCore.Modules/OrchardCore.Lucene/Views/ContentIndexSettings.Edit.cshtml +++ /dev/null @@ -1,24 +0,0 @@ -@model OrchardCore.Lucene.Settings.ContentIndexSettingsViewModel - -
-
- - - @T["Check to include the value of this element in the index."] -
-
- -
- - - @T["Check to be able to retrieve the value from the index."] -
- -
- - - @T["Check to analyze the value as readable text."] -
-
-
- diff --git a/src/OrchardCore.Modules/OrchardCore.Lucene/Views/LuceneContentIndexSettings.Edit.cshtml b/src/OrchardCore.Modules/OrchardCore.Lucene/Views/LuceneContentIndexSettings.Edit.cshtml new file mode 100644 index 00000000000..fd0427088c3 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Lucene/Views/LuceneContentIndexSettings.Edit.cshtml @@ -0,0 +1,25 @@ +@model OrchardCore.Lucene.Settings.LuceneContentIndexSettingsViewModel +

Lucene

+ +
+
+ + + @T["Check to include the value of this element in the index."] +
+
+ +
+ + + @T["Check to be able to retrieve the value from the index."] +
+ +
+ + + @T["Check to analyze the value as readable text."] +
+
+
+ diff --git a/src/OrchardCore.Modules/OrchardCore.Menu/Migrations/content-menu-updatefrom1.recipe.json b/src/OrchardCore.Modules/OrchardCore.Menu/Migrations/content-menu-updatefrom1.recipe.json index f86de31b601..d2cebafe794 100644 --- a/src/OrchardCore.Modules/OrchardCore.Menu/Migrations/content-menu-updatefrom1.recipe.json +++ b/src/OrchardCore.Modules/OrchardCore.Menu/Migrations/content-menu-updatefrom1.recipe.json @@ -58,7 +58,7 @@ "DisplayAllContentTypes": true, "DisplayedContentTypes": [] }, - "ContentIndexSettings": {} + "LuceneContentIndexSettings": {} } } ] diff --git a/src/OrchardCore.Modules/OrchardCore.Menu/Migrations/menu.recipe.json b/src/OrchardCore.Modules/OrchardCore.Menu/Migrations/menu.recipe.json index 42af14045da..29db047fee6 100644 --- a/src/OrchardCore.Modules/OrchardCore.Menu/Migrations/menu.recipe.json +++ b/src/OrchardCore.Modules/OrchardCore.Menu/Migrations/menu.recipe.json @@ -182,7 +182,7 @@ "DisplayAllContentTypes": true, "DisplayedContentTypes": [] }, - "ContentIndexSettings": {} + "LuceneContentIndexSettings": {} } } ] diff --git a/src/OrchardCore.Modules/OrchardCore.Search.Elastic/AdminMenu.cs b/src/OrchardCore.Modules/OrchardCore.Search.Elastic/AdminMenu.cs new file mode 100644 index 00000000000..f88edcf5ada --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Search.Elastic/AdminMenu.cs @@ -0,0 +1,48 @@ +using System; +using System.Threading.Tasks; +using Microsoft.Extensions.Localization; +using OrchardCore.Search.Elastic.Drivers; +using OrchardCore.Navigation; +using OrchardCore.Search.Elastic; + +namespace OrchardCore.Search.Elastic +{ + public class AdminMenu : INavigationProvider + { + private readonly IStringLocalizer S; + + public AdminMenu(IStringLocalizer 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["Elastic Indices"], S["Elastic Indices"].PrefixPosition(), indexes => indexes + .Action("Index", "Admin", new { area = "OrchardCore.Search.Elastic" }) + .Permission(Permissions.ManageIndexes) + .LocalNav()) + .Add(S["Run Elastic Query"], S["Run Elastic Query"].PrefixPosition(), queries => queries + .Action("Query", "Admin", new { area = "OrchardCore.Search.Elastic" }) + .Permission(Permissions.ManageIndexes) + .LocalNav())) + .Add(S["Settings"], settings => settings + .Add(S["Elastic Search"], S["Elastic Search"].PrefixPosition(), entry => entry + .Action("Index", "Admin", new { area = "OrchardCore.Settings", groupId = ElasticSettingsDisplayDriver.GroupId }) + .Permission(Permissions.ManageIndexes) + .LocalNav() + ))); + + return Task.CompletedTask; + } + } +} diff --git a/src/OrchardCore.Modules/OrchardCore.Search.Elastic/Configurations/ElasticConnectionOptions.cs b/src/OrchardCore.Modules/OrchardCore.Search.Elastic/Configurations/ElasticConnectionOptions.cs new file mode 100644 index 00000000000..eb612f0d1f8 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Search.Elastic/Configurations/ElasticConnectionOptions.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace OrchardCore.Search.Elastic.Configurations +{ + public class ElasticConnectionOptions + { + /// + /// The server url. + /// + public string Url { get; set; } + + /// + /// Whether the configuration section exists. + /// + public bool ConfigurationExists { get; set; } + } +} diff --git a/src/OrchardCore.Modules/OrchardCore.Search.Elastic/Controllers/AdminController.cs b/src/OrchardCore.Modules/OrchardCore.Search.Elastic/Controllers/AdminController.cs new file mode 100644 index 00000000000..a32002dc5a7 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Search.Elastic/Controllers/AdminController.cs @@ -0,0 +1,502 @@ +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.Liquid; +using OrchardCore.Search.Elastic.Model; +using OrchardCore.Search.Elastic.ViewModels; +using OrchardCore.Mvc.Utilities; +using OrchardCore.Navigation; +using OrchardCore.Routing; +using OrchardCore.Search.Elastic.Services; +using OrchardCore.Settings; +using YesSql; +using OrchardCore.Environment.Shell; + +namespace OrchardCore.Search.Elastic.Controllers +{ + public class AdminController : Controller + { + private readonly ISession _session; + private readonly ElasticIndexManager _elasticIndexManager; + private readonly ElasticIndexingService _elasticIndexingService; + private readonly IAuthorizationService _authorizationService; + private readonly INotifier _notifier; + private readonly ElasticAnalyzerManager _elasticAnalyzerManager; + private readonly ElasticIndexSettingsService _elasticIndexSettingsService; + private readonly IElasticQueryService _queryService; + private readonly ILiquidTemplateManager _liquidTemplateManager; + private readonly IContentDefinitionManager _contentDefinitionManager; + private readonly ISiteService _siteService; + private readonly dynamic New; + private readonly JavaScriptEncoder _javaScriptEncoder; + private readonly IStringLocalizer S; + private readonly IHtmlLocalizer H; + private readonly ILogger _logger; + private readonly IOptions _templateOptions; + private readonly ShellSettings _shellSettings; + + public AdminController( + ISession session, + IContentDefinitionManager contentDefinitionManager, + ElasticIndexManager elasticIndexManager, + ElasticIndexingService elasticIndexingService, + IAuthorizationService authorizationService, + ElasticAnalyzerManager elasticAnalyzerManager, + ElasticIndexSettingsService elasticIndexSettingsService, + IElasticQueryService queryService, + ILiquidTemplateManager liquidTemplateManager, + INotifier notifier, + ISiteService siteService, + IShapeFactory shapeFactory, + JavaScriptEncoder javaScriptEncoder, + IStringLocalizer stringLocalizer, + IHtmlLocalizer htmlLocalizer, + ILogger logger, + IOptions templateOptions, + ShellSettings shellSettings + ) + { + _session = session; + _elasticIndexManager = elasticIndexManager; + _elasticIndexingService = elasticIndexingService; + _authorizationService = authorizationService; + _elasticAnalyzerManager = elasticAnalyzerManager; + _elasticIndexSettingsService = elasticIndexSettingsService; + _queryService = queryService; + _liquidTemplateManager = liquidTemplateManager; + _contentDefinitionManager = contentDefinitionManager; + _notifier = notifier; + _siteService = siteService; + + New = shapeFactory; + _javaScriptEncoder = javaScriptEncoder; + S = stringLocalizer; + H = htmlLocalizer; + _logger = logger; + _templateOptions = templateOptions; + _shellSettings = shellSettings; + } + + public async Task Index(ContentOptions options, PagerParameters pagerParameters) + { + if (!await _authorizationService.AuthorizeAsync(User, Permissions.ManageIndexes)) + { + 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.IndexOf(options.Search, StringComparison.OrdinalIgnoreCase) >= 0); + } + + 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.ManageIndexes)) + { + 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 + }; + + return View(model); + } + + [HttpPost, ActionName("Edit")] + public async Task EditPost(ElasticIndexSettingsViewModel model, string[] indexedContentTypes) + { + if (!await _authorizationService.AuthorizeAsync(User, Permissions.ManageIndexes)) + { + return Forbid(); + } + + bool nameWasSplit = false; + //This was needed to work-around the name validation + if(!String.IsNullOrEmpty(model.IndexName)) + { + //Just before validation we remove shellName + string[] indexNameParts = model.IndexName.Split("_"); + if(indexNameParts.Length >= 1) + { + if(indexNameParts[0].ToLower() == _shellSettings.Name.ToLower()) + { + model.IndexName = indexNameParts[1]; + nameWasSplit = true; + } + } + } + + ValidateModel(model); + + if (model.IsCreate) + { + //We will need to add ShellName here to keep the indexes unique/Scoped + model.IndexName = $"{_shellSettings.Name}_{model.IndexName}".ToLower(); + + if (await _elasticIndexManager.Exists(model.IndexName)) + { + ModelState.AddModelError(nameof(ElasticIndexSettingsViewModel.IndexName), S["An index named {0} already exists.", model.IndexName]); + } + } + else + { + if(nameWasSplit) + { + model.IndexName = $"{_shellSettings.Name}_{model.IndexName}".ToLower(); + } + if (! await _elasticIndexManager.Exists(model.IndexName)) + { + ModelState.AddModelError(nameof(ElasticIndexSettingsViewModel.IndexName), S["An index named {0} doesn't exist.", 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 ?? "" }; + + // 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.", model.IndexName]); + } + else + { + try + { + var settings = new ElasticIndexSettings { IndexName = model.IndexName, AnalyzerName = model.AnalyzerName, IndexLatest = model.IndexLatest, IndexedContentTypes = indexedContentTypes, Culture = model.Culture ?? "" }; + + 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.", model.IndexName]); + } + + return RedirectToAction("Index"); + } + + [HttpPost] + public async Task Reset(string id) + { + if (!await _authorizationService.AuthorizeAsync(User, Permissions.ManageIndexes)) + { + return Forbid(); + } + + if (!await _elasticIndexManager.Exists(id)) + { + return NotFound(); + } + + _elasticIndexingService.ResetIndex(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.ManageIndexes)) + { + return Forbid(); + } + + if (! await _elasticIndexManager.Exists(id)) + { + return NotFound(); + } + + await _elasticIndexingService.RebuildIndexAsync(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.ManageIndexes)) + { + return Forbid(); + } + + if (! await _elasticIndexManager.Exists(model.IndexName)) + { + return NotFound(); + } + + try + { + await _elasticIndexingService.DeleteIndexAsync(model.IndexName); + + await _notifier.SuccessAsync(H["Index {0} deleted successfully.", model.IndexName]); + } + catch (Exception e) + { + await _notifier.ErrorAsync(H["An error occurred while deleting the index."]); + _logger.LogError("An error occurred while deleting the index " + model.IndexName, e); + } + + 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.ManageIndexes)) + { + 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 context = new ElasticQueryContext(model.IndexName); + 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 parameterizedQuery = JObject.Parse(tokenizedContent); + var elasticTopDocs = await _queryService.SearchAsync(context, parameterizedQuery); + + if (elasticTopDocs != null) + { + model.Documents = elasticTopDocs.TopDocs; + 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(ViewModels.ContentOptions options, IEnumerable itemIds) + { + if (!await _authorizationService.AuthorizeAsync(User, Permissions.ManageIndexes)) + { + 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(); + } + + _elasticIndexingService.ResetIndex(item.IndexName); + await _elasticIndexingService.ProcessContentItemsAsync(item.IndexName); + + await _notifier.SuccessAsync(H["Index {0} reset successfully.", item.IndexName]); + } + break; + case ContentsBulkAction.Rebuild: + foreach (var item in checkedContentItems) + { + if (!await _elasticIndexManager.Exists(item.IndexName)) + { + return NotFound(); + } + + await _elasticIndexingService.RebuildIndexAsync(item.IndexName); + await _elasticIndexingService.ProcessContentItemsAsync(item.IndexName); + await _notifier.SuccessAsync(H["Index {0} rebuilt successfully.", item.IndexName]); + } + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + + return RedirectToAction("Index"); + } + + private void ValidateModel(ElasticIndexSettingsViewModel model) + { + if (model.IndexedContentTypes == null || model.IndexedContentTypes.Count() < 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 (model.IndexName.ToSafeName() != model.IndexName) + { + ModelState.AddModelError(nameof(ElasticIndexSettingsViewModel.IndexName), S["The index name contains unallowed chars."]); + } + } + } +} diff --git a/src/OrchardCore.Modules/OrchardCore.Search.Elastic/Controllers/ApiController.cs b/src/OrchardCore.Modules/OrchardCore.Search.Elastic/Controllers/ApiController.cs new file mode 100644 index 00000000000..5125bb006eb --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Search.Elastic/Controllers/ApiController.cs @@ -0,0 +1,101 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Newtonsoft.Json; +using OrchardCore.Mvc.Utilities; +using OrchardCore.Search.Elastic.Model; + +namespace OrchardCore.Search.Elastic.Controllers +{ + [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] ElasticQueryModel 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(ElasticQueryModel 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] ElasticQueryModel 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(ElasticQueryModel queryModel) + { + if (!await _authorizationService.AuthorizeAsync(User, Permissions.QueryElasticApi)) + { + return this.ChallengeOrForbid("Api"); + } + + var result = await ElasticQueryApiAsync(queryModel); + + return new ObjectResult(result); + } + + private Task ElasticQueryApiAsync(ElasticQueryModel 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.Elastic/Controllers/SearchController.cs b/src/OrchardCore.Modules/OrchardCore.Search.Elastic/Controllers/SearchController.cs new file mode 100644 index 00000000000..d2593376b85 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Search.Elastic/Controllers/SearchController.cs @@ -0,0 +1,220 @@ +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 OrchardCore.ContentManagement; +using OrchardCore.ContentManagement.Records; +using OrchardCore.DisplayManagement; +using OrchardCore.Entities; +using OrchardCore.Search.Elastic.Model; +using OrchardCore.Search.Elastic.Services; +using OrchardCore.Mvc.Utilities; +using OrchardCore.Navigation; +using OrchardCore.Search.Abstractions.ViewModels; +using OrchardCore.Security.Permissions; +using OrchardCore.Settings; +using YesSql; +using YesSql.Services; + +namespace OrchardCore.Search.Elastic.Controllers +{ + public class SearchController : Controller + { + private readonly IAuthorizationService _authorizationService; + private readonly ISiteService _siteService; + private readonly ElasticIndexManager _elasticIndexProvider; + private readonly ElasticIndexingService _elasticIndexingService; + private readonly ElasticIndexSettingsService _elasticIndexSettingsService; + private readonly ElasticAnalyzerManager _elasticAnalyzerManager; + private readonly ISearchQueryService _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 elasticIndexProvider, + ElasticIndexingService elasticIndexingService, + ElasticIndexSettingsService elasticIndexSettingsService, + ElasticAnalyzerManager elasticAnalyzerManager, + ISearchQueryService searchQueryService, + ISession session, + IStringLocalizer stringLocalizer, + IEnumerable permissionProviders, + IShapeFactory shapeFactory, + ILogger logger + ) + { + _authorizationService = authorizationService; + _siteService = siteService; + _elasticIndexProvider = elasticIndexProvider; + _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.Elastic.Permissions"); + var permissions = await permissionsProvider.GetPermissionsAsync(); + + var siteSettings = await _siteService.GetSiteSettingsAsync(); + var searchSettings = siteSettings.As(); + + if (permissions.FirstOrDefault(x => x.Name == "QueryElastic" + searchSettings.SearchIndex + "Index") != null) + { + if (!await _authorizationService.AuthorizeAsync(User, permissions.FirstOrDefault(x => x.Name == "QueryElastic" + searchSettings.SearchIndex + "Index"))) + { + return this.ChallengeOrForbid(); + } + } + else + { + _logger.LogInformation("Couldn't execute Elastic search. The search index doesn't exist."); + return BadRequest("Elastic Search is not configured."); + } + + if (searchSettings.SearchIndex != null && ! await _elasticIndexProvider.Exists(searchSettings.SearchIndex)) + { + _logger.LogInformation("Couldn't execute Elastic search. The Elastic search index doesn't exist."); + return BadRequest("Elastic Search is not configured."); + } + + var elasticSettings = await _elasticIndexingService.GetElasticSettingsAsync(); + + if (elasticSettings == null || elasticSettings?.DefaultSearchFields == null) + { + _logger.LogInformation("Couldn't execute Elastic search. No Elastic settings was defined."); + return BadRequest("Elastic Search is not configured."); + } + + var elasticIndexSettings = await _elasticIndexSettingsService.GetSettingsAsync(searchSettings.SearchIndex); + + if (elasticIndexSettings == null) + { + _logger.LogInformation("Couldn't execute search. No Elastic 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); + + // We Query Elastic index + var analyzer = _elasticAnalyzerManager.CreateAnalyzer(await _elasticIndexSettingsService.GetIndexAnalyzerAsync(elasticIndexSettings.IndexName)); + + // Fetch one more result than PageSize to generate "More" links + var start = 0; + var end = pager.PageSize + 1; + + if (pagerParameters.Before != null) + { + start = Convert.ToInt32(pagerParameters.Before) - pager.PageSize - 1; + end = Convert.ToInt32(pagerParameters.Before); + } + else if (pagerParameters.After != null) + { + start = Convert.ToInt32(pagerParameters.After); + end = Convert.ToInt32(pagerParameters.After) + pager.PageSize + 1; + } + + var terms = viewModel.Terms; + if (!searchSettings.AllowElasticQueryStringQueryInSearch) + { + //Need to revisit this + //terms = QueryParser.Escape(terms); + } + + IList contentItemIds; + try + { + //var query = queryParser.Parse(terms); + contentItemIds = (await _searchQueryService.ExecuteQueryAsync(viewModel.Terms, searchSettings.SearchIndex, start, end)) + .ToList(); + } + catch (Exception e) + { + ModelState.AddModelError("Terms", S["Incorrect query syntax."]); + _logger.LogError(e, "Incorrect Elastic 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 Elastic. + 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 (start + 1 > 1) + { + pager.Before = (start + 1).ToString(); + } + else + { + pager.Before = null; + } + } + + if (containedItems.Count() == pager.PageSize + 1) + { + pager.After = (end - 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.Elastic/Deployment/ElasticIndexDeploymentSource.cs b/src/OrchardCore.Modules/OrchardCore.Search.Elastic/Deployment/ElasticIndexDeploymentSource.cs new file mode 100644 index 00000000000..25828f39de0 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Search.Elastic/Deployment/ElasticIndexDeploymentSource.cs @@ -0,0 +1,50 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Newtonsoft.Json.Linq; +using OrchardCore.Deployment; +using OrchardCore.Search.Elastic.Model; + +namespace OrchardCore.Search.Elastic.Deployment +{ + public class ElasticIndexDeploymentSource : IDeploymentSource + { + private readonly ElasticIndexSettingsService _elasticIndexSettingsService; + + public ElasticIndexDeploymentSource(ElasticIndexSettingsService elasticIndexSettingsService) + { + _elasticIndexSettingsService = elasticIndexSettingsService; + } + + public async Task ProcessDeploymentStepAsync(DeploymentStep step, DeploymentPlanResult result) + { + var elasticIndexStep = step as ElasticIndexDeploymentStep; + + if (elasticIndexStep == null) + { + return; + } + + var indexSettings = await _elasticIndexSettingsService.GetSettingsAsync(); + + var data = new JArray(); + var indicesToAdd = elasticIndexStep.IncludeAll ? indexSettings.Select(x => x.IndexName).ToArray() : elasticIndexStep.IndexNames; + + foreach (var index in indexSettings) + { + if (indicesToAdd.Contains(index.IndexName)) + { + var indexSettingsDict = new Dictionary(); + indexSettingsDict.Add(index.IndexName, index); + data.Add(JObject.FromObject(indexSettingsDict)); + } + } + + // Adding Elastic settings + result.Steps.Add(new JObject( + new JProperty("name", "elastic-index"), + new JProperty("Indices", data) + )); + } + } +} diff --git a/src/OrchardCore.Modules/OrchardCore.Search.Elastic/Deployment/ElasticIndexDeploymentStep.cs b/src/OrchardCore.Modules/OrchardCore.Search.Elastic/Deployment/ElasticIndexDeploymentStep.cs new file mode 100644 index 00000000000..5789f574ff1 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Search.Elastic/Deployment/ElasticIndexDeploymentStep.cs @@ -0,0 +1,19 @@ +using OrchardCore.Deployment; + +namespace OrchardCore.Search.Elastic.Deployment +{ + /// + /// Adds layers to a . + /// + public class ElasticIndexDeploymentStep : DeploymentStep + { + public ElasticIndexDeploymentStep() + { + Name = "ElasticIndex"; + } + + public bool IncludeAll { get; set; } = true; + + public string[] IndexNames { get; set; } + } +} diff --git a/src/OrchardCore.Modules/OrchardCore.Search.Elastic/Deployment/ElasticIndexDeploymentStepDriver.cs b/src/OrchardCore.Modules/OrchardCore.Search.Elastic/Deployment/ElasticIndexDeploymentStepDriver.cs new file mode 100644 index 00000000000..4c0d9e44561 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Search.Elastic/Deployment/ElasticIndexDeploymentStepDriver.cs @@ -0,0 +1,58 @@ +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.Elastic.ViewModels; + +namespace OrchardCore.Search.Elastic.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.Elastic/Deployment/ElasticSettingsDeploymentSource.cs b/src/OrchardCore.Modules/OrchardCore.Search.Elastic/Deployment/ElasticSettingsDeploymentSource.cs new file mode 100644 index 00000000000..4f45263441f --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Search.Elastic/Deployment/ElasticSettingsDeploymentSource.cs @@ -0,0 +1,34 @@ +using System.Threading.Tasks; +using Newtonsoft.Json.Linq; +using OrchardCore.Deployment; + +namespace OrchardCore.Search.Elastic.Deployment +{ + public class ElasticSettingsDeploymentSource : IDeploymentSource + { + private readonly ElasticIndexingService _elasticIndexingService; + + public ElasticSettingsDeploymentSource(ElasticIndexingService elasticIndexingService) + { + _elasticIndexingService = elasticIndexingService; + } + + public async Task ProcessDeploymentStepAsync(DeploymentStep step, DeploymentPlanResult result) + { + var elasticSettingsStep = step as ElasticSettingsDeploymentStep; + + if (elasticSettingsStep == null) + { + return; + } + + var elasticSettings = await _elasticIndexingService.GetElasticSettingsAsync(); + + // Adding Elastic settings + result.Steps.Add(new JObject( + new JProperty("name", "Settings"), + new JProperty("ElaticSettings", JObject.FromObject(elasticSettings)) + )); + } + } +} diff --git a/src/OrchardCore.Modules/OrchardCore.Search.Elastic/Deployment/ElasticSettingsDeploymentStep.cs b/src/OrchardCore.Modules/OrchardCore.Search.Elastic/Deployment/ElasticSettingsDeploymentStep.cs new file mode 100644 index 00000000000..a6ebd663fc1 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Search.Elastic/Deployment/ElasticSettingsDeploymentStep.cs @@ -0,0 +1,15 @@ +using OrchardCore.Deployment; + +namespace OrchardCore.Search.Elastic.Deployment +{ + /// + /// Adds layers to a . + /// + public class ElasticSettingsDeploymentStep : DeploymentStep + { + public ElasticSettingsDeploymentStep() + { + Name = "ElasticSettings"; + } + } +} diff --git a/src/OrchardCore.Modules/OrchardCore.Search.Elastic/Deployment/ElasticSettingsDeploymentStepDriver.cs b/src/OrchardCore.Modules/OrchardCore.Search.Elastic/Deployment/ElasticSettingsDeploymentStepDriver.cs new file mode 100644 index 00000000000..75bb6edc699 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Search.Elastic/Deployment/ElasticSettingsDeploymentStepDriver.cs @@ -0,0 +1,23 @@ +using OrchardCore.Deployment; +using OrchardCore.DisplayManagement.Handlers; +using OrchardCore.DisplayManagement.Views; + +namespace OrchardCore.Search.Elastic.Deployment +{ + 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.Elastic/Drivers/ElasticQueryDisplayDriver.cs b/src/OrchardCore.Modules/OrchardCore.Search.Elastic/Drivers/ElasticQueryDisplayDriver.cs new file mode 100644 index 00000000000..1382feb6d1c --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Search.Elastic/Drivers/ElasticQueryDisplayDriver.cs @@ -0,0 +1,74 @@ +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.Search.Elastic.ViewModels; +using OrchardCore.Queries; + +namespace OrchardCore.Search.Elastic.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.Elastic/Drivers/ElasticSettingsDisplayDriver.cs b/src/OrchardCore.Modules/OrchardCore.Search.Elastic/Drivers/ElasticSettingsDisplayDriver.cs new file mode 100644 index 00000000000..c2de5e5e68e --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Search.Elastic/Drivers/ElasticSettingsDisplayDriver.cs @@ -0,0 +1,74 @@ +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.Elastic.Model; +using OrchardCore.Search.Elastic.ViewModels; +using OrchardCore.Settings; + +namespace OrchardCore.Search.Elastic.Drivers +{ + public class ElasticSettingsDisplayDriver : SectionDisplayDriver + { + public const string GroupId = "elastic"; + 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.ManageIndexes)) + { + 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.ManageIndexes)) + { + 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.Elastic/GraphQL/ElasticQueryFieldTypeProvider.cs b/src/OrchardCore.Modules/OrchardCore.Search.Elastic/GraphQL/ElasticQueryFieldTypeProvider.cs new file mode 100644 index 00000000000..b7c1c0603fe --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Search.Elastic/GraphQL/ElasticQueryFieldTypeProvider.cs @@ -0,0 +1,198 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +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.Search.Elastic; + +namespace OrchardCore.Queries.Elastic.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.ResolveServiceProvider().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.ResolveServiceProvider().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.Elastic/GraphQL/Startup.cs b/src/OrchardCore.Modules/OrchardCore.Search.Elastic/GraphQL/Startup.cs new file mode 100644 index 00000000000..b556318eb05 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Search.Elastic/GraphQL/Startup.cs @@ -0,0 +1,19 @@ +using Microsoft.Extensions.DependencyInjection; +using OrchardCore.Apis.GraphQL; +using OrchardCore.Modules; +using OrchardCore.Queries.Elastic.GraphQL.Queries; + +namespace OrchardCore.Search.Elastic.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.Elastic/Handler/ElasticIndexingContentHandler.cs b/src/OrchardCore.Modules/OrchardCore.Search.Elastic/Handler/ElasticIndexingContentHandler.cs new file mode 100644 index 00000000000..9df9020b8e2 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Search.Elastic/Handler/ElasticIndexingContentHandler.cs @@ -0,0 +1,121 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using OrchardCore.ContentLocalization; +using OrchardCore.ContentManagement; +using OrchardCore.ContentManagement.Handlers; +using OrchardCore.ContentPreview; +using OrchardCore.Environment.Shell.Scope; +using OrchardCore.Indexing; +using OrchardCore.Modules; +using OrchardCore.Search.Elastic.Model; + +namespace OrchardCore.Search.Elastic.Handlers +{ + public class ElasticIndexingContentHandler : ContentHandlerBase + { + private readonly List _contexts = new List(); + private readonly IHttpContextAccessor _httpContextAccessor; + + public ElasticIndexingContentHandler(IHttpContextAccessor httpContextAccessor) + { + _httpContextAccessor = httpContextAccessor; + } + + public override Task PublishedAsync(PublishContentContext context) => AddContextAsync(context); + public override Task CreatedAsync(CreateContentContext context) => AddContextAsync(context); + public override Task UpdatedAsync(UpdateContentContext context) => AddContextAsync(context); + public override Task RemovedAsync(RemoveContentContext context) => AddContextAsync(context); + public override Task UnpublishedAsync(PublishContentContext context) => AddContextAsync(context); + + private Task AddContextAsync(ContentContextBase context) + { + // Do not index a preview content item. + if (_httpContextAccessor.HttpContext?.Features.Get()?.Previewing == true) + { + return Task.CompletedTask; + } + + if (context.ContentItem.Id == 0) + { + // Ignore that case, when Update is called on a content item which has not be "created" yet. + return Task.CompletedTask; + } + + if (_contexts.Count == 0) + { + var contexts = _contexts; + + // Using a local var prevents the lambda from holding a ref on this scoped service. + ShellScope.AddDeferredTask(scope => IndexingAsync(scope, contexts)); + } + + _contexts.Add(context); + + return Task.CompletedTask; + } + + private static async Task IndexingAsync(ShellScope scope, IEnumerable contexts) + { + var services = scope.ServiceProvider; + var contentManager = services.GetRequiredService(); + var contentItemIndexHandlers = services.GetServices(); + var elasticIndexManager = services.GetRequiredService(); + var elasticIndexSettingsService = services.GetRequiredService(); + var logger = services.GetRequiredService>(); + // Multiple items may have been updated in the same scope, e.g through a recipe. + var contextsGroupById = contexts.GroupBy(c => c.ContentItem.ContentItemId, c => c); + + // Get all contexts for each content item id. + foreach (var ContextsById in contextsGroupById) + { + // Only process the last context. + var context = ContextsById.Last(); + + ContentItem published = null, latest = null; + bool publishedLoaded = false, latestLoaded = false; + + foreach (var indexSettings in await elasticIndexSettingsService.GetSettingsAsync()) + { + var cultureAspect = await contentManager.PopulateAspectAsync(context.ContentItem); + var culture = cultureAspect.HasCulture ? cultureAspect.Culture.Name : null; + var ignoreIndexedCulture = indexSettings.Culture == "any" ? false : culture != indexSettings.Culture; + + if (indexSettings.IndexedContentTypes.Contains(context.ContentItem.ContentType) && !ignoreIndexedCulture) + { + if (!indexSettings.IndexLatest && !publishedLoaded) + { + publishedLoaded = true; + published = await contentManager.GetAsync(context.ContentItem.ContentItemId, VersionOptions.Published); + } + + if (indexSettings.IndexLatest && !latestLoaded) + { + latestLoaded = true; + latest = await contentManager.GetAsync(context.ContentItem.ContentItemId, VersionOptions.Latest); + } + + var contentItem = !indexSettings.IndexLatest ? published : latest; + + if (contentItem == null) + { + await elasticIndexManager.DeleteDocumentsAsync(indexSettings.IndexName, new string[] { context.ContentItem.ContentItemId }); + } + else + { + var buildIndexContext = new BuildIndexContext(new DocumentIndex(contentItem.ContentItemId), contentItem, new string[] { contentItem.ContentType }, new ElasticContentIndexSettings()); + await contentItemIndexHandlers.InvokeAsync(x => x.BuildIndexAsync(buildIndexContext), logger); + + await elasticIndexManager.DeleteDocumentsAsync(indexSettings.IndexName, new string[] { contentItem.ContentItemId }); + await elasticIndexManager.StoreDocumentsAsync(indexSettings.IndexName, new DocumentIndex[] { buildIndexContext.DocumentIndex }); + } + } + } + } + } + } +} diff --git a/src/OrchardCore.Modules/OrchardCore.Search.Elastic/Manifest.cs b/src/OrchardCore.Modules/OrchardCore.Search.Elastic/Manifest.cs new file mode 100644 index 00000000000..9298c3a2125 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Search.Elastic/Manifest.cs @@ -0,0 +1,28 @@ +using OrchardCore.Modules.Manifest; + +[assembly: Module( + Name = "Elastic Search", + Author = ManifestConstants.OrchardCoreTeam, + Website = ManifestConstants.OrchardCoreWebsite, + Version = ManifestConstants.OrchardCoreVersion +)] + +[assembly: Feature( + Id = "OrchardCore.Search.Elastic", + Name = "ElasticSearch", + Description = "Creates Elastic indexes to support search scenarios, introduces a preconfigured container-enabled content type.", + Dependencies = new[] + { + "OrchardCore.Indexing", + "OrchardCore.ContentTypes" + }, + Category = "Content Management" +)] + +[assembly: Feature( + Id = "OrchardCore.Search.Elastic.ContentPicker", + Name = "Elastic Content Picker", + Description = "Provides a Elastic content picker field editor.", + Dependencies = new[] { "OrchardCore.Search.Elastic", "OrchardCore.ContentFields" }, + Category = "Content Management" +)] diff --git a/src/OrchardCore.Modules/OrchardCore.Search.Elastic/Model/ElasticContentIndexSettings.cs b/src/OrchardCore.Modules/OrchardCore.Search.Elastic/Model/ElasticContentIndexSettings.cs new file mode 100644 index 00000000000..6ed0a1ea1df --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Search.Elastic/Model/ElasticContentIndexSettings.cs @@ -0,0 +1,14 @@ +using OrchardCore.Indexing; + +namespace OrchardCore.Search.Elastic.Model +{ + public class ElasticContentIndexSettings : IContentIndexSettings + { + public bool Included { get; set; } + + public DocumentIndexOptions ToOptions() + { + return DocumentIndexOptions.None; + } + } +} diff --git a/src/OrchardCore.Modules/OrchardCore.Search.Elastic/Model/ElasticIndexSettings.cs b/src/OrchardCore.Modules/OrchardCore.Search.Elastic/Model/ElasticIndexSettings.cs new file mode 100644 index 00000000000..b6854c38b27 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Search.Elastic/Model/ElasticIndexSettings.cs @@ -0,0 +1,25 @@ +using System.Collections.Generic; +using Newtonsoft.Json; +using OrchardCore.Data.Documents; + +namespace OrchardCore.Search.Elastic.Model +{ + public class ElasticIndexSettings + { + [JsonIgnore] + public string IndexName { get; set; } + + public string AnalyzerName { get; set; } + + public bool IndexLatest { get; set; } + + public string[] IndexedContentTypes { get; set; } + + public string Culture { get; set; } + } + + public class ElasticIndexSettingsDocument : Document + { + public Dictionary ElasticIndexSettings { get; set; } = new Dictionary(); + } +} diff --git a/src/OrchardCore.Modules/OrchardCore.Search.Elastic/Model/ElasticQueryModel.cs b/src/OrchardCore.Modules/OrchardCore.Search.Elastic/Model/ElasticQueryModel.cs new file mode 100644 index 00000000000..f376737d044 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Search.Elastic/Model/ElasticQueryModel.cs @@ -0,0 +1,9 @@ +namespace OrchardCore.Search.Elastic.Model +{ + public class ElasticQueryModel + { + public string IndexName { set; get; } + public string Query { set; get; } + public string Parameters { set; get; } + } +} diff --git a/src/OrchardCore.Modules/OrchardCore.Search.Elastic/Model/ElasticSettings.cs b/src/OrchardCore.Modules/OrchardCore.Search.Elastic/Model/ElasticSettings.cs new file mode 100644 index 00000000000..8948c39dbc3 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Search.Elastic/Model/ElasticSettings.cs @@ -0,0 +1,17 @@ +using OrchardCore.Contents.Indexing; + +namespace OrchardCore.Search.Elastic.Model +{ + public class ElasticSettings + { + public static readonly string[] FullTextField = new string[] { IndexingConstants.FullTextKey }; + + public static string StandardAnalyzer = "standardanalyzer"; + + public string SearchIndex { get; set; } + + public string[] DefaultSearchFields { get; set; } = FullTextField; + + public bool AllowElasticQueryStringQueryInSearch { get; set; } = false; + } +} diff --git a/src/OrchardCore.Modules/OrchardCore.Search.Elastic/OrchardCore.Search.Elastic.csproj b/src/OrchardCore.Modules/OrchardCore.Search.Elastic/OrchardCore.Search.Elastic.csproj new file mode 100644 index 00000000000..b6ac157cbc4 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Search.Elastic/OrchardCore.Search.Elastic.csproj @@ -0,0 +1,46 @@ + + + + true + + OrchardCore Elastic Search + + $(OCCMSDescription) + + Creates Elastic Search indexes to support search scenarios, introduces a preconfigured container-enabled content type. + + $(PackageTags) OrchardCoreCMS + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/OrchardCore.Modules/OrchardCore.Search.Elastic/Permissions.cs b/src/OrchardCore.Modules/OrchardCore.Search.Elastic/Permissions.cs new file mode 100644 index 00000000000..0a377551fc6 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Search.Elastic/Permissions.cs @@ -0,0 +1,52 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using OrchardCore.Security.Permissions; + +namespace OrchardCore.Search.Elastic +{ + public class Permissions : IPermissionProvider + { + private readonly ElasticIndexSettingsService _elasticIndexSettingsService; + + public static readonly Permission ManageIndexes = new Permission("ManageIndexes", "Manage Indexes"); + public static readonly Permission QueryElasticApi = new Permission("QueryElasticApi", "Query Elastic Api", new[] { ManageIndexes }); + + 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("QueryElastic" + index.IndexName + "Index", "Query Elastic " + index.IndexName + " Index", new[] { ManageIndexes }); + result.Add(permission); + } + + result.Add(ManageIndexes); + result.Add(QueryElasticApi); + + return result; + } + + public IEnumerable GetDefaultStereotypes() + { + return new[] + { + new PermissionStereotype + { + Name = "Administrator", + Permissions = new[] { ManageIndexes } + }, + new PermissionStereotype + { + Name = "Editor", + Permissions = new[] { QueryElasticApi } + } + }; + } + } +} diff --git a/src/OrchardCore.Modules/OrchardCore.Search.Elastic/Properties/AssemblyInfo.cs b/src/OrchardCore.Modules/OrchardCore.Search.Elastic/Properties/AssemblyInfo.cs new file mode 100644 index 00000000000..00fa9af54b3 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Search.Elastic/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.Elastic")] +[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.Elastic/Recipes/ElasticIndexStep.cs b/src/OrchardCore.Modules/OrchardCore.Search.Elastic/Recipes/ElasticIndexStep.cs new file mode 100644 index 00000000000..895eb02db3f --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Search.Elastic/Recipes/ElasticIndexStep.cs @@ -0,0 +1,57 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Newtonsoft.Json.Linq; +using OrchardCore.Search.Elastic.Model; +using OrchardCore.Recipes.Models; +using OrchardCore.Recipes.Services; + +namespace OrchardCore.Search.Elastic.Recipes +{ + /// + /// This recipe step creates a Elastic index. + /// + public class ElasticIndexStep : IRecipeStepHandler + { + private readonly ElasticIndexingService _elasticIndexingService; + private readonly ElasticIndexManager _elasticIndexManager; + + public ElasticIndexStep( + ElasticIndexingService elasticIndexingService, + ElasticIndexManager elasticIndexManager + ) + { + _elasticIndexManager = elasticIndexManager; + _elasticIndexingService = elasticIndexingService; + } + + public async Task ExecuteAsync(RecipeExecutionContext context) + { + if (!String.Equals(context.Name, "elastic-index", StringComparison.OrdinalIgnoreCase)) + { + return; + } + + var indices = context.Step["Indices"]; + if (indices != null) + { + foreach (var index in indices) + { + var elasticIndexSettings = index.ToObject>().FirstOrDefault(); + + if (! await _elasticIndexManager.Exists(elasticIndexSettings.Key)) + { + elasticIndexSettings.Value.IndexName = elasticIndexSettings.Key; + await _elasticIndexingService.CreateIndexAsync(elasticIndexSettings.Value); + } + } + } + } + } + + public class ContentStepModel + { + public JObject Data { get; set; } + } +} diff --git a/src/OrchardCore.Modules/OrchardCore.Search.Elastic/Services/ElasticAnalyzer.cs b/src/OrchardCore.Modules/OrchardCore.Search.Elastic/Services/ElasticAnalyzer.cs new file mode 100644 index 00000000000..75c2b49ea1a --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Search.Elastic/Services/ElasticAnalyzer.cs @@ -0,0 +1,29 @@ +using System; +using Nest; + +namespace OrchardCore.Search.Elastic.Services +{ + /// + /// All Elastic related analyzers needs to replaced + /// + public class ElasticAnalyzer : IElasticAnalyzer + { + private readonly Func _factory; + + public ElasticAnalyzer(string name, Func factory) + { + _factory = factory; + Name = name; + } + + public ElasticAnalyzer(string name, IAnalyzer instance) : this(name, () => instance) + { + } + + public string Name { get; } + public IAnalyzer CreateAnalyzer() + { + return _factory(); + } + } +} diff --git a/src/OrchardCore.Modules/OrchardCore.Search.Elastic/Services/ElasticAnalyzerManager.cs b/src/OrchardCore.Modules/OrchardCore.Search.Elastic/Services/ElasticAnalyzerManager.cs new file mode 100644 index 00000000000..306798db1d2 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Search.Elastic/Services/ElasticAnalyzerManager.cs @@ -0,0 +1,41 @@ +using System; +using System.Collections.Generic; +using Nest; +using Microsoft.Extensions.Options; + +namespace OrchardCore.Search.Elastic.Services +{ + /// + /// Coordinates implementations provided by + /// to return the list of all available objects. + /// + public class ElasticAnalyzerManager + { + private readonly Dictionary _analyzers; + + public ElasticAnalyzerManager(IOptions options) + { + _analyzers = new Dictionary(StringComparer.OrdinalIgnoreCase); + + foreach (var analyzer in options.Value.Analyzers) + { + _analyzers[analyzer.Name] = analyzer; + } + } + + public IEnumerable GetAnalyzers() + { + return _analyzers.Values; + } + + public IAnalyzer CreateAnalyzer(string name) + { + if (_analyzers.TryGetValue(name, out var analyzer)) + { + return analyzer.CreateAnalyzer(); + } + + return null; + } + } +} diff --git a/src/OrchardCore.Modules/OrchardCore.Search.Elastic/Services/ElasticContentPickerResultProvider.cs b/src/OrchardCore.Modules/OrchardCore.Search.Elastic/Services/ElasticContentPickerResultProvider.cs new file mode 100644 index 00000000000..e4cf3c3f03b --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Search.Elastic/Services/ElasticContentPickerResultProvider.cs @@ -0,0 +1,74 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using OrchardCore.ContentManagement; +using OrchardCore.Search.Elastic.Settings; + +namespace OrchardCore.Search.Elastic +{ + public class ElasticContentPickerResultProvider : IContentPickerResultProvider + { + private readonly ElasticIndexManager _elasticIndexProvider; + + public ElasticContentPickerResultProvider(ElasticIndexManager elasticIndexProvider) + { + _elasticIndexProvider = elasticIndexProvider; + } + + public string Name => "ElasticSearch"; + + public async Task> Search(ContentPickerSearchContext searchContext) + { + //Todo: this needs to be implemented in a different way for Elastic Search + //Must implement tenantscoped + var indexName = "Search"; + + var fieldSettings = searchContext.PartFieldDefinition?.GetSettings(); + if (!string.IsNullOrWhiteSpace(fieldSettings?.Index)) + { + indexName = fieldSettings.Index; + } + + if (! await _elasticIndexProvider.Exists(indexName)) + { + return new List(); + } + + var results = new List(); + + //await _elasticIndexProvider.SearchAsync(indexName, searcher => + //{ + // Query query = null; + + // if (string.IsNullOrWhiteSpace(searchContext.Query)) + // { + // query = new MatchAllDocsQuery(); + // } + // else + // { + // query = new WildcardQuery(new Term("Content.ContentItem.DisplayText.Analyzed", searchContext.Query.ToLowerInvariant() + "*")); + // } + + // var filter = new FieldCacheTermsFilter("Content.ContentItem.ContentType", searchContext.ContentTypes.ToArray()); + + // var docs = searcher.Search(query, filter, 50, Sort.RELEVANCE); + + // foreach (var hit in docs.ScoreDocs) + // { + // var doc = searcher.Doc(hit.Doc); + + // 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 + // }); + // } + + // return Task.CompletedTask; + //}); + + return results.OrderBy(x => x.DisplayText); + } + } +} diff --git a/src/OrchardCore.Modules/OrchardCore.Search.Elastic/Services/ElasticIndexManager.cs b/src/OrchardCore.Modules/OrchardCore.Search.Elastic/Services/ElasticIndexManager.cs new file mode 100644 index 00000000000..3ec9db4138a --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Search.Elastic/Services/ElasticIndexManager.cs @@ -0,0 +1,250 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +using Microsoft.Extensions.Logging; + +using Nest; + +using OrchardCore.Contents.Indexing; +using OrchardCore.Indexing; +using OrchardCore.Modules; +using OrchardCore.Search.Elastic.Services; + +namespace OrchardCore.Search.Elastic +{ + /// + /// Provides methods to manage physical Lucene indices. + /// This class is provided as a singleton to that the index searcher can be reused across requests. + /// + public class ElasticIndexManager : IDisposable + { + private readonly IElasticClient _elasticClient; + + + private readonly IClock _clock; + private readonly ILogger _logger; + private bool _disposing; + private ConcurrentDictionary _timestamps = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); + private readonly ElasticAnalyzerManager _elasticAnalyzerManager; + private readonly ElasticIndexSettingsService _elasticIndexSettingsService; + private readonly string[] IgnoredFields = { + "Analyzed", + "Sanitize", + "Normalized", + "Inherited" + }; + + public ElasticIndexManager( + IClock clock, + ILogger logger, + ElasticAnalyzerManager elasticAnalyzerManager, + ElasticIndexSettingsService elasticIndexSettingsService, + IElasticClient elasticClient + ) + { + + _clock = clock; + _logger = logger; + _elasticAnalyzerManager = elasticAnalyzerManager; + _elasticIndexSettingsService = elasticIndexSettingsService; + _elasticClient = elasticClient; + } + + public async Task CreateIndexAsync(string indexName) + { + //Get Index name scoped by ShellName + if (!await Exists(indexName)) + { + var response = await _elasticClient.Indices.CreateAsync(indexName); + return response.Acknowledged; + } + else + { + return true; + } + } + + public async Task DeleteDocumentsAsync(string indexName, IEnumerable contentItemIds) + { + bool success = true; + List> documents = new List>(); + foreach (var contentItemId in contentItemIds) + { + documents.Add(CreateElasticDocument(contentItemId)); + } + if(documents.Any()) + { + var result = await _elasticClient.DeleteManyAsync(documents, indexName); + if (result.Errors) + { + _logger.LogWarning($"There were issues deleting documents from Elastic search. {result.OriginalException}"); + } + success = result.IsValid; + } + return success; + } + + public async Task DeleteIndex(string indexName) + { + if (await Exists(indexName)) + { + var result = await _elasticClient.Indices.DeleteAsync(indexName); + return result.Acknowledged; + } + else + { + return true; + } + } + + public async Task Exists(string indexName) + { + if (string.IsNullOrWhiteSpace(indexName)) + { + return false; + } + var existResponse = await _elasticClient.Indices.ExistsAsync(indexName); + return existResponse.Exists; + } + + public async Task StoreDocumentsAsync(string indexName, IEnumerable indexDocuments) + { + //Convert Document to a structure suitable for Elastic + List> documents = new List>(); + foreach (var indexDocument in indexDocuments) + { + documents.Add(CreateElasticDocument(indexDocument)); + } + if(documents.Any()) + { + var result = await _elasticClient.IndexManyAsync(documents, indexName); + if (result.Errors) + { + _logger.LogWarning($"There were issues reported indexing the documents. {result.ServerError}"); + } + } + } + + public async Task SearchAsync(string indexName, string query) + { + ElasticTopDocs elasticTopDocs = new ElasticTopDocs(); + if (await Exists(indexName)) + { + var searchResponse = await _elasticClient.SearchAsync>(s => s + .Index(indexName) + .Query(q => new RawQuery(query)) + ); + + if (searchResponse.IsValid) + { + elasticTopDocs.Count = searchResponse.Documents.Count; + elasticTopDocs.TopDocs = searchResponse.Documents.ToList(); + } + + _timestamps[indexName] = _clock.UtcNow; + } + return elasticTopDocs; + } + + private Dictionary CreateElasticDocument(DocumentIndex documentIndex) + { + Dictionary entries = new Dictionary(); + entries.Add("ContentItemId", documentIndex.ContentItemId); + entries.Add("Id", documentIndex.ContentItemId); + + foreach (var entry in documentIndex.Entries) + { + if (entries.ContainsKey(entry.Name) + || entry.Name.Contains(IndexingConstants.FullTextKey) + || Array.Exists(IgnoredFields, x => entry.Name.Contains(x))) + { + continue; + } + + switch (entry.Type) + { + case DocumentIndex.Types.Boolean: + // store "true"/"false" for booleans + entries.Add(entry.Name, (bool)(entry.Value)); + break; + + case DocumentIndex.Types.DateTime: + if (entry.Value != null) + { + if (entry.Value is DateTimeOffset) + { + entries.Add(entry.Name, ((DateTimeOffset)(entry.Value)).UtcDateTime); + } + else + { + entries.Add(entry.Name, ((DateTime)(entry.Value)).ToUniversalTime()); + } + } + //else + //{ + // elasticDocument.Set(entry.Name, null); + //} + break; + + case DocumentIndex.Types.Integer: + if (entry.Value != null && Int64.TryParse(entry.Value.ToString(), out var value)) + { + entries.Add(entry.Name, value); + } + //else + //{ + // elasticDocument.Set(entry.Name, null); + //} + + break; + + case DocumentIndex.Types.Number: + if (entry.Value != null) + { + entries.Add(entry.Name, Convert.ToDouble(entry.Value)); + } + //else + //{ + // elasticDocument.Set(entry.Name, null); + //} + break; + + case DocumentIndex.Types.Text: + if (entry.Value != null && !String.IsNullOrEmpty(Convert.ToString(entry.Value))) + { + entries.Add(entry.Name, Convert.ToString(entry.Value)); + } + //else + //{ + // elasticDocument.Set(entry.Name, null); + //} + break; + } + + } + return entries; + } + private Dictionary CreateElasticDocument(string contentItemId) + { + Dictionary entries = new Dictionary(); + entries.Add("Id", contentItemId); + return entries; + } + public void Dispose() + { + if (_disposing) + { + return; + } + + _disposing = true; + } + ~ElasticIndexManager() + { + Dispose(); + } + } +} diff --git a/src/OrchardCore.Modules/OrchardCore.Search.Elastic/Services/ElasticIndexSettingsService.cs b/src/OrchardCore.Modules/OrchardCore.Search.Elastic/Services/ElasticIndexSettingsService.cs new file mode 100644 index 00000000000..20b668e6527 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Search.Elastic/Services/ElasticIndexSettingsService.cs @@ -0,0 +1,94 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using OrchardCore.Documents; +using OrchardCore.Environment.Shell.Scope; +using OrchardCore.Search.Elastic.Model; + +namespace OrchardCore.Search.Elastic +{ + public class ElasticIndexSettingsService + { + public ElasticIndexSettingsService() + { + } + + /// + /// Loads the index settings document from the store for updating and that should not be cached. + /// + public Task LoadDocumentAsync() => DocumentManager.GetOrCreateMutableAsync(); + + /// + /// Gets the index settings document from the cache for sharing and that should not be updated. + /// + public async Task GetDocumentAsync() + { + var document = await DocumentManager.GetOrCreateImmutableAsync(); + + foreach (var name in document.ElasticIndexSettings.Keys) + { + document.ElasticIndexSettings[name].IndexName = name; + } + + return document; + } + + public async Task> GetSettingsAsync() + { + return (await GetDocumentAsync()).ElasticIndexSettings.Values; + } + + public async Task GetSettingsAsync(string indexName) + { + var document = await GetDocumentAsync(); + + if (document.ElasticIndexSettings.TryGetValue(indexName, out var settings)) + { + return settings; + } + + return null; + } + + public async Task GetIndexAnalyzerAsync(string indexName) + { + var document = await GetDocumentAsync(); + + if (document.ElasticIndexSettings.TryGetValue(indexName, out var settings)) + { + return settings.AnalyzerName; + } + + return ElasticSettings.StandardAnalyzer; + } + + public async Task LoadIndexAnalyzerAsync(string indexName) + { + var document = await LoadDocumentAsync(); + + if (document.ElasticIndexSettings.TryGetValue(indexName, out var settings)) + { + return settings.AnalyzerName; + } + + return ElasticSettings.StandardAnalyzer; + } + + public async Task UpdateIndexAsync(ElasticIndexSettings settings) + { + var document = await LoadDocumentAsync(); + document.ElasticIndexSettings[settings.IndexName] = settings; + await DocumentManager.UpdateAsync(document); + } + + public async Task DeleteIndexAsync(string indexName) + { + var document = await LoadDocumentAsync(); + document.ElasticIndexSettings.Remove(indexName); + await DocumentManager.UpdateAsync(document); + } + + private static IDocumentManager DocumentManager => + ShellScope.Services.GetRequiredService>(); + } +} diff --git a/src/OrchardCore.Modules/OrchardCore.Search.Elastic/Services/ElasticIndexingBackgroundTask.cs b/src/OrchardCore.Modules/OrchardCore.Search.Elastic/Services/ElasticIndexingBackgroundTask.cs new file mode 100644 index 00000000000..e02a1ed94f3 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Search.Elastic/Services/ElasticIndexingBackgroundTask.cs @@ -0,0 +1,25 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using OrchardCore.BackgroundTasks; + +namespace OrchardCore.Search.Elastic +{ + + /// + /// This background task will index content items using. + /// + /// + /// This services is only registered from OrchardCore.Lucene.Worker feature. + /// + [BackgroundTask(Schedule = "* * * * *", Description = "Update Elastic indexes.")] + public class ElasticIndexingBackgroundTask : IBackgroundTask + { + public Task DoWorkAsync(IServiceProvider serviceProvider, CancellationToken cancellationToken) + { + var indexingService = serviceProvider.GetService(); + return indexingService.ProcessContentItemsAsync(); + } + } +} diff --git a/src/OrchardCore.Modules/OrchardCore.Search.Elastic/Services/ElasticIndexingService.cs b/src/OrchardCore.Modules/OrchardCore.Search.Elastic/Services/ElasticIndexingService.cs new file mode 100644 index 00000000000..de4af1bce03 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Search.Elastic/Services/ElasticIndexingService.cs @@ -0,0 +1,305 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using OrchardCore.ContentLocalization; +using OrchardCore.ContentManagement; +using OrchardCore.Entities; +using OrchardCore.Environment.Shell; +using OrchardCore.Indexing; +using OrchardCore.Search.Elastic.Model; +using OrchardCore.Modules; +using OrchardCore.Settings; + +namespace OrchardCore.Search.Elastic +{ + /// + /// This class provides services to update all the Elastic indices. It is non-rentrant so that calls + /// from different components can be done simultaneously, e.g. from a background task, an event or a UI interaction. + /// It also indexes one content item at a time and provides the result to all indices. + /// + public class ElasticIndexingService + { + private const int BatchSize = 100; + private readonly IShellHost _shellHost; + private readonly ShellSettings _shellSettings; + private readonly ElasticIndexingState _indexingState; + private readonly ElasticIndexSettingsService _elasticIndexSettingsService; + private readonly ElasticIndexManager _indexManager; + private readonly IIndexingTaskManager _indexingTaskManager; + private readonly ISiteService _siteService; + private readonly ILogger _logger; + + public ElasticIndexingService( + IShellHost shellHost, + ShellSettings shellSettings, + ElasticIndexingState indexingState, + ElasticIndexSettingsService elasticIndexSettingsService, + ElasticIndexManager indexManager, + IIndexingTaskManager indexingTaskManager, + ISiteService siteService, + ILogger logger) + { + _shellHost = shellHost; + _shellSettings = shellSettings; + _indexingState = indexingState; + _elasticIndexSettingsService = elasticIndexSettingsService; + _indexManager = indexManager; + _indexingTaskManager = indexingTaskManager; + _siteService = siteService; + _logger = logger; + } + + public async Task ProcessContentItemsAsync(string indexName = default) + { + // TODO: Lock over the filesystem in case two instances get a command to rebuild the index concurrently. + var allIndices = new Dictionary(); + var lastTaskId = Int32.MaxValue; + IEnumerable indexSettingsList = null; + + if (String.IsNullOrEmpty(indexName)) + { + indexSettingsList = await _elasticIndexSettingsService.GetSettingsAsync(); + + if (!indexSettingsList.Any()) + { + return; + } + + // Find the lowest task id to process + foreach (var indexSetting in indexSettingsList) + { + var taskId = _indexingState.GetLastTaskId(indexSetting.IndexName); + lastTaskId = Math.Min(lastTaskId, taskId); + allIndices.Add(indexSetting.IndexName, taskId); + } + } + else + { + var settings = await _elasticIndexSettingsService.GetSettingsAsync(indexName); + + if (settings == null) + { + return; + } + + indexSettingsList = new ElasticIndexSettings[1] { settings }.AsEnumerable(); + + var taskId = _indexingState.GetLastTaskId(indexName); + lastTaskId = Math.Min(lastTaskId, taskId); + allIndices.Add(indexName, taskId); + } + + if (allIndices.Count == 0) + { + return; + } + + var batch = Array.Empty(); + + do + { + // Create a scope for the content manager + var shellScope = await _shellHost.GetScopeAsync(_shellSettings); + + await shellScope.UsingAsync(async scope => + { + // Load the next batch of tasks + batch = (await _indexingTaskManager.GetIndexingTasksAsync(lastTaskId, BatchSize)).ToArray(); + + if (!batch.Any()) + { + return; + } + + var contentManager = scope.ServiceProvider.GetRequiredService(); + var indexHandlers = scope.ServiceProvider.GetServices(); + + // Pre-load all content items to prevent SELECT N+1 + var updatedContentItemIds = batch + .Where(x => x.Type == IndexingTaskTypes.Update) + .Select(x => x.ContentItemId) + .ToArray(); + + var allPublished = await contentManager.GetAsync(updatedContentItemIds); + var allLatest = await contentManager.GetAsync(updatedContentItemIds, latest: true); + + // Group all DocumentIndex by index to batch update them + var updatedDocumentsByIndex = new Dictionary>(); + + foreach (var index in allIndices) + { + updatedDocumentsByIndex[index.Key] = new List(); + } + + if (indexName != null) + { + indexSettingsList = indexSettingsList.Where(x => x.IndexName == indexName); + } + + var needLatest = indexSettingsList.FirstOrDefault(x => x.IndexLatest) != null; + var needPublished = indexSettingsList.FirstOrDefault(x => !x.IndexLatest) != null; + + var settingsByIndex = indexSettingsList.ToDictionary(x => x.IndexName, x => x); + + foreach (var task in batch) + { + if (task.Type == IndexingTaskTypes.Update) + { + BuildIndexContext publishedIndexContext = null, latestIndexContext = null; + + if (needPublished) + { + var contentItem = await contentManager.GetAsync(task.ContentItemId); + if (contentItem != null) + { + publishedIndexContext = new BuildIndexContext(new DocumentIndex(task.ContentItemId), contentItem, new string[] { contentItem.ContentType }, new ElasticContentIndexSettings()); + await indexHandlers.InvokeAsync(x => x.BuildIndexAsync(publishedIndexContext), _logger); + } + } + + if (needLatest) + { + var contentItem = await contentManager.GetAsync(task.ContentItemId, VersionOptions.Latest); + if (contentItem != null) + { + latestIndexContext = new BuildIndexContext(new DocumentIndex(task.ContentItemId), contentItem, new string[] { contentItem.ContentType }, new ElasticContentIndexSettings()); + await indexHandlers.InvokeAsync(x => x.BuildIndexAsync(latestIndexContext), _logger); + } + } + + // Update the document from the index if its lastIndexId is smaller than the current task id. + foreach (var index in allIndices) + { + if (index.Value >= task.Id || !settingsByIndex.TryGetValue(index.Key, out var settings)) + { + continue; + } + + var context = !settings.IndexLatest ? publishedIndexContext : latestIndexContext; + + //We index only if we actually found a content item in the database + if (context == null) + { + //TODO purge these content items from IndexingTask table + continue; + } + + var cultureAspect = await contentManager.PopulateAspectAsync(context.ContentItem); + var culture = cultureAspect.HasCulture ? cultureAspect.Culture.Name : null; + var ignoreIndexedCulture = settings.Culture == "any" ? false : culture != settings.Culture; + + // Ignore if the content item content type or culture is not indexed in this index + if (!settings.IndexedContentTypes.Contains(context.ContentItem.ContentType) || ignoreIndexedCulture) + { + continue; + } + + updatedDocumentsByIndex[index.Key].Add(context.DocumentIndex); + } + } + } + + // Delete all the existing documents + foreach (var index in updatedDocumentsByIndex) + { + var deletedDocuments = updatedDocumentsByIndex[index.Key].Select(x => x.ContentItemId); + + await _indexManager.DeleteDocumentsAsync(index.Key, deletedDocuments); + } + + // Submits all the new documents to the index + foreach (var index in updatedDocumentsByIndex) + { + await _indexManager.StoreDocumentsAsync(index.Key, updatedDocumentsByIndex[index.Key]); + } + + // Update task ids + lastTaskId = batch.Last().Id; + + foreach (var indexStatus in allIndices) + { + if (indexStatus.Value < lastTaskId) + { + _indexingState.SetLastTaskId(indexStatus.Key, lastTaskId); + } + } + + _indexingState.Update(); + }); + } while (batch.Length == BatchSize); + } + + /// + /// Creates a new index + /// + /// + public async Task CreateIndexAsync(ElasticIndexSettings indexSettings) + { + await _elasticIndexSettingsService.UpdateIndexAsync(indexSettings); + await RebuildIndexAsync(indexSettings.IndexName); + } + + /// + /// Update an existing index + /// + /// + public Task UpdateIndexAsync(ElasticIndexSettings indexSettings) + { + return _elasticIndexSettingsService.UpdateIndexAsync(indexSettings); + } + + /// + /// Deletes permanently an index + /// + /// + public async Task DeleteIndexAsync(string indexName) + { + //Delete the Elastic Index first + bool result = await _indexManager.DeleteIndex(indexName); + + if (result) + { + //Now delete it's setting + await _elasticIndexSettingsService.DeleteIndexAsync(indexName); + } + return result; + } + + /// + /// 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) + { + _indexingState.SetLastTaskId(indexName, 0); + _indexingState.Update(); + } + + /// + /// Deletes and recreates the full index content. + /// + public async Task RebuildIndexAsync(string indexName) + { + await _indexManager.DeleteIndex(indexName); + await _indexManager.CreateIndexAsync(indexName); + ResetIndex(indexName); + } + + public async Task GetElasticSettingsAsync() + { + var siteSettings = await _siteService.GetSiteSettingsAsync(); + + if (siteSettings.Has()) + { + return siteSettings.As(); + } + else + { + return new ElasticSettings(); + } + } + } +} diff --git a/src/OrchardCore.Modules/OrchardCore.Search.Elastic/Services/ElasticIndexingState.cs b/src/OrchardCore.Modules/OrchardCore.Search.Elastic/Services/ElasticIndexingState.cs new file mode 100644 index 00000000000..fd702ae9316 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Search.Elastic/Services/ElasticIndexingState.cs @@ -0,0 +1,73 @@ +using System.IO; +using Microsoft.Extensions.Options; +using Newtonsoft.Json.Linq; +using OrchardCore.Environment.Shell; + +namespace OrchardCore.Search.Elastic +{ + /// + /// This class persists the indexing state, a cursor, on the filesystem alongside the index itself. + /// This state has to be on the filesystem as each node has its own local storage for the index. + /// We will not need this at all + /// + public class ElasticIndexingState + { + private readonly string _indexSettingsFilename; + private readonly JObject _content; + + public ElasticIndexingState( + IOptions shellOptions, + ShellSettings shellSettings + ) + { + _indexSettingsFilename = PathExtensions.Combine( + shellOptions.Value.ShellsApplicationDataPath, + shellOptions.Value.ShellsContainerName, + shellSettings.Name, + "elastic.status.json"); + + if (!File.Exists(_indexSettingsFilename)) + { + Directory.CreateDirectory(Path.GetDirectoryName(_indexSettingsFilename)); + + File.WriteAllText(_indexSettingsFilename, new JObject().ToString(Newtonsoft.Json.Formatting.Indented)); + } + + _content = JObject.Parse(File.ReadAllText(_indexSettingsFilename)); + } + + public int GetLastTaskId(string indexName) + { + JToken value; + if (_content.TryGetValue(indexName, out value)) + { + return value.Value(); + } + else + { + lock (this) + { + _content.Add(new JProperty(indexName, 0)); + } + + return 0; + } + } + + public void SetLastTaskId(string indexName, int taskId) + { + lock (this) + { + _content[indexName] = taskId; + } + } + + public void Update() + { + lock (this) + { + File.WriteAllText(_indexSettingsFilename, _content.ToString(Newtonsoft.Json.Formatting.Indented)); + } + } + } +} diff --git a/src/OrchardCore.Modules/OrchardCore.Search.Elastic/Services/ElasticQuery.cs b/src/OrchardCore.Modules/OrchardCore.Search.Elastic/Services/ElasticQuery.cs new file mode 100644 index 00000000000..9e36a598d46 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Search.Elastic/Services/ElasticQuery.cs @@ -0,0 +1,15 @@ +using OrchardCore.Queries; + +namespace OrchardCore.Search.Elastic +{ + public class ElasticQuery : Query + { + public ElasticQuery() : base("Elastic") + { + } + + public string Index { get; set; } + public string Template { get; set; } + public bool ReturnContentItems { get; set; } + } +} diff --git a/src/OrchardCore.Modules/OrchardCore.Search.Elastic/Services/ElasticQuerySource.cs b/src/OrchardCore.Modules/OrchardCore.Search.Elastic/Services/ElasticQuerySource.cs new file mode 100644 index 00000000000..6d4aaff8baf --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Search.Elastic/Services/ElasticQuerySource.cs @@ -0,0 +1,99 @@ +using System.Collections.Generic; +using System.Linq; +using System.Text.Encodings.Web; +using System.Threading.Tasks; +using Fluid; +using Fluid.Values; +using Microsoft.Extensions.Options; +using Newtonsoft.Json.Linq; +using OrchardCore.ContentManagement; +using OrchardCore.ContentManagement.Records; +using OrchardCore.Liquid; +using OrchardCore.Queries; +using YesSql; +using YesSql.Services; + +namespace OrchardCore.Search.Elastic +{ + public class ElasticQuerySource : IQuerySource + { + private readonly ElasticIndexManager _elasticIndexProvider; + //private readonly ElasticIndexSettingsService _elasticIndexSettingsService; + //private readonly ElasticAnalyzerManager _elasticAnalyzerManager; + //private readonly IElasticQueryService _queryService; + private readonly ILiquidTemplateManager _liquidTemplateManager; + private readonly ISession _session; + private readonly JavaScriptEncoder _javaScriptEncoder; + private readonly TemplateOptions _templateOptions; + + public ElasticQuerySource( + ElasticIndexManager elasticIndexProvider, + //ElasticIndexSettingsService elasticIndexSettingsService, + //ElasticAnalyzerManager elasticAnalyzerManager, + //IElasticQueryService queryService, + ILiquidTemplateManager liquidTemplateManager, + ISession session, + JavaScriptEncoder javaScriptEncoder, + IOptions templateOptions) + { + _elasticIndexProvider = elasticIndexProvider; + //_elasticIndexSettingsService = elasticIndexSettingsService; + //_elasticAnalyzerManager = elasticAnalyzerManager; + //_queryService = queryService; + _liquidTemplateManager = liquidTemplateManager; + _session = session; + _javaScriptEncoder = javaScriptEncoder; + _templateOptions = templateOptions.Value; + } + + public string Name => "ElasticSearch"; + + public Query Create() + { + return new ElasticQuery(); + } + + public async Task ExecuteQueryAsync(Query query, IDictionary parameters) + { + var elasticQuery = query as ElasticQuery; + + //Should be renamed at OrchardCore.Queries to SearchQueryResults + + var elasticQueryResults = new ElasticQueryResults(); + + var tokenizedContent = await _liquidTemplateManager.RenderStringAsync(elasticQuery.Template, _javaScriptEncoder, parameters.Select(x => new KeyValuePair(x.Key, FluidValue.Create(x.Value, _templateOptions)))); + var parameterizedQuery = JObject.Parse(tokenizedContent); + + var elasticSearchResult = await _elasticIndexProvider.SearchAsync(elasticQuery.Index, elasticQuery.Template); + + + if (elasticQuery.ReturnContentItems) + { + // We always return an empty collection if the bottom lines queries have no results. + elasticQueryResults.Items = new List(); + + // Load corresponding content item versions + var indexedContentItemVersionIds = elasticSearchResult.TopDocs.Select(x => x.GetValueOrDefault("Content.ContentItem.ContentItemVersionId").ToString()).ToArray(); + var dbContentItems = await _session.Query(x => x.ContentItemVersionId.IsIn(indexedContentItemVersionIds)).ListAsync(); + + // Reorder the result to preserve the one from the Elastic query + if (dbContentItems.Any()) + { + var dbContentItemVersionIds = dbContentItems.ToDictionary(x => x.ContentItemVersionId, x => x); + var indexedAndInDB = indexedContentItemVersionIds.Where(dbContentItemVersionIds.ContainsKey); + elasticQueryResults.Items = indexedAndInDB.Select(x => dbContentItemVersionIds[x]).ToArray(); + } + } + else + { + var results = new List(); + foreach (var document in elasticSearchResult.TopDocs) + { + results.Add(new JObject(document.Select(x => new JProperty(x.Key, x.Value.ToString())))); + } + elasticQueryResults.Items = results; + } + return elasticQueryResults; + } + } +} diff --git a/src/OrchardCore.Modules/OrchardCore.Search.Elastic/Services/ISearchQueryService.cs b/src/OrchardCore.Modules/OrchardCore.Search.Elastic/Services/ISearchQueryService.cs new file mode 100644 index 00000000000..24c1f9706c5 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Search.Elastic/Services/ISearchQueryService.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; +using System.Threading.Tasks; + + +namespace OrchardCore.Search.Elastic +{ + public interface ISearchQueryService + { + Task> ExecuteQueryAsync(string query, string indexName, int start = 0, int end = 20); + } +} diff --git a/src/OrchardCore.Modules/OrchardCore.Search.Elastic/Services/SearchQueryService.cs b/src/OrchardCore.Modules/OrchardCore.Search.Elastic/Services/SearchQueryService.cs new file mode 100644 index 00000000000..55a1a6fac3f --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Search.Elastic/Services/SearchQueryService.cs @@ -0,0 +1,27 @@ +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace OrchardCore.Search.Elastic +{ + public class SearchQueryService : ISearchQueryService + { + private readonly ElasticIndexManager _elasticIndexManager; + + private static HashSet IdSet = new HashSet(new string[] { "ContentItemId" }); + + public SearchQueryService(ElasticIndexManager elasticIndexManager) + { + _elasticIndexManager = elasticIndexManager; + } + + public async Task> ExecuteQueryAsync(string query, string indexName, int start, int end) + { + var contentItemIds = new List(); + + await _elasticIndexManager.SearchAsync(indexName, query); + + //Here return the contentItemIds + return contentItemIds; + } + } +} diff --git a/src/OrchardCore.Modules/OrchardCore.Search.Elastic/Settings/ContentPartFieldIndexSettingsDisplayDriver.cs b/src/OrchardCore.Modules/OrchardCore.Search.Elastic/Settings/ContentPartFieldIndexSettingsDisplayDriver.cs new file mode 100644 index 00000000000..ce820955665 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Search.Elastic/Settings/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.Indexing; +using OrchardCore.Search.Elastic.Model; + +namespace OrchardCore.Search.Elastic.Settings +{ + 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.ManageIndexes)) + { + 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.ManageIndexes)) + { + 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.Elastic/Settings/ContentPickerFieldElasticEditorSettings.cs b/src/OrchardCore.Modules/OrchardCore.Search.Elastic/Settings/ContentPickerFieldElasticEditorSettings.cs new file mode 100644 index 00000000000..54c5c378c91 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Search.Elastic/Settings/ContentPickerFieldElasticEditorSettings.cs @@ -0,0 +1,12 @@ +using Microsoft.AspNetCore.Mvc.ModelBinding; + +namespace OrchardCore.Search.Elastic.Settings +{ + public class ContentPickerFieldElasticEditorSettings + { + public string Index { get; set; } + + [BindNever] + public string[] Indices { get; set; } + } +} diff --git a/src/OrchardCore.Modules/OrchardCore.Search.Elastic/Settings/ContentPickerFieldElasticEditorSettingsDriver.cs b/src/OrchardCore.Modules/OrchardCore.Search.Elastic/Settings/ContentPickerFieldElasticEditorSettingsDriver.cs new file mode 100644 index 00000000000..1d3ca7e5e70 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Search.Elastic/Settings/ContentPickerFieldElasticEditorSettingsDriver.cs @@ -0,0 +1,47 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using OrchardCore.ContentManagement.Metadata.Models; +using OrchardCore.ContentTypes.Editors; +using OrchardCore.DisplayManagement.Views; + +namespace OrchardCore.Search.Elastic.Settings +{ + 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() == "Elastic") + { + 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.Elastic/Settings/ContentTypePartIndexSettingsDisplayDriver.cs b/src/OrchardCore.Modules/OrchardCore.Search.Elastic/Settings/ContentTypePartIndexSettingsDisplayDriver.cs new file mode 100644 index 00000000000..dc83b5c5f95 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Search.Elastic/Settings/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.Indexing; +using OrchardCore.Search.Elastic.Model; + +namespace OrchardCore.Search.Elastic.Settings +{ + 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.ManageIndexes)) + { + 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.ManageIndexes)) + { + 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.Elastic/Settings/ElasticContentIndexSettingsViewModel.cs b/src/OrchardCore.Modules/OrchardCore.Search.Elastic/Settings/ElasticContentIndexSettingsViewModel.cs new file mode 100644 index 00000000000..43b62d98479 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Search.Elastic/Settings/ElasticContentIndexSettingsViewModel.cs @@ -0,0 +1,9 @@ +using OrchardCore.Search.Elastic.Model; + +namespace OrchardCore.Search.Elastic.Settings +{ + public class ElasticContentIndexSettingsViewModel + { + public ElasticContentIndexSettings ElasticContentIndexSettings { get; set; } + } +} diff --git a/src/OrchardCore.Modules/OrchardCore.Search.Elastic/Settings/TypeIndexSettings.cs b/src/OrchardCore.Modules/OrchardCore.Search.Elastic/Settings/TypeIndexSettings.cs new file mode 100644 index 00000000000..b22e2beceb7 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Search.Elastic/Settings/TypeIndexSettings.cs @@ -0,0 +1,24 @@ +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.Search.Elastic/Shapes/ElasticContentPickerShapeProvider.cs b/src/OrchardCore.Modules/OrchardCore.Search.Elastic/Shapes/ElasticContentPickerShapeProvider.cs new file mode 100644 index 00000000000..90724a52076 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Search.Elastic/Shapes/ElasticContentPickerShapeProvider.cs @@ -0,0 +1,31 @@ +using Microsoft.AspNetCore.Html; +using Microsoft.Extensions.Localization; +using OrchardCore.DisplayManagement; +using OrchardCore.DisplayManagement.Descriptors; +using OrchardCore.DisplayManagement.Shapes; +using OrchardCore.Modules; + +namespace OrchardCore.Elastic.Search +{ + [Feature("OrchardCore.Search.Elastic.ContentPicker")] + public class ElasticContentPickerShapeProvider : IShapeAttributeProvider + { + private readonly IStringLocalizer S; + + public ElasticContentPickerShapeProvider(IStringLocalizer stringLocalizer) + { + S = stringLocalizer; + } + + [Shape] + public IHtmlContent ContentPickerField_Option__Elastic(dynamic shape) + { + var selected = shape.Editor == "Elastic"; + if (selected) + { + return new HtmlString($""); + } + return new HtmlString($""); + } + } +} diff --git a/src/OrchardCore.Modules/OrchardCore.Search.Elastic/Shapes/SearchShapes.cs b/src/OrchardCore.Modules/OrchardCore.Search.Elastic/Shapes/SearchShapes.cs new file mode 100644 index 00000000000..588dd52d31d --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Search.Elastic/Shapes/SearchShapes.cs @@ -0,0 +1,53 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Html; +using Microsoft.Extensions.Localization; +using OrchardCore.ContentManagement; +using OrchardCore.DisplayManagement; +using OrchardCore.DisplayManagement.Descriptors; +using OrchardCore.DisplayManagement.Shapes; + +namespace OrchardCore.Search.Elastic +{ + public class SearchShapesTableProvider : IShapeTableProvider + { + public void Discover(ShapeTableBuilder builder) + { + builder.Describe("Search__Form") + .OnDisplaying(context => + { + dynamic searchForm = context.Shape; + }); + + builder.Describe("Search__Results") + .OnDisplaying(context => + { + dynamic searchResults = context.Shape; + }); + } + } + + public class SearchShapes : IShapeAttributeProvider + { + private readonly IStringLocalizer S; + + public SearchShapes(IStringLocalizer localizer) + { + S = localizer; + } + + [Shape] + public Task SearchForm(Shape Shape, dynamic DisplayAsync, string Terms) + { + Shape.Metadata.Type = "Search__Form"; + return DisplayAsync(Shape); + } + + [Shape] + public Task SearchResults(Shape Shape, dynamic DisplayAsync, IEnumerable ContentItems) + { + Shape.Metadata.Type = "Search__Results"; + return DisplayAsync(Shape); + } + } +} diff --git a/src/OrchardCore.Modules/OrchardCore.Search.Elastic/Startup.cs b/src/OrchardCore.Modules/OrchardCore.Search.Elastic/Startup.cs new file mode 100644 index 00000000000..330042fb95c --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Search.Elastic/Startup.cs @@ -0,0 +1,194 @@ +using System; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using OrchardCore.Admin; +using OrchardCore.ContentManagement; +using OrchardCore.ContentManagement.Handlers; +using OrchardCore.ContentTypes.Editors; +using OrchardCore.Deployment; +using OrchardCore.DisplayManagement.Descriptors; +using OrchardCore.DisplayManagement.Handlers; +using OrchardCore.Search.Elastic.Controllers; +using OrchardCore.Search.Elastic.Deployment; +using OrchardCore.Search.Elastic.Drivers; +using OrchardCore.Search.Elastic.Handlers; +using OrchardCore.Search.Elastic.Recipes; +using OrchardCore.Search.Elastic.Services; +using OrchardCore.Search.Elastic.Settings; +using OrchardCore.Modules; +using OrchardCore.Mvc.Core.Utilities; +using OrchardCore.Navigation; +using OrchardCore.Queries; +using OrchardCore.Recipes; +using OrchardCore.Security.Permissions; +using OrchardCore.Settings; +using OrchardCore.Environment.Shell.Configuration; +using Microsoft.Extensions.Logging; +using OrchardCore.Search.Elastic.Model; +using OrchardCore.Search.Elastic.Configurations; +using Nest; +using OrchardCore.Elastic.Search; +using Microsoft.Extensions.Configuration; + +namespace OrchardCore.Search.Elastic +{ + /// + /// These services are registered on the tenant service collection + /// + public class Startup : StartupBase + { + private const string ConfigSectionName = "OrchardCore_Elastic"; + 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); + services.Configure(configuration); + var url = _shellConfiguration[ConfigSectionName + $":{nameof(ElasticConnectionOptions.Url)}"]; + + if (configuration.Exists() && CheckOptions(url, _logger)) + { + services.Configure(o => o.ConfigurationExists = true); + + services.AddSingleton(); + services.AddScoped(); + services.AddScoped(); + + var settings = new ConnectionSettings(new Uri(url)); + var client = new ElasticClient(settings); + services.AddSingleton(client); + services.AddSingleton(); + + services.AddSingleton(); + services.AddSingleton(); + services.AddScoped(); + services.AddScoped(); + + services.AddScoped(); + services.AddScoped(); + + services.Configure(o => + o.Analyzers.Add(new ElasticAnalyzer(ElasticSettings.StandardAnalyzer, new StandardAnalyzer()))); + + services.AddScoped, ElasticSettingsDisplayDriver>(); + services.AddScoped, ElasticQueryDisplayDriver>(); + + services.AddScoped(); + services.AddElasticQueries(); + + // LuceneQuerySource is registered for both the Queries module and local usage + services.AddScoped(); + services.AddScoped(); + services.AddRecipeExecutionStep(); + + services.AddScoped(); + services.AddShapeAttributes(); + } + } + + public override void Configure(IApplicationBuilder app, IEndpointRouteBuilder routes, IServiceProvider serviceProvider) + { + var options = serviceProvider.GetRequiredService>().Value; + if (!options.ConfigurationExists) + { + return; + } + + routes.MapAreaControllerRoute( + name: "Elastic.Search", + areaName: "OrchardCore.Search.Elastic", + pattern: "Search", + defaults: new { controller = "Search", action = "Search" } + ); + + var adminControllerName = typeof(AdminController).ControllerName(); + + routes.MapAreaControllerRoute( + name: "Elastic.Index", + areaName: "OrchardCore.Search.Elastic", + pattern: _adminOptions.AdminUrlPrefix + "/elastic/Index", + defaults: new { controller = adminControllerName, action = nameof(AdminController.Index) } + ); + + routes.MapAreaControllerRoute( + name: "Elastic.Delete", + areaName: "OrchardCore.Search.Elastic", + pattern: _adminOptions.AdminUrlPrefix + "/Elastic/Delete/{id}", + defaults: new { controller = adminControllerName, action = nameof(AdminController.Delete) } + ); + + routes.MapAreaControllerRoute( + name: "Elastic.Query", + areaName: "OrchardCore.Search.Elastic", + pattern: _adminOptions.AdminUrlPrefix + "/Elastic/Query", + defaults: new { controller = adminControllerName, action = nameof(AdminController.Query) } + ); + + routes.MapAreaControllerRoute( + name: "Elastic.Rebuild", + areaName: "OrchardCore.Search.Elastic", + pattern: _adminOptions.AdminUrlPrefix + "/Elastic/Rebuild/{id}", + defaults: new { controller = adminControllerName, action = nameof(AdminController.Rebuild) } + ); + + routes.MapAreaControllerRoute( + name: "Elastic.Reset", + areaName: "OrchardCore.Search.Elastic", + pattern: _adminOptions.AdminUrlPrefix + "/Elastic/Reset/{id}", + defaults: new { controller = adminControllerName, action = nameof(AdminController.Reset) } + ); + } + + private static bool CheckOptions(string url, ILogger logger) + { + var optionsAreValid = true; + + if (String.IsNullOrWhiteSpace(url)) + { + logger.LogError("Elastic Search is enabled but not active because the 'Url' is missing or empty 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>(); + } + } + + [Feature("OrchardCore.Search.Elastic.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.Elastic/ViewModels/AdminIndexViewModel.cs b/src/OrchardCore.Modules/OrchardCore.Search.Elastic/ViewModels/AdminIndexViewModel.cs new file mode 100644 index 00000000000..8433ed70963 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Search.Elastic/ViewModels/AdminIndexViewModel.cs @@ -0,0 +1,38 @@ +using System.Collections.Generic; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.AspNetCore.Mvc.Rendering; + +namespace OrchardCore.Search.Elastic.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.Elastic/ViewModels/AdminQueryViewModel.cs b/src/OrchardCore.Modules/OrchardCore.Search.Elastic/ViewModels/AdminQueryViewModel.cs new file mode 100644 index 00000000000..6b335dc3187 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Search.Elastic/ViewModels/AdminQueryViewModel.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.AspNetCore.Mvc.ModelBinding; + +namespace OrchardCore.Search.Elastic.ViewModels +{ + public class AdminQueryViewModel + { + public string DecodedQuery { get; set; } + public string IndexName { get; set; } + public string Parameters { get; set; } + + [BindNever] + public int 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>(); + } +} diff --git a/src/OrchardCore.Modules/OrchardCore.Search.Elastic/ViewModels/ElasticIndexDeploymentStepViewModel.cs b/src/OrchardCore.Modules/OrchardCore.Search.Elastic/ViewModels/ElasticIndexDeploymentStepViewModel.cs new file mode 100644 index 00000000000..b1cb208ae88 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Search.Elastic/ViewModels/ElasticIndexDeploymentStepViewModel.cs @@ -0,0 +1,9 @@ +namespace OrchardCore.Search.Elastic.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.Elastic/ViewModels/ElasticIndexSettingsViewModel.cs b/src/OrchardCore.Modules/OrchardCore.Search.Elastic/ViewModels/ElasticIndexSettingsViewModel.cs new file mode 100644 index 00000000000..aa1baddb0e4 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Search.Elastic/ViewModels/ElasticIndexSettingsViewModel.cs @@ -0,0 +1,31 @@ +using System.Collections.Generic; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.AspNetCore.Mvc.Rendering; + +namespace OrchardCore.Search.Elastic.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; } + + #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.Elastic/ViewModels/ElasticQueryViewModel.cs b/src/OrchardCore.Modules/OrchardCore.Search.Elastic/ViewModels/ElasticQueryViewModel.cs new file mode 100644 index 00000000000..14d8dae051f --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Search.Elastic/ViewModels/ElasticQueryViewModel.cs @@ -0,0 +1,10 @@ +namespace OrchardCore.Search.Elastic.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.Elastic/ViewModels/ElasticSettingsViewModel.cs b/src/OrchardCore.Modules/OrchardCore.Search.Elastic/ViewModels/ElasticSettingsViewModel.cs new file mode 100644 index 00000000000..4a168356319 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Search.Elastic/ViewModels/ElasticSettingsViewModel.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; + +namespace OrchardCore.Search.Elastic.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.Elastic/ViewModels/IndexViewModel.cs b/src/OrchardCore.Modules/OrchardCore.Search.Elastic/ViewModels/IndexViewModel.cs new file mode 100644 index 00000000000..c725a6930af --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Search.Elastic/ViewModels/IndexViewModel.cs @@ -0,0 +1,11 @@ +using System; + +namespace OrchardCore.Search.Elastic.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.Elastic/ViewModels/QueryIndexViewModel.cs b/src/OrchardCore.Modules/OrchardCore.Search.Elastic/ViewModels/QueryIndexViewModel.cs new file mode 100644 index 00000000000..30cab4538fa --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Search.Elastic/ViewModels/QueryIndexViewModel.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.AspNetCore.Mvc.ModelBinding; + +namespace OrchardCore.Search.Elastic.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.Elastic/Views/Admin/Edit.cshtml b/src/OrchardCore.Modules/OrchardCore.Search.Elastic/Views/Admin/Edit.cshtml new file mode 100644 index 00000000000..99e13a252d8 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Search.Elastic/Views/Admin/Edit.cshtml @@ -0,0 +1,69 @@ +@model ElasticIndexSettingsViewModel +@if (Model.IsCreate) +{ +

@RenderTitleSegments(T["Create Index"])

+} +else +{ +

@RenderTitleSegments(T["Edit Index"])

+} +
+ + +
+ + + @T["The shell name shall be prepended and automatically. Index name to be in lower."] + +
+ +
+ + + +
+ +
+ + + @T["The content culture that it will index."] + +
+ +
+ + @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["Check to index draft if it exists, otherwise only the published version is indexed."] +
+ +
+ +
+ @if (Model.IsCreate) + { + + @T["Cancel"] + } + else + { + + @T["Cancel"] + } +
+ diff --git a/src/OrchardCore.Modules/OrchardCore.Search.Elastic/Views/Admin/Index.cshtml b/src/OrchardCore.Modules/OrchardCore.Search.Elastic/Views/Admin/Index.cshtml new file mode 100644 index 00000000000..5c133425a2c --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Search.Elastic/Views/Admin/Index.cshtml @@ -0,0 +1,149 @@ +@model AdminIndexViewModel +@{ + int startIndex = (Model.Pager.Page - 1) * (Model.Pager.PageSize) + 1; + int endIndex = startIndex + Model.Indexes.Count() - 1; +} + +

@RenderTitleSegments(T["Elastic Indices"])

+ +@* the form is necessary to generate and antiforgery token for the delete action *@ +
+ + + + +
+
+
+ + +
+
+
+
    + @if (Model.Indexes.Any()) + { +
  • +
    +
    +
    + + + + +
    +
    + +
    +
  • + @foreach (var entry in Model.Indexes) + { +
  • +
    +
    + + +
    +
    +
    @entry.Name
    +

    @entry.AnalyzerName

    +
    + +
    +
  • + } + } + else + { +
  • + +
  • + } +
+
+ +@await DisplayAsync(Model.Pager) + + diff --git a/src/OrchardCore.Modules/OrchardCore.Search.Elastic/Views/Admin/Query.cshtml b/src/OrchardCore.Modules/OrchardCore.Search.Elastic/Views/Admin/Query.cshtml new file mode 100644 index 00000000000..a44f5261cb0 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Search.Elastic/Views/Admin/Query.cshtml @@ -0,0 +1,95 @@ +@model AdminQueryViewModel +@using OrchardCore.ContentManagement; +@inject IContentManager ContentManager + +@{ + var matchAllQuery = Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes(@"{ + ""query"": { + ""match_all"": { } + } +}")); + +} + + + + + +

@RenderTitleSegments(T["Elastic Query"])

+ +
+
+
+ + + @T["The Elastic index to search on"] +
+ +
+ + + @T["You can use the Match All query to search all documents.", Html.Raw(Url.Action("Query", "Admin", new { area = "OrchardCore.Search.Elastic", 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["An optional Json object containing the parameter values for this 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()) +{ + var fieldNames = Model.Documents.SelectMany(d => d.Keys.Distinct()).Distinct().ToList(); + + + + + + + + @foreach (var name in fieldNames) + { + + } + + + + @{ int row = 1; } + @foreach (var document in Model.Documents) + { + + + @foreach (var name in fieldNames) + { + + } + + } + +
#@name
@(row++)@(document.GetValueOrDefault(name)?.ToString()?? String.Empty)
+} + diff --git a/src/OrchardCore.Modules/OrchardCore.Search.Elastic/Views/ContentPickerFieldElasticEditorSettings.Edit.cshtml b/src/OrchardCore.Modules/OrchardCore.Search.Elastic/Views/ContentPickerFieldElasticEditorSettings.Edit.cshtml new file mode 100644 index 00000000000..54204ad2ea3 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Search.Elastic/Views/ContentPickerFieldElasticEditorSettings.Edit.cshtml @@ -0,0 +1,16 @@ +@model OrchardCore.Search.Elastic.Settings.ContentPickerFieldElasticEditorSettings + +
+
+
+ + +
+ @T["The Elastic index to query for content items"] +
+
diff --git a/src/OrchardCore.Modules/OrchardCore.Search.Elastic/Views/ElasticContentIndexSettings.Edit.cshtml b/src/OrchardCore.Modules/OrchardCore.Search.Elastic/Views/ElasticContentIndexSettings.Edit.cshtml new file mode 100644 index 00000000000..375f709b2b5 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Search.Elastic/Views/ElasticContentIndexSettings.Edit.cshtml @@ -0,0 +1,12 @@ +@model OrchardCore.Search.Elastic.Settings.ElasticContentIndexSettingsViewModel + +

Elastic Search

+ +
+
+ + + @T["Check to include the value of this element in the index."] +
+
+ diff --git a/src/OrchardCore.Modules/OrchardCore.Search.Elastic/Views/ElasticQuery.Buttons.SummaryAdmin.cshtml b/src/OrchardCore.Modules/OrchardCore.Search.Elastic/Views/ElasticQuery.Buttons.SummaryAdmin.cshtml new file mode 100644 index 00000000000..33aa5d6385f --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Search.Elastic/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.Elastic/Views/ElasticQuery.Edit.cshtml b/src/OrchardCore.Modules/OrchardCore.Search.Elastic/Views/ElasticQuery.Edit.cshtml new file mode 100644 index 00000000000..34879c4080d --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Search.Elastic/Views/ElasticQuery.Edit.cshtml @@ -0,0 +1,36 @@ +@model ElasticQueryViewModel + + + + + +
+ + + @T["The Elastic index to search on"] +
+ +
+
+ + + @T["Check to return the corresponding content items."] +
+
+ +
+ + + @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.Elastic/Views/ElasticQuery.SummaryAdmin.cshtml b/src/OrchardCore.Modules/OrchardCore.Search.Elastic/Views/ElasticQuery.SummaryAdmin.cshtml new file mode 100644 index 00000000000..9922eed7101 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Search.Elastic/Views/ElasticQuery.SummaryAdmin.cshtml @@ -0,0 +1 @@ +@T["Elastic query"] diff --git a/src/OrchardCore.Modules/OrchardCore.Search.Elastic/Views/ElasticSettings.Edit.cshtml b/src/OrchardCore.Modules/OrchardCore.Search.Elastic/Views/ElasticSettings.Edit.cshtml new file mode 100644 index 00000000000..cf0745a4127 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Search.Elastic/Views/ElasticSettings.Edit.cshtml @@ -0,0 +1,35 @@ +@model ElasticSettingsViewModel + +@if (Model.SearchIndexes.Any()) +{ +
+ + + + @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["A comma separated list of fields to use for search pages. The default value is Content.ContentItem.FullText."] +
+ +
+
+ + + @T["Whether search queries should be allowed to use Elastic Search \"query string query\" syntax."] @T["See documentation"] +
+
diff --git a/src/OrchardCore.Modules/OrchardCore.Search.Elastic/Views/Items/ElasticIndexDeploymentStep.Fields.Edit.cshtml b/src/OrchardCore.Modules/OrchardCore.Search.Elastic/Views/Items/ElasticIndexDeploymentStep.Fields.Edit.cshtml new file mode 100644 index 00000000000..987baf27fb0 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Search.Elastic/Views/Items/ElasticIndexDeploymentStep.Fields.Edit.cshtml @@ -0,0 +1,62 @@ +@model ElasticIndexDeploymentStepViewModel + +@{ + var indexNames = Model.IndexNames; + var allIndexNames = Model.AllIndexNames; +} + +
@T["Search Indexes"]
+ +
+
+
+
+ +
+
+
+
+ +
+
+
+ @T["The search indexes to add as part of the plan."] +
+
+
+
+
    + @foreach (var indexName in allIndexNames) + { + var checkd = indexNames?.Contains(indexName); + +
  • +
    + +
    +
  • + } +
+
+
+
+ + diff --git a/src/OrchardCore.Modules/OrchardCore.Search.Elastic/Views/Items/ElasticIndexDeploymentStep.Fields.Summary.cshtml b/src/OrchardCore.Modules/OrchardCore.Search.Elastic/Views/Items/ElasticIndexDeploymentStep.Fields.Summary.cshtml new file mode 100644 index 00000000000..2234a0d4772 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Search.Elastic/Views/Items/ElasticIndexDeploymentStep.Fields.Summary.cshtml @@ -0,0 +1,27 @@ +@using OrchardCore.DisplayManagement.Views +@using OrchardCore.Search.Elastic.Deployment + +@model ShapeViewModel + +@{ + var includeAll = Model.Value.IncludeAll; + var indexNames = Model.Value.IndexNames; +} + +
@T["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.Elastic/Views/Items/ElasticIndexDeploymentStep.Fields.Thumbnail.cshtml b/src/OrchardCore.Modules/OrchardCore.Search.Elastic/Views/Items/ElasticIndexDeploymentStep.Fields.Thumbnail.cshtml new file mode 100644 index 00000000000..f2c3985f214 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Search.Elastic/Views/Items/ElasticIndexDeploymentStep.Fields.Thumbnail.cshtml @@ -0,0 +1,4 @@ +@model dynamic + +

@T["Search Indexes"]

+

@T["Exports all or specified search indexes."]

diff --git a/src/OrchardCore.Modules/OrchardCore.Search.Elastic/Views/Items/ElasticSettingsDeploymentStep.Fields.Edit.cshtml b/src/OrchardCore.Modules/OrchardCore.Search.Elastic/Views/Items/ElasticSettingsDeploymentStep.Fields.Edit.cshtml new file mode 100644 index 00000000000..fe55dba1e62 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Search.Elastic/Views/Items/ElasticSettingsDeploymentStep.Fields.Edit.cshtml @@ -0,0 +1,3 @@ +@model dynamic + +
@T["Search Settings"]
diff --git a/src/OrchardCore.Modules/OrchardCore.Search.Elastic/Views/Items/ElasticSettingsDeploymentStep.Fields.Summary.cshtml b/src/OrchardCore.Modules/OrchardCore.Search.Elastic/Views/Items/ElasticSettingsDeploymentStep.Fields.Summary.cshtml new file mode 100644 index 00000000000..690c196d450 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Search.Elastic/Views/Items/ElasticSettingsDeploymentStep.Fields.Summary.cshtml @@ -0,0 +1,5 @@ +@model dynamic + +
@T["Search Settings"]
+ +@T["Adds search settings to the plan."] diff --git a/src/OrchardCore.Modules/OrchardCore.Search.Elastic/Views/Items/ElasticSettingsDeploymentStep.Fields.Thumbnail.cshtml b/src/OrchardCore.Modules/OrchardCore.Search.Elastic/Views/Items/ElasticSettingsDeploymentStep.Fields.Thumbnail.cshtml new file mode 100644 index 00000000000..b868bdc6d65 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Search.Elastic/Views/Items/ElasticSettingsDeploymentStep.Fields.Thumbnail.cshtml @@ -0,0 +1,4 @@ +@model dynamic + +

@T["Search Settings"]

+

@T["Exports search settings."]

diff --git a/src/OrchardCore.Modules/OrchardCore.Search.Elastic/Views/NavigationItemText-elasticsearch.Id.cshtml b/src/OrchardCore.Modules/OrchardCore.Search.Elastic/Views/NavigationItemText-elasticsearch.Id.cshtml new file mode 100644 index 00000000000..66a2be20957 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Search.Elastic/Views/NavigationItemText-elasticsearch.Id.cshtml @@ -0,0 +1 @@ +@T["Search"] diff --git a/src/OrchardCore.Modules/OrchardCore.Search.Elastic/Views/Query-ElasticSearch.Link.cshtml b/src/OrchardCore.Modules/OrchardCore.Search.Elastic/Views/Query-ElasticSearch.Link.cshtml new file mode 100644 index 00000000000..8d931772552 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Search.Elastic/Views/Query-ElasticSearch.Link.cshtml @@ -0,0 +1,16 @@ +@model dynamic +@using OrchardCore.Search.Elastic +@inject ElasticIndexSettingsService ElasticIndexSettingsService + +@{ + var disabled = !(await ElasticIndexSettingsService.GetSettingsAsync()).Any(); +} +
+
+

@T["Elastic"]

+

@T["Queries a Elastic index."]

+
+ +
diff --git a/src/OrchardCore.Modules/OrchardCore.Search.Elastic/Views/Search-Form.cshtml b/src/OrchardCore.Modules/OrchardCore.Search.Elastic/Views/Search-Form.cshtml new file mode 100644 index 00000000000..ed252b8ef9f --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Search.Elastic/Views/Search-Form.cshtml @@ -0,0 +1,10 @@ +@model SearchFormViewModel + +
+
+ +
+ +
+
+
diff --git a/src/OrchardCore.Modules/OrchardCore.Search.Elastic/Views/Search-Results.cshtml b/src/OrchardCore.Modules/OrchardCore.Search.Elastic/Views/Search-Results.cshtml new file mode 100644 index 00000000000..20897588df0 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Search.Elastic/Views/Search-Results.cshtml @@ -0,0 +1,18 @@ +@model SearchResultsViewModel +@inject IContentItemDisplayManager DisplayManager + +@if (Model.ContentItems != null && Model.ContentItems.Any()) +{ +
    + @foreach (var contentItem in Model.ContentItems) + { +
  • + @await DisplayAsync(await DisplayManager.BuildDisplayAsync(contentItem, null, "Summary")) +
  • + } +
+} +else +{ +

@T["There are no such results."]

+} diff --git a/src/OrchardCore.Modules/OrchardCore.Search.Elastic/Views/Search/Search.cshtml b/src/OrchardCore.Modules/OrchardCore.Search.Elastic/Views/Search/Search.cshtml new file mode 100644 index 00000000000..fe6aa4033b3 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Search.Elastic/Views/Search/Search.cshtml @@ -0,0 +1,9 @@ +@model SearchIndexViewModel +@inject IContentItemDisplayManager DisplayManager + +@await DisplayAsync(Model.SearchForm) +@await DisplayAsync(Model.SearchResults) +@await DisplayAsync(Model.Pager) +@if (!ViewData.ModelState.IsValid) { + +} diff --git a/src/OrchardCore.Modules/OrchardCore.Search.Elastic/Views/_ViewImports.cshtml b/src/OrchardCore.Modules/OrchardCore.Search.Elastic/Views/_ViewImports.cshtml new file mode 100644 index 00000000000..3b548e458bb --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Search.Elastic/Views/_ViewImports.cshtml @@ -0,0 +1,14 @@ +@* + 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.Elastic.ViewModels +@using OrchardCore.Search.Abstractions.ViewModels +@using OrchardCore.ContentManagement.Display diff --git a/src/OrchardCore.Modules/OrchardCore.Seo/Migrations/socialmetasettings.recipe.json b/src/OrchardCore.Modules/OrchardCore.Seo/Migrations/socialmetasettings.recipe.json index d42a3e5c086..1ae18d21e10 100644 --- a/src/OrchardCore.Modules/OrchardCore.Seo/Migrations/socialmetasettings.recipe.json +++ b/src/OrchardCore.Modules/OrchardCore.Seo/Migrations/socialmetasettings.recipe.json @@ -47,7 +47,7 @@ "DisplayName": "Default social image", "Position": "1" }, - "ContentIndexSettings": {}, + "LuceneContentIndexSettings": {}, "MediaFieldSettings": { "Multiple": false } @@ -61,7 +61,7 @@ "DisplayName": "Open graph image", "Position": "2" }, - "ContentIndexSettings": {}, + "LuceneContentIndexSettings": {}, "MediaFieldSettings": { "Multiple": false } @@ -75,7 +75,7 @@ "DisplayName": "Twitter Image", "Position": "3" }, - "ContentIndexSettings": {}, + "LuceneContentIndexSettings": {}, "MediaFieldSettings": { "Multiple": false } @@ -131,7 +131,7 @@ "Position": "12" }, "TextFieldSettings": {}, - "ContentIndexSettings": {} + "LuceneContentIndexSettings": {} } }, { @@ -164,7 +164,7 @@ "Position": "0" }, "TextFieldSettings": {}, - "ContentIndexSettings": {} + "LuceneContentIndexSettings": {} } }, { @@ -177,7 +177,7 @@ "Position": "9" }, "TextFieldSettings": {}, - "ContentIndexSettings": {} + "LuceneContentIndexSettings": {} } }, { @@ -190,7 +190,7 @@ "Position": "5" }, "TextFieldSettings": {}, - "ContentIndexSettings": {} + "LuceneContentIndexSettings": {} } } ] diff --git a/src/OrchardCore.Themes/TheBlogTheme/Recipes/blog.recipe.json b/src/OrchardCore.Themes/TheBlogTheme/Recipes/blog.recipe.json index 6712c56e02c..0a5560625f7 100644 --- a/src/OrchardCore.Themes/TheBlogTheme/Recipes/blog.recipe.json +++ b/src/OrchardCore.Themes/TheBlogTheme/Recipes/blog.recipe.json @@ -241,7 +241,7 @@ "ContentTypePartSettings": { "Position": "1" }, - "ContentIndexSettings": { + "LuceneContentIndexSettings": { "Included": false, "Stored": false, "Analyzed": false, @@ -569,7 +569,7 @@ "DisplayName": "Banner Image", "Position": "0" }, - "ContentIndexSettings": { + "LuceneContentIndexSettings": { "Included": false, "Stored": false, "Analyzed": false, @@ -607,7 +607,7 @@ "DisplayName": "Banner Image", "Position": "1" }, - "ContentIndexSettings": { + "LuceneContentIndexSettings": { "Included": false, "Stored": false, "Analyzed": false, @@ -631,7 +631,7 @@ "DisplayMode": "Tags", "Position": "2" }, - "ContentIndexSettings": {}, + "LuceneContentIndexSettings": {}, "TaxonomyFieldSettings": { "TaxonomyContentItemId": "[js: variables('tagsContentItemId')]" }, @@ -646,7 +646,7 @@ "DisplayName": "Category", "Position": "3" }, - "ContentIndexSettings": {}, + "LuceneContentIndexSettings": {}, "TaxonomyFieldSettings": { "TaxonomyContentItemId": "[js: variables('categoriesContentItemId')]", "Unique": true, @@ -788,7 +788,7 @@ "TextFieldSettings": { "Required": true }, - "ContentIndexSettings": {} + "LuceneContentIndexSettings": {} } } ] diff --git a/src/OrchardCore/OrchardCore.Application.Cms.Core.Targets/OrchardCore.Application.Cms.Core.Targets.csproj b/src/OrchardCore/OrchardCore.Application.Cms.Core.Targets/OrchardCore.Application.Cms.Core.Targets.csproj index 5793fa21b95..244bd819375 100644 --- a/src/OrchardCore/OrchardCore.Application.Cms.Core.Targets/OrchardCore.Application.Cms.Core.Targets.csproj +++ b/src/OrchardCore/OrchardCore.Application.Cms.Core.Targets/OrchardCore.Application.Cms.Core.Targets.csproj @@ -30,6 +30,7 @@ --> + + OrchardCore Elastic Search Abstractions + $(OCCMSDescription) + + Abstractions for Lucene. + $(PackageTags) OrchardCoreCMS Abstractions + + + + + + + + + + + + + + +
\ No newline at end of file diff --git a/src/OrchardCore/OrchardCore.Search.Elastic.Core/ElasticQueryService.cs b/src/OrchardCore/OrchardCore.Search.Elastic.Core/ElasticQueryService.cs new file mode 100644 index 00000000000..f67cd154e4d --- /dev/null +++ b/src/OrchardCore/OrchardCore.Search.Elastic.Core/ElasticQueryService.cs @@ -0,0 +1,89 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Nest; +using Newtonsoft.Json.Linq; + +namespace OrchardCore.Search.Elastic +{ + public class ElasticQueryService : IElasticQueryService + { + private readonly IEnumerable _queryProviders; + private readonly IElasticClient _elasticClient; + private readonly ILogger _logger; + + public ElasticQueryService( + IEnumerable queryProviders, + IElasticClient elasticClient, + ILogger logger + ) + { + _queryProviders = queryProviders; + _elasticClient = elasticClient; + _logger = logger; + } + + public async Task SearchAsync(ElasticQueryContext context, JObject queryObj) + { + var queryProp = queryObj["query"] as JObject; + + if (queryProp == null) + { + throw new ArgumentException("Query DSL requires a [query] property"); + } + + var elasticTopDocs = new ElasticTopDocs(); + if (_elasticClient == null) + { + _logger.LogWarning("Elastic Client is not setup, please validate your Elastic Configurations"); + } + + try + { + var searchResponse = await _elasticClient.SearchAsync>(s + => s.Index(context.IndexName).Query(q => new RawQuery(queryProp.ToString()))); + if (searchResponse.IsValid) + { + elasticTopDocs.Count = searchResponse.Documents.Count; + elasticTopDocs.TopDocs = searchResponse.Documents.ToList(); + } + else + { + _logger.LogError($"Received failure response from Elastic: { searchResponse.ServerError }"); + } + } + catch (Exception ex) + { + _logger.LogError(ex, $"Error while querying elastic with exception: { ex.Message}"); + } + return elasticTopDocs; + } + + /// + /// May not be needed + /// + /// + /// + /// + public IQuery CreateQueryFragment(ElasticQueryContext context, JObject queryObj) + { + var first = queryObj.Properties().First(); + + IQuery query = null; + + foreach (var queryProvider in _queryProviders) + { + query = queryProvider.CreateQuery(this, context, first.Name, (JObject)first.Value); + + if (query != null) + { + break; + } + } + + return query; + } + } +} diff --git a/src/OrchardCore/OrchardCore.Search.Elastic.Core/OrchardCore.Search.Elastic.Core.csproj b/src/OrchardCore/OrchardCore.Search.Elastic.Core/OrchardCore.Search.Elastic.Core.csproj new file mode 100644 index 00000000000..eaa82b0a6aa --- /dev/null +++ b/src/OrchardCore/OrchardCore.Search.Elastic.Core/OrchardCore.Search.Elastic.Core.csproj @@ -0,0 +1,26 @@ + + + + OrchardCore.Search.Elastic + + OrchardCore Elastic Search Core + $(OCCMSDescription) + + Core Implementation for Elastic Search module. + $(PackageTags) OrchardCoreCMS + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/OrchardCore/OrchardCore.Search.Elastic.Core/ServiceCollectionExtensions.cs b/src/OrchardCore/OrchardCore.Search.Elastic.Core/ServiceCollectionExtensions.cs new file mode 100644 index 00000000000..31824ecc59d --- /dev/null +++ b/src/OrchardCore/OrchardCore.Search.Elastic.Core/ServiceCollectionExtensions.cs @@ -0,0 +1,17 @@ +using Microsoft.Extensions.DependencyInjection; + + +namespace OrchardCore.Search.Elastic +{ + public static class ServiceCollectionExtensions + { + /// + /// Adds Lucene queries services. + /// + public static IServiceCollection AddElasticQueries(this IServiceCollection services) + { + services.AddScoped(); + return services; + } + } +} diff --git a/test/OrchardCore.Tests/Apis/Lucene/Recipes/luceneQueryTest.json b/test/OrchardCore.Tests/Apis/Lucene/Recipes/luceneQueryTest.json index db02db51cb4..3b60bff2b9c 100644 --- a/test/OrchardCore.Tests/Apis/Lucene/Recipes/luceneQueryTest.json +++ b/test/OrchardCore.Tests/Apis/Lucene/Recipes/luceneQueryTest.json @@ -54,7 +54,7 @@ "Position": "1", "Editor": "Wysiwyg" }, - "ContentIndexSettings": { + "LuceneContentIndexSettings": { "Included": true, "Stored": true, "Analyzed": true