From 7f970d489f414ef99e2d35b788f2794348fc786b Mon Sep 17 00:00:00 2001 From: Ben Date: Tue, 4 Jun 2024 17:32:31 +0800 Subject: [PATCH] refactoring the code. --- .../{ => Admin}/SearchResults/Index.razor | 66 +++---- .../{ => Admin}/SearchResults/ModuleInfo.cs | 9 +- .../{ => Admin}/SearchResults/Settings.razor | 10 +- .../Services/SearchResultsService.cs | 23 --- .../{ => Admin}/SearchResults/Index.resx | 0 .../{ => Admin}/SearchResults/Settings.resx | 0 .../Interfaces}/ISearchResultsService.cs | 4 +- .../Services/SearchResultsService.cs | 23 +++ .../Themes/Controls/Theme/Search.razor | 6 +- .../Controllers/SearchResultsController.cs | 16 +- .../OqtaneServiceCollectionExtensions.cs | 3 +- .../Infrastructure/Jobs/SearchIndexJob.cs | 8 +- .../SiteTemplates/DefaultSiteTemplate.cs | 4 +- .../Infrastructure/UpgradeManager.cs | 46 +++++ .../Search/ModuleSearchIndexManager.cs | 85 +++++---- .../Search/ModuleSearchResultManager.cs | 17 +- .../Managers/Search/PageSearchIndexManager.cs | 43 +++-- .../Search/PageSearchResultManager.cs | 46 ----- .../Managers/Search/SearchIndexManagerBase.cs | 6 +- ...ilder.cs => SearchContentEntityBuilder.cs} | 27 ++- .../SearchContentPropertyEntityBuilder.cs | 40 +++++ .../SearchContentWordCountEntityBuilder.cs | 42 +++++ .../SearchContentWordSourceEntityBuilder.cs | 31 ++++ .../SearchDocumentPropertyEntityBuilder.cs | 40 ----- .../SearchDocumentTagEntityBuilder.cs | 37 ---- .../Tenant/05020001_AddSearchTables.cs | 32 ++-- .../HtmlText/Manager/HtmlTextManager.cs | 12 +- .../Services/SearchResultsService.cs | 36 ---- .../SearchResults/Startup/ServerStartup.cs | 24 --- Oqtane.Server/Oqtane.Server.csproj | 1 + .../Providers/DatabaseSearchProvider.cs | 165 +++++++++++++---- .../Repository/Context/TenantDBContext.cs | 7 +- .../Interfaces/ISearchContentRepository.cs | 24 +++ .../Interfaces/ISearchDocumentRepository.cs | 17 -- .../Repository/SearchContentRepository.cs | 167 ++++++++++++++++++ .../Repository/SearchDocumentRepository.cs | 136 -------------- Oqtane.Server/Services/SearchService.cs | 81 +++++++-- .../Module.css | 0 .../Interfaces/ISearchIndexManager.cs | 2 +- Oqtane.Shared/Interfaces/ISearchProvider.cs | 6 +- .../Interfaces/ISearchResultManager.cs | 2 +- .../{IModuleSearch.cs => ISearchable.cs} | 4 +- .../{SearchDocument.cs => SearchContent.cs} | 18 +- ...ntProperty.cs => SearchContentProperty.cs} | 4 +- .../Models/SearchContentWordSource.cs | 12 ++ Oqtane.Shared/Models/SearchContentWords.cs | 19 ++ Oqtane.Shared/Models/SearchDocumentTag.cs | 14 -- Oqtane.Shared/Models/SearchQuery.cs | 4 +- Oqtane.Shared/Models/SearchResult.cs | 2 +- Oqtane.Shared/Shared/Constants.cs | 16 +- Oqtane.Shared/Shared/SearchUtils.cs | 69 +------- 51 files changed, 806 insertions(+), 700 deletions(-) rename Oqtane.Client/Modules/{ => Admin}/SearchResults/Index.razor (68%) rename Oqtane.Client/Modules/{ => Admin}/SearchResults/ModuleInfo.cs (64%) rename Oqtane.Client/Modules/{ => Admin}/SearchResults/Settings.razor (83%) delete mode 100644 Oqtane.Client/Modules/SearchResults/Services/SearchResultsService.cs rename Oqtane.Client/Resources/Modules/{ => Admin}/SearchResults/Index.resx (100%) rename Oqtane.Client/Resources/Modules/{ => Admin}/SearchResults/Settings.resx (100%) rename Oqtane.Client/{Modules/SearchResults/Services => Services/Interfaces}/ISearchResultsService.cs (67%) create mode 100644 Oqtane.Client/Services/SearchResultsService.cs rename Oqtane.Server/{Modules/SearchResults => }/Controllers/SearchResultsController.cs (57%) delete mode 100644 Oqtane.Server/Managers/Search/PageSearchResultManager.cs rename Oqtane.Server/Migrations/EntityBuilders/{SearchDocumentEntityBuilder.cs => SearchContentEntityBuilder.cs} (58%) create mode 100644 Oqtane.Server/Migrations/EntityBuilders/SearchContentPropertyEntityBuilder.cs create mode 100644 Oqtane.Server/Migrations/EntityBuilders/SearchContentWordCountEntityBuilder.cs create mode 100644 Oqtane.Server/Migrations/EntityBuilders/SearchContentWordSourceEntityBuilder.cs delete mode 100644 Oqtane.Server/Migrations/EntityBuilders/SearchDocumentPropertyEntityBuilder.cs delete mode 100644 Oqtane.Server/Migrations/EntityBuilders/SearchDocumentTagEntityBuilder.cs delete mode 100644 Oqtane.Server/Modules/SearchResults/Services/SearchResultsService.cs delete mode 100644 Oqtane.Server/Modules/SearchResults/Startup/ServerStartup.cs create mode 100644 Oqtane.Server/Repository/Interfaces/ISearchContentRepository.cs delete mode 100644 Oqtane.Server/Repository/Interfaces/ISearchDocumentRepository.cs create mode 100644 Oqtane.Server/Repository/SearchContentRepository.cs delete mode 100644 Oqtane.Server/Repository/SearchDocumentRepository.cs rename Oqtane.Server/wwwroot/Modules/{Oqtane.Modules.SearchResults => Oqtane.Modules.Admin.SearchResults}/Module.css (100%) rename Oqtane.Shared/Interfaces/{IModuleSearch.cs => ISearchable.cs} (58%) rename Oqtane.Shared/Models/{SearchDocument.cs => SearchContent.cs} (57%) rename Oqtane.Shared/Models/{SearchDocumentProperty.cs => SearchContentProperty.cs} (74%) create mode 100644 Oqtane.Shared/Models/SearchContentWordSource.cs create mode 100644 Oqtane.Shared/Models/SearchContentWords.cs delete mode 100644 Oqtane.Shared/Models/SearchDocumentTag.cs diff --git a/Oqtane.Client/Modules/SearchResults/Index.razor b/Oqtane.Client/Modules/Admin/SearchResults/Index.razor similarity index 68% rename from Oqtane.Client/Modules/SearchResults/Index.razor rename to Oqtane.Client/Modules/Admin/SearchResults/Index.razor index 8ae0c087..2cbc3098 100644 --- a/Oqtane.Client/Modules/SearchResults/Index.razor +++ b/Oqtane.Client/Modules/Admin/SearchResults/Index.razor @@ -1,22 +1,26 @@ -@using Oqtane.Modules.SearchResults.Services -@namespace Oqtane.Modules.SearchResults +@using Microsoft.AspNetCore.Http +@using Oqtane.Services +@using System.Net +@namespace Oqtane.Modules.Admin.SearchResults @inherits ModuleBase @inject ISearchResultsService SearchResultsService @inject IStringLocalizer Localizer +@inject IHttpContextAccessor HttpContext
-
- @Localizer["SearchPrefix"] - - -
+
+
+ @Localizer["SearchPrefix"] + + + +
+
@@ -35,12 +39,13 @@ Format="Grid" PageSize="@_pageSize.ToString()" DisplayPages="@_displayPages.ToString()" - CurrentPage="@_currentPage.ToString()" Columns="1" - Toolbar="Bottom"> + CurrentPage="@_currentPage.ToString()" + Columns="1" + Toolbar="Bottom" + Parameters="@($"q={_keywords}")">

@context.Title

-
@context.Url

@((MarkupString)context.Snippet)

@@ -59,13 +64,16 @@
@code { + public override string RenderMode => RenderModes.Static; + private const int SearchDefaultPageSize = 10; + private SearchSortDirections _searchSortDirection = SearchSortDirections.Descending; //default sort by private SearchSortFields _searchSortField = SearchSortFields.Relevance; private string _keywords; private bool _loading; private SearchResults _searchResults; private int _currentPage = 0; - private int _pageSize = Constants.SearchDefaultPageSize; + private int _pageSize = SearchDefaultPageSize; private int _displayPages = 7; protected override async Task OnInitializedAsync() @@ -75,18 +83,9 @@ _pageSize = int.Parse(ModuleState.Settings["PageSize"]); } - if (PageState.QueryString.ContainsKey("s")) + if (PageState.QueryString.ContainsKey("q")) { - _keywords = PageState.QueryString["s"]; - } - - if (PageState.QueryString.ContainsKey("p")) - { - _currentPage = Convert.ToInt32(PageState.QueryString["p"]); - if (_currentPage < 1) - { - _currentPage = 1; - } + _keywords = WebUtility.UrlDecode(PageState.QueryString["q"]); } if (!string.IsNullOrEmpty(_keywords)) @@ -95,19 +94,9 @@ } } - private async Task KeywordsChanged(KeyboardEventArgs e) - { - if (e.Code == "Enter" || e.Code == "NumpadEnter") - { - if (!string.IsNullOrEmpty(_keywords)) - { - await Search(); - } - } - } - private async Task Search() { + _keywords = HttpContext.HttpContext.Request.Form["keywords"]; if (string.IsNullOrEmpty(_keywords)) { AddModuleMessage(Localizer["MissingKeywords"], MessageType.Warning); @@ -116,7 +105,6 @@ { ClearModuleMessage(); - _currentPage = 0; await PerformSearch(); } diff --git a/Oqtane.Client/Modules/SearchResults/ModuleInfo.cs b/Oqtane.Client/Modules/Admin/SearchResults/ModuleInfo.cs similarity index 64% rename from Oqtane.Client/Modules/SearchResults/ModuleInfo.cs rename to Oqtane.Client/Modules/Admin/SearchResults/ModuleInfo.cs index ae4bc7d2..97a6835e 100644 --- a/Oqtane.Client/Modules/SearchResults/ModuleInfo.cs +++ b/Oqtane.Client/Modules/Admin/SearchResults/ModuleInfo.cs @@ -3,19 +3,18 @@ using Oqtane.Documentation; using Oqtane.Models; using Oqtane.Shared; -namespace Oqtane.Modules.SearchResults +namespace Oqtane.Modules.Admin.SearchResults { - [PrivateApi("Mark SearchResults classes as private, since it's not very useful in the public docs")] + [PrivateApi("Mark this as private, since it's not very useful in the public docs")] public class ModuleInfo : IModule { public ModuleDefinition ModuleDefinition => new ModuleDefinition { Name = "Search Results", Description = "Display Search Results", - Version = "1.0.0", + Version = Constants.Version, ServerManagerType = "", - ReleaseVersions = "1.0.0", - SettingsType = "Oqtane.Modules.SearchResults.Settings, Oqtane.Client", + SettingsType = "Oqtane.Modules.Admin.SearchResults.Settings, Oqtane.Client", Resources = new List() { new Resource { ResourceType = ResourceType.Stylesheet, Url = "~/Module.css" } diff --git a/Oqtane.Client/Modules/SearchResults/Settings.razor b/Oqtane.Client/Modules/Admin/SearchResults/Settings.razor similarity index 83% rename from Oqtane.Client/Modules/SearchResults/Settings.razor rename to Oqtane.Client/Modules/Admin/SearchResults/Settings.razor index b38d2946..1e98f0f6 100644 --- a/Oqtane.Client/Modules/SearchResults/Settings.razor +++ b/Oqtane.Client/Modules/Admin/SearchResults/Settings.razor @@ -1,7 +1,7 @@ -@namespace Oqtane.Modules.SearchResults +@namespace Oqtane.Modules.Admin.SearchResults @inherits ModuleBase -@inject ISettingService SettingService @implements Oqtane.Interfaces.ISettingsControl +@inject ISettingService SettingService @inject IStringLocalizer Localizer @inject IStringLocalizer SharedLocalizer @@ -15,14 +15,16 @@ @code { - private string resourceType = "Oqtane.Modules.SearchResults.Settings, Oqtane.Client"; // for localization + private const string SearchDefaultPageSize = "10"; + + private string resourceType = "Oqtane.Modules.Admin.SearchResults.Settings, Oqtane.Client"; // for localization private string _pageSize; protected override void OnInitialized() { try { - _pageSize = SettingService.GetSetting(ModuleState.Settings, "PageSize", Constants.SearchDefaultPageSize.ToString()); + _pageSize = SettingService.GetSetting(ModuleState.Settings, "PageSize", SearchDefaultPageSize); } catch (Exception ex) { diff --git a/Oqtane.Client/Modules/SearchResults/Services/SearchResultsService.cs b/Oqtane.Client/Modules/SearchResults/Services/SearchResultsService.cs deleted file mode 100644 index 3b241ea4..00000000 --- a/Oqtane.Client/Modules/SearchResults/Services/SearchResultsService.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System.Collections.Generic; -using System.Net.Http; -using System.Threading.Tasks; -using Oqtane.Documentation; -using Oqtane.Models; -using Oqtane.Services; -using Oqtane.Shared; - -namespace Oqtane.Modules.SearchResults.Services -{ - [PrivateApi("Mark SearchResults classes as private, since it's not very useful in the public docs")] - public class SearchResultsService : ServiceBase, ISearchResultsService, IClientService - { - public SearchResultsService(HttpClient http, SiteState siteState) : base(http, siteState) {} - - private string ApiUrl => CreateApiUrl("SearchResults"); - - public async Task SearchAsync(int moduleId, SearchQuery searchQuery) - { - return await PostJsonAsync(CreateAuthorizationPolicyUrl(ApiUrl, EntityNames.Module, moduleId), searchQuery); - } - } -} diff --git a/Oqtane.Client/Resources/Modules/SearchResults/Index.resx b/Oqtane.Client/Resources/Modules/Admin/SearchResults/Index.resx similarity index 100% rename from Oqtane.Client/Resources/Modules/SearchResults/Index.resx rename to Oqtane.Client/Resources/Modules/Admin/SearchResults/Index.resx diff --git a/Oqtane.Client/Resources/Modules/SearchResults/Settings.resx b/Oqtane.Client/Resources/Modules/Admin/SearchResults/Settings.resx similarity index 100% rename from Oqtane.Client/Resources/Modules/SearchResults/Settings.resx rename to Oqtane.Client/Resources/Modules/Admin/SearchResults/Settings.resx diff --git a/Oqtane.Client/Modules/SearchResults/Services/ISearchResultsService.cs b/Oqtane.Client/Services/Interfaces/ISearchResultsService.cs similarity index 67% rename from Oqtane.Client/Modules/SearchResults/Services/ISearchResultsService.cs rename to Oqtane.Client/Services/Interfaces/ISearchResultsService.cs index 2f1c1308..0671117c 100644 --- a/Oqtane.Client/Modules/SearchResults/Services/ISearchResultsService.cs +++ b/Oqtane.Client/Services/Interfaces/ISearchResultsService.cs @@ -3,11 +3,11 @@ using System.Threading.Tasks; using Oqtane.Documentation; using Oqtane.Models; -namespace Oqtane.Modules.SearchResults.Services +namespace Oqtane.Services { [PrivateApi("Mark SearchResults classes as private, since it's not very useful in the public docs")] public interface ISearchResultsService { - Task SearchAsync(int moduleId, SearchQuery searchQuery); + Task SearchAsync(int moduleId, SearchQuery searchQuery); } } diff --git a/Oqtane.Client/Services/SearchResultsService.cs b/Oqtane.Client/Services/SearchResultsService.cs new file mode 100644 index 00000000..da9687d5 --- /dev/null +++ b/Oqtane.Client/Services/SearchResultsService.cs @@ -0,0 +1,23 @@ +using System.Collections.Generic; +using System.Net.Http; +using System.Threading.Tasks; +using Oqtane.Documentation; +using Oqtane.Models; +using Oqtane.Modules; +using Oqtane.Shared; + +namespace Oqtane.Services +{ + [PrivateApi("Don't show in the documentation, as everything should use the Interface")] + public class SearchResultsService : ServiceBase, ISearchResultsService, IClientService + { + public SearchResultsService(HttpClient http, SiteState siteState) : base(http, siteState) { } + + private string ApiUrl => CreateApiUrl("SearchResults"); + + public async Task SearchAsync(int moduleId, SearchQuery searchQuery) + { + return await PostJsonAsync(CreateAuthorizationPolicyUrl(ApiUrl, EntityNames.Module, moduleId), searchQuery); + } + } +} diff --git a/Oqtane.Client/Themes/Controls/Theme/Search.razor b/Oqtane.Client/Themes/Controls/Theme/Search.razor index dbd28b40..fc4980ef 100644 --- a/Oqtane.Client/Themes/Controls/Theme/Search.razor +++ b/Oqtane.Client/Themes/Controls/Theme/Search.razor @@ -16,7 +16,7 @@ @bind-value="_keywords" placeholder="@Localizer["SearchPlaceHolder"]" aria-label="Search" /> - @@ -26,7 +26,7 @@ @code { - private const string SearchResultPagePath = "search-results"; + private const string SearchResultPagePath = "search"; private Page _searchResultsPage; private string _keywords = ""; @@ -48,7 +48,7 @@ var keywords = HttpContext.HttpContext.Request.Form["keywords"]; if (!string.IsNullOrEmpty(keywords) && _searchResultsPage != null) { - var url = NavigateUrl(_searchResultsPage.Path, $"s={keywords}"); + var url = NavigateUrl(_searchResultsPage.Path, $"q={keywords}"); NavigationManager.NavigateTo(url); } } diff --git a/Oqtane.Server/Modules/SearchResults/Controllers/SearchResultsController.cs b/Oqtane.Server/Controllers/SearchResultsController.cs similarity index 57% rename from Oqtane.Server/Modules/SearchResults/Controllers/SearchResultsController.cs rename to Oqtane.Server/Controllers/SearchResultsController.cs index d134b1ae..4b72a464 100644 --- a/Oqtane.Server/Modules/SearchResults/Controllers/SearchResultsController.cs +++ b/Oqtane.Server/Controllers/SearchResultsController.cs @@ -4,24 +4,22 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; -using Oqtane.Controllers; using Oqtane.Documentation; using Oqtane.Enums; using Oqtane.Infrastructure; -using Oqtane.Modules.SearchResults.Services; +using Oqtane.Services; using Oqtane.Shared; -namespace Oqtane.Modules.SearchResults.Controllers +namespace Oqtane.Controllers { [Route(ControllerRoutes.ApiRoute)] - [PrivateApi("Mark SearchResults classes as private, since it's not very useful in the public docs")] public class SearchResultsController : ModuleControllerBase { - private readonly ISearchResultsService _searchResultsService; + private readonly ISearchService _searchService; - public SearchResultsController(ISearchResultsService searchResultsService, ILogManager logger, IHttpContextAccessor accessor) : base(logger, accessor) + public SearchResultsController(ISearchService searchService, ILogManager logger, IHttpContextAccessor accessor) : base(logger, accessor) { - _searchResultsService = searchResultsService; + _searchService = searchService; } [HttpPost] @@ -30,9 +28,9 @@ namespace Oqtane.Modules.SearchResults.Controllers { try { - return await _searchResultsService.SearchAsync(AuthEntityId(EntityNames.Module), searchQuery); + return await _searchService.SearchAsync(searchQuery); } - catch(Exception ex) + catch (Exception ex) { _logger.Log(LogLevel.Error, this, LogFunction.Other, ex, "Fetch search results failed.", searchQuery); HttpContext.Response.StatusCode = (int)HttpStatusCode.BadRequest; diff --git a/Oqtane.Server/Extensions/OqtaneServiceCollectionExtensions.cs b/Oqtane.Server/Extensions/OqtaneServiceCollectionExtensions.cs index a6612866..d018a4bc 100644 --- a/Oqtane.Server/Extensions/OqtaneServiceCollectionExtensions.cs +++ b/Oqtane.Server/Extensions/OqtaneServiceCollectionExtensions.cs @@ -98,6 +98,7 @@ namespace Microsoft.Extensions.DependencyInjection services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddScoped(); @@ -134,7 +135,7 @@ namespace Microsoft.Extensions.DependencyInjection services.AddTransient(); services.AddTransient(); services.AddTransient(); - services.AddTransient(); + services.AddTransient(); // managers services.AddTransient(); diff --git a/Oqtane.Server/Infrastructure/Jobs/SearchIndexJob.cs b/Oqtane.Server/Infrastructure/Jobs/SearchIndexJob.cs index e6b7f216..8ad3e194 100644 --- a/Oqtane.Server/Infrastructure/Jobs/SearchIndexJob.cs +++ b/Oqtane.Server/Infrastructure/Jobs/SearchIndexJob.cs @@ -11,6 +11,8 @@ namespace Oqtane.Infrastructure { public class SearchIndexJob : HostedServiceBase { + private const string SearchIndexStartTimeSettingName = "SearchIndex_StartTime"; + public SearchIndexJob(IServiceScopeFactory serviceScopeFactory) : base(serviceScopeFactory) { Name = "Search Index Job"; @@ -54,7 +56,7 @@ namespace Oqtane.Infrastructure private DateTime? GetSearchStartTime(int siteId, ISettingRepository settingRepository) { - var setting = settingRepository.GetSetting(EntityNames.Site, siteId, Constants.SearchIndexStartTimeSettingName); + var setting = settingRepository.GetSetting(EntityNames.Site, siteId, SearchIndexStartTimeSettingName); if(setting == null) { return null; @@ -65,14 +67,14 @@ namespace Oqtane.Infrastructure private void UpdateSearchStartTime(int siteId, DateTime startTime, ISettingRepository settingRepository) { - var setting = settingRepository.GetSetting(EntityNames.Site, siteId, Constants.SearchIndexStartTimeSettingName); + var setting = settingRepository.GetSetting(EntityNames.Site, siteId, SearchIndexStartTimeSettingName); if (setting == null) { setting = new Setting { EntityName = EntityNames.Site, EntityId = siteId, - SettingName = Constants.SearchIndexStartTimeSettingName, + SettingName = SearchIndexStartTimeSettingName, SettingValue = Convert.ToString(startTime), }; diff --git a/Oqtane.Server/Infrastructure/SiteTemplates/DefaultSiteTemplate.cs b/Oqtane.Server/Infrastructure/SiteTemplates/DefaultSiteTemplate.cs index 99bb8aaa..447678ed 100644 --- a/Oqtane.Server/Infrastructure/SiteTemplates/DefaultSiteTemplate.cs +++ b/Oqtane.Server/Infrastructure/SiteTemplates/DefaultSiteTemplate.cs @@ -138,7 +138,7 @@ namespace Oqtane.SiteTemplates Name = "Search Results", Parent = "", Order = 7, - Path = "search-results", + Path = "search", Icon = "oi oi-magnifying-glass", IsNavigation = false, IsPersonalizable = false, @@ -148,7 +148,7 @@ namespace Oqtane.SiteTemplates new Permission(PermissionNames.Edit, RoleNames.Admin, true) }, PageTemplateModules = new List { - new PageTemplateModule { ModuleDefinitionName = "Oqtane.Modules.SearchResults, Oqtane.Client", Title = "Search Results", Pane = PaneNames.Default, + new PageTemplateModule { ModuleDefinitionName = "Oqtane.Modules.Admin.SearchResults, Oqtane.Client", Title = "Search Results", Pane = PaneNames.Default, PermissionList = new List { new Permission(PermissionNames.View, RoleNames.Everyone, true), new Permission(PermissionNames.View, RoleNames.Admin, true), diff --git a/Oqtane.Server/Infrastructure/UpgradeManager.cs b/Oqtane.Server/Infrastructure/UpgradeManager.cs index 3f0dd0f6..8737dd65 100644 --- a/Oqtane.Server/Infrastructure/UpgradeManager.cs +++ b/Oqtane.Server/Infrastructure/UpgradeManager.cs @@ -66,6 +66,9 @@ namespace Oqtane.Infrastructure case "5.1.0": Upgrade_5_1_0(tenant, scope); break; + case "5.2.0": + Upgrade_5_2_0(tenant, scope); + break; } } } @@ -385,5 +388,48 @@ namespace Oqtane.Infrastructure } } + + private void Upgrade_5_2_0(Tenant tenant, IServiceScope scope) + { + CreateSearchResultsPages(tenant, scope); + } + + private void CreateSearchResultsPages(Tenant tenant, IServiceScope scope) + { + var pageTemplates = new List(); + pageTemplates.Add(new PageTemplate + { + Name = "Search Results", + Parent = "", + Path = "search", + Icon = "oi oi-magnifying-glass", + IsNavigation = false, + IsPersonalizable = false, + PermissionList = new List { + new Permission(PermissionNames.View, RoleNames.Everyone, true), + new Permission(PermissionNames.View, RoleNames.Admin, true), + new Permission(PermissionNames.Edit, RoleNames.Admin, true) + }, + PageTemplateModules = new List { + new PageTemplateModule { ModuleDefinitionName = "Oqtane.Modules.Admin.SearchResults, Oqtane.Client", Title = "Search Results", Pane = PaneNames.Default, + PermissionList = new List { + new Permission(PermissionNames.View, RoleNames.Everyone, true), + new Permission(PermissionNames.View, RoleNames.Admin, true), + new Permission(PermissionNames.Edit, RoleNames.Admin, true) + } + } + } + }); + + var pages = scope.ServiceProvider.GetRequiredService(); + var sites = scope.ServiceProvider.GetRequiredService(); + foreach (var site in sites.GetSites().ToList()) + { + if (!pages.GetPages(site.SiteId).ToList().Where(item => item.Path == "search").Any()) + { + sites.CreatePages(site, pageTemplates, null); + } + } + } } } diff --git a/Oqtane.Server/Managers/Search/ModuleSearchIndexManager.cs b/Oqtane.Server/Managers/Search/ModuleSearchIndexManager.cs index fec739d2..b68c993f 100644 --- a/Oqtane.Server/Managers/Search/ModuleSearchIndexManager.cs +++ b/Oqtane.Server/Managers/Search/ModuleSearchIndexManager.cs @@ -12,6 +12,8 @@ namespace Oqtane.Managers.Search { public class ModuleSearchIndexManager : SearchIndexManagerBase { + public const int ModuleSearchIndexManagerPriority = 200; + private readonly IServiceProvider _serviceProvider; private readonly ILogger _logger; private readonly IPageModuleRepository _pageModuleRepostory; @@ -30,35 +32,41 @@ namespace Oqtane.Managers.Search _pageRepository = pageRepository; } - public override string Name => Constants.ModuleSearchIndexManagerName; + public override string Name => EntityNames.Module; - public override int Priority => Constants.ModuleSearchIndexManagerPriority; + public override int Priority => ModuleSearchIndexManagerPriority; - public override int IndexDocuments(int siteId, DateTime? startTime, Action> processSearchDocuments, Action handleError) + public override int IndexContent(int siteId, DateTime? startTime, Action> processSearchContent, Action handleError) { var pageModules = _pageModuleRepostory.GetPageModules(siteId).DistinctBy(i => i.ModuleId); - var searchDocuments = new List(); + var searchContentList = new List(); foreach(var pageModule in pageModules) { + var page = _pageRepository.GetPage(pageModule.PageId); + if(page == null || SearchUtils.IsSystemPage(page)) + { + continue; + } + var module = pageModule.Module; if (module.ModuleDefinition.ServerManagerType != "") { _logger.LogDebug($"Search: Begin index module {module.ModuleId}."); var type = Type.GetType(module.ModuleDefinition.ServerManagerType); - if (type?.GetInterface("IModuleSearch") != null) + if (type?.GetInterface(nameof(ISearchable)) != null) { try { - var moduleSearch = (IModuleSearch)ActivatorUtilities.CreateInstance(_serviceProvider, type); - var documents = moduleSearch.GetSearchDocuments(module, startTime.GetValueOrDefault(DateTime.MinValue)); - if(documents != null) + var moduleSearch = (ISearchable)ActivatorUtilities.CreateInstance(_serviceProvider, type); + var contentList = moduleSearch.GetSearchContentList(module, startTime.GetValueOrDefault(DateTime.MinValue)); + if(contentList != null) { - foreach(var document in documents) + foreach(var searchContent in contentList) { - SaveModuleMetaData(document, pageModule); + SaveModuleMetaData(searchContent, pageModule); - searchDocuments.Add(document); + searchContentList.Add(searchContent); } } @@ -73,54 +81,65 @@ namespace Oqtane.Managers.Search } } - processSearchDocuments(searchDocuments); + processSearchContent(searchContentList); - return searchDocuments.Count; + return searchContentList.Count; } - private void SaveModuleMetaData(SearchDocument document, PageModule pageModule) + private void SaveModuleMetaData(SearchContent searchContent, PageModule pageModule) { - - document.EntryId = pageModule.ModuleId; - document.IndexerName = Name; - document.SiteId = pageModule.Module.SiteId; - document.LanguageCode = string.Empty; + searchContent.SiteId = pageModule.Module.SiteId; - if(document.ModifiedTime == DateTime.MinValue) + if(string.IsNullOrEmpty(searchContent.EntityName)) { - document.ModifiedTime = pageModule.ModifiedOn; + searchContent.EntityName = EntityNames.Module; } - if (string.IsNullOrEmpty(document.AdditionalContent)) + if(searchContent.EntityId == 0) { - document.AdditionalContent = string.Empty; + searchContent.EntityId = pageModule.ModuleId; + } + + if (searchContent.IsActive) + { + searchContent.IsActive = !pageModule.Module.IsDeleted; + } + + if (searchContent.ModifiedTime == DateTime.MinValue) + { + searchContent.ModifiedTime = pageModule.ModifiedOn; + } + + if (string.IsNullOrEmpty(searchContent.AdditionalContent)) + { + searchContent.AdditionalContent = string.Empty; } var page = _pageRepository.GetPage(pageModule.PageId); - if (string.IsNullOrEmpty(document.Url) && page != null) + if (string.IsNullOrEmpty(searchContent.Url) && page != null) { - document.Url = page.Url; + searchContent.Url = $"{(!string.IsNullOrEmpty(page.Path) && !page.Path.StartsWith("/") ? "/" : "")}{page.Path}"; } - if (string.IsNullOrEmpty(document.Title) && page != null) + if (string.IsNullOrEmpty(searchContent.Title) && page != null) { - document.Title = !string.IsNullOrEmpty(page.Title) ? page.Title : page.Name; + searchContent.Title = !string.IsNullOrEmpty(page.Title) ? page.Title : page.Name; } - if (document.Properties == null) + if (searchContent.Properties == null) { - document.Properties = new List(); + searchContent.Properties = new List(); } - if(!document.Properties.Any(i => i.Name == Constants.SearchPageIdPropertyName)) + if(!searchContent.Properties.Any(i => i.Name == Constants.SearchPageIdPropertyName)) { - document.Properties.Add(new SearchDocumentProperty { Name = Constants.SearchPageIdPropertyName, Value = pageModule.PageId.ToString() }); + searchContent.Properties.Add(new SearchContentProperty { Name = Constants.SearchPageIdPropertyName, Value = pageModule.PageId.ToString() }); } - if (!document.Properties.Any(i => i.Name == Constants.SearchModuleIdPropertyName)) + if (!searchContent.Properties.Any(i => i.Name == Constants.SearchModuleIdPropertyName)) { - document.Properties.Add(new SearchDocumentProperty { Name = Constants.SearchModuleIdPropertyName, Value = pageModule.ModuleId.ToString() }); + searchContent.Properties.Add(new SearchContentProperty { Name = Constants.SearchModuleIdPropertyName, Value = pageModule.ModuleId.ToString() }); } } } diff --git a/Oqtane.Server/Managers/Search/ModuleSearchResultManager.cs b/Oqtane.Server/Managers/Search/ModuleSearchResultManager.cs index 55789c23..c40f5f0b 100644 --- a/Oqtane.Server/Managers/Search/ModuleSearchResultManager.cs +++ b/Oqtane.Server/Managers/Search/ModuleSearchResultManager.cs @@ -11,7 +11,7 @@ namespace Oqtane.Managers.Search { public class ModuleSearchResultManager : ISearchResultManager { - public string Name => Constants.ModuleSearchIndexManagerName; + public string Name => EntityNames.Module; private readonly IServiceProvider _serviceProvider; @@ -37,26 +37,17 @@ namespace Oqtane.Managers.Search return string.Empty; } - public bool Visible(SearchDocument searchResult, SearchQuery searchQuery) + public bool Visible(SearchContent searchResult, SearchQuery searchQuery) { var pageIdValue = searchResult.Properties?.FirstOrDefault(i => i.Name == Constants.SearchPageIdPropertyName)?.Value ?? string.Empty; - var moduleIdValue = searchResult.Properties?.FirstOrDefault(i => i.Name == Constants.SearchModuleIdPropertyName)?.Value ?? string.Empty; - if (!string.IsNullOrEmpty(pageIdValue) && int.TryParse(pageIdValue, out int pageId) - && !string.IsNullOrEmpty(moduleIdValue) && int.TryParse(moduleIdValue, out int moduleId)) + if (!string.IsNullOrEmpty(pageIdValue) && int.TryParse(pageIdValue, out int pageId)) { - return CanViewPage(pageId, searchQuery.User) && CanViewModule(moduleId, searchQuery.User); + return CanViewPage(pageId, searchQuery.User); } return false; } - private bool CanViewModule(int moduleId, User user) - { - var moduleRepository = _serviceProvider.GetRequiredService(); - var module = moduleRepository.GetModule(moduleId); - return module != null && !module.IsDeleted && UserSecurity.IsAuthorized(user, PermissionNames.View, module.PermissionList); - } - private bool CanViewPage(int pageId, User user) { var pageRepository = _serviceProvider.GetRequiredService(); diff --git a/Oqtane.Server/Managers/Search/PageSearchIndexManager.cs b/Oqtane.Server/Managers/Search/PageSearchIndexManager.cs index 68c4af02..49599804 100644 --- a/Oqtane.Server/Managers/Search/PageSearchIndexManager.cs +++ b/Oqtane.Server/Managers/Search/PageSearchIndexManager.cs @@ -14,6 +14,8 @@ namespace Oqtane.Managers.Search { public class PageSearchIndexManager : SearchIndexManagerBase { + private const int PageSearchIndexManagerPriority = 100; + private readonly IServiceProvider _serviceProvider; private readonly ILogger _logger; private readonly IPageRepository _pageRepository; @@ -29,50 +31,50 @@ namespace Oqtane.Managers.Search _pageRepository = pageRepository; } - public override string Name => Constants.PageSearchIndexManagerName; + public override string Name => EntityNames.Page; - public override int Priority => Constants.PageSearchIndexManagerPriority; + public override int Priority => PageSearchIndexManagerPriority; - public override int IndexDocuments(int siteId, DateTime? startTime, Action> processSearchDocuments, Action handleError) + public override int IndexContent(int siteId, DateTime? startTime, Action> processSearchContent, Action handleError) { var startTimeValue = startTime.GetValueOrDefault(DateTime.MinValue); var pages = _pageRepository.GetPages(siteId).Where(i => i.ModifiedOn >= startTimeValue); - var searchDocuments = new List(); + var searchContentList = new List(); foreach(var page in pages) { try { - if(IsSystemPage(page)) + if(SearchUtils.IsSystemPage(page)) { continue; } - var document = new SearchDocument + var searchContent = new SearchContent { - EntryId = page.PageId, - IndexerName = Name, + EntityName = EntityNames.Page, + EntityId = page.PageId, SiteId = page.SiteId, - LanguageCode = string.Empty, ModifiedTime = page.ModifiedOn, AdditionalContent = string.Empty, - Url = page.Url ?? string.Empty, + Url = $"{(!string.IsNullOrEmpty(page.Path) && !page.Path.StartsWith("/") ? "/" : "")}{page.Path}", Title = !string.IsNullOrEmpty(page.Title) ? page.Title : page.Name, Description = string.Empty, - Body = $"{page.Name} {page.Title}" + Body = $"{page.Name} {page.Title}", + IsActive = !page.IsDeleted && Utilities.IsPageModuleVisible(page.EffectiveDate, page.ExpiryDate) }; - if (document.Properties == null) + if (searchContent.Properties == null) { - document.Properties = new List(); + searchContent.Properties = new List(); } - if (!document.Properties.Any(i => i.Name == Constants.SearchPageIdPropertyName)) + if (!searchContent.Properties.Any(i => i.Name == Constants.SearchPageIdPropertyName)) { - document.Properties.Add(new SearchDocumentProperty { Name = Constants.SearchPageIdPropertyName, Value = page.PageId.ToString() }); + searchContent.Properties.Add(new SearchContentProperty { Name = Constants.SearchPageIdPropertyName, Value = page.PageId.ToString() }); } - searchDocuments.Add(document); + searchContentList.Add(searchContent); } catch(Exception ex) { @@ -81,14 +83,9 @@ namespace Oqtane.Managers.Search } } - processSearchDocuments(searchDocuments); + processSearchContent(searchContentList); - return searchDocuments.Count; - } - - private bool IsSystemPage(Models.Page page) - { - return page.Path.Contains("admin") || page.Path == "login" || page.Path == "register" || page.Path == "profile"; + return searchContentList.Count; } } } diff --git a/Oqtane.Server/Managers/Search/PageSearchResultManager.cs b/Oqtane.Server/Managers/Search/PageSearchResultManager.cs deleted file mode 100644 index fd5456a4..00000000 --- a/Oqtane.Server/Managers/Search/PageSearchResultManager.cs +++ /dev/null @@ -1,46 +0,0 @@ -using System; -using System.Linq; -using Microsoft.Extensions.DependencyInjection; -using Oqtane.Models; -using Oqtane.Repository; -using Oqtane.Security; -using Oqtane.Services; -using Oqtane.Shared; - -namespace Oqtane.Managers.Search -{ - public class PageSearchResultManager : ISearchResultManager - { - public string Name => Constants.PageSearchIndexManagerName; - - private readonly IServiceProvider _serviceProvider; - - public PageSearchResultManager( - IServiceProvider serviceProvider) - { - _serviceProvider = serviceProvider; - } - - public string GetUrl(SearchResult searchResult, SearchQuery searchQuery) - { - var pageRepository = _serviceProvider.GetRequiredService(); - var page = pageRepository.GetPage(searchResult.EntryId); - if (page != null) - { - return $"{searchQuery.Alias.Protocol}{searchQuery.Alias.Name}{(!string.IsNullOrEmpty(page.Path) && !page.Path.StartsWith("/") ? "/" : "")}{page.Path}"; - } - - return string.Empty; - } - - public bool Visible(SearchDocument searchResult, SearchQuery searchQuery) - { - var pageRepository = _serviceProvider.GetRequiredService(); - var page = pageRepository.GetPage(searchResult.EntryId); - - return page != null && !page.IsDeleted - && UserSecurity.IsAuthorized(searchQuery.User, PermissionNames.View, page.PermissionList) - && (Utilities.IsPageModuleVisible(page.EffectiveDate, page.ExpiryDate) || UserSecurity.IsAuthorized(searchQuery.User, PermissionNames.Edit, page.PermissionList)); - } - } -} diff --git a/Oqtane.Server/Managers/Search/SearchIndexManagerBase.cs b/Oqtane.Server/Managers/Search/SearchIndexManagerBase.cs index 0ecb2880..779a950d 100644 --- a/Oqtane.Server/Managers/Search/SearchIndexManagerBase.cs +++ b/Oqtane.Server/Managers/Search/SearchIndexManagerBase.cs @@ -10,6 +10,8 @@ namespace Oqtane.Managers.Search { public abstract class SearchIndexManagerBase : ISearchIndexManager { + private const string SearchIndexManagerEnabledSettingFormat = "SearchIndexManager_{0}_Enabled"; + private readonly IServiceProvider _serviceProvider; public SearchIndexManagerBase(IServiceProvider serviceProvider) @@ -21,11 +23,11 @@ namespace Oqtane.Managers.Search public abstract string Name { get; } - public abstract int IndexDocuments(int siteId, DateTime? startDate, Action> processSearchDocuments, Action handleError); + public abstract int IndexContent(int siteId, DateTime? startDate, Action> processSearchContent, Action handleError); public virtual bool IsIndexEnabled(int siteId) { - var settingName = string.Format(Constants.SearchIndexManagerEnabledSettingFormat, Name); + var settingName = string.Format(SearchIndexManagerEnabledSettingFormat, Name); var settingRepository = _serviceProvider.GetRequiredService(); var setting = settingRepository.GetSetting(EntityNames.Site, siteId, settingName); return setting == null || setting.SettingValue == "true"; diff --git a/Oqtane.Server/Migrations/EntityBuilders/SearchDocumentEntityBuilder.cs b/Oqtane.Server/Migrations/EntityBuilders/SearchContentEntityBuilder.cs similarity index 58% rename from Oqtane.Server/Migrations/EntityBuilders/SearchDocumentEntityBuilder.cs rename to Oqtane.Server/Migrations/EntityBuilders/SearchContentEntityBuilder.cs index 2f5b6d8d..4cf2aa4a 100644 --- a/Oqtane.Server/Migrations/EntityBuilders/SearchDocumentEntityBuilder.cs +++ b/Oqtane.Server/Migrations/EntityBuilders/SearchContentEntityBuilder.cs @@ -2,25 +2,26 @@ using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Migrations.Operations; using Microsoft.EntityFrameworkCore.Migrations.Operations.Builders; using Oqtane.Databases.Interfaces; +using Oqtane.Models; namespace Oqtane.Migrations.EntityBuilders { - public class SearchDocumentEntityBuilder : AuditableBaseEntityBuilder + public class SearchContentEntityBuilder : AuditableBaseEntityBuilder { - private const string _entityTableName = "SearchDocument"; - private readonly PrimaryKey _primaryKey = new("PK_SearchDocument", x => x.SearchDocumentId); + private const string _entityTableName = "SearchContent"; + private readonly PrimaryKey _primaryKey = new("PK_SearchContent", x => x.SearchContentId); - public SearchDocumentEntityBuilder(MigrationBuilder migrationBuilder, IDatabase database) : base(migrationBuilder, database) + public SearchContentEntityBuilder(MigrationBuilder migrationBuilder, IDatabase database) : base(migrationBuilder, database) { EntityTableName = _entityTableName; PrimaryKey = _primaryKey; } - protected override SearchDocumentEntityBuilder BuildTable(ColumnsBuilder table) + protected override SearchContentEntityBuilder BuildTable(ColumnsBuilder table) { - SearchDocumentId = AddAutoIncrementColumn(table, "SearchDocumentId"); - EntryId = AddIntegerColumn(table, "EntryId"); - IndexerName = AddStringColumn(table, "IndexerName", 50); + SearchContentId = AddAutoIncrementColumn(table, "SearchContentId"); + EntityName = AddStringColumn(table, "EntityName", 50); + EntityId = AddIntegerColumn(table, "EntityId"); SiteId = AddIntegerColumn(table, "SiteId"); Title = AddStringColumn(table, "Title", 255); Description = AddMaxStringColumn(table, "Description"); @@ -29,18 +30,17 @@ namespace Oqtane.Migrations.EntityBuilders ModifiedTime = AddDateTimeColumn(table, "ModifiedTime"); IsActive = AddBooleanColumn(table, "IsActive"); AdditionalContent = AddMaxStringColumn(table, "AdditionalContent"); - LanguageCode = AddStringColumn(table, "LanguageCode", 20); AddAuditableColumns(table); return this; } - public OperationBuilder SearchDocumentId { get; private set; } + public OperationBuilder SearchContentId { get; private set; } - public OperationBuilder EntryId { get; private set; } + public OperationBuilder EntityName { get; private set; } - public OperationBuilder IndexerName { get; private set; } + public OperationBuilder EntityId { get; private set; } public OperationBuilder SiteId { get; private set; } @@ -57,8 +57,5 @@ namespace Oqtane.Migrations.EntityBuilders public OperationBuilder IsActive { get; private set; } public OperationBuilder AdditionalContent { get; private set; } - - public OperationBuilder LanguageCode { get; private set; } - } } diff --git a/Oqtane.Server/Migrations/EntityBuilders/SearchContentPropertyEntityBuilder.cs b/Oqtane.Server/Migrations/EntityBuilders/SearchContentPropertyEntityBuilder.cs new file mode 100644 index 00000000..01870cd1 --- /dev/null +++ b/Oqtane.Server/Migrations/EntityBuilders/SearchContentPropertyEntityBuilder.cs @@ -0,0 +1,40 @@ +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Migrations.Operations; +using Microsoft.EntityFrameworkCore.Migrations.Operations.Builders; +using Oqtane.Databases.Interfaces; + +namespace Oqtane.Migrations.EntityBuilders +{ + public class SearchContentPropertyEntityBuilder : BaseEntityBuilder + { + private const string _entityTableName = "SearchContentProperty"; + private readonly PrimaryKey _primaryKey = new("PK_SearchContentProperty", x => x.PropertyId); + private readonly ForeignKey _searchContentForeignKey = new("FK_SearchContentProperty_SearchContent", x => x.SearchContentId, "SearchContent", "SearchContentId", ReferentialAction.Cascade); + + public SearchContentPropertyEntityBuilder(MigrationBuilder migrationBuilder, IDatabase database) : base(migrationBuilder, database) + { + EntityTableName = _entityTableName; + PrimaryKey = _primaryKey; + + ForeignKeys.Add(_searchContentForeignKey); + } + + protected override SearchContentPropertyEntityBuilder BuildTable(ColumnsBuilder table) + { + PropertyId = AddAutoIncrementColumn(table, "PropertyId"); + SearchContentId = AddIntegerColumn(table, "SearchContentId"); + Name = AddStringColumn(table, "Name", 50); + Value = AddStringColumn(table, "Value", 50); + + return this; + } + + public OperationBuilder PropertyId { get; private set; } + + public OperationBuilder SearchContentId { get; private set; } + + public OperationBuilder Name { get; private set; } + + public OperationBuilder Value { get; private set; } + } +} diff --git a/Oqtane.Server/Migrations/EntityBuilders/SearchContentWordCountEntityBuilder.cs b/Oqtane.Server/Migrations/EntityBuilders/SearchContentWordCountEntityBuilder.cs new file mode 100644 index 00000000..2eb845fc --- /dev/null +++ b/Oqtane.Server/Migrations/EntityBuilders/SearchContentWordCountEntityBuilder.cs @@ -0,0 +1,42 @@ +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Migrations.Operations; +using Microsoft.EntityFrameworkCore.Migrations.Operations.Builders; +using Oqtane.Databases.Interfaces; + +namespace Oqtane.Migrations.EntityBuilders +{ + public class SearchContentWordsEntityBuilder : BaseEntityBuilder + { + private const string _entityTableName = "SearchContentWords"; + private readonly PrimaryKey _primaryKey = new("PK_SearchContentWords", x => x.WordId); + private readonly ForeignKey _searchContentForeignKey = new("FK_SearchContentWords_SearchContent", x => x.SearchContentId, "SearchContent", "SearchContentId", ReferentialAction.Cascade); + private readonly ForeignKey _wordSourceForeignKey = new("FK_SearchContentWords_WordSource", x => x.WordSourceId, "SearchContentWordSource", "WordSourceId", ReferentialAction.Cascade); + + public SearchContentWordsEntityBuilder(MigrationBuilder migrationBuilder, IDatabase database) : base(migrationBuilder, database) + { + EntityTableName = _entityTableName; + PrimaryKey = _primaryKey; + + ForeignKeys.Add(_searchContentForeignKey); + ForeignKeys.Add(_wordSourceForeignKey); + } + + protected override SearchContentWordsEntityBuilder BuildTable(ColumnsBuilder table) + { + WordId = AddAutoIncrementColumn(table, "WordId"); + SearchContentId = AddIntegerColumn(table, "SearchContentId"); + WordSourceId = AddIntegerColumn(table, "WordSourceId"); + Count = AddIntegerColumn(table, "Count"); + + return this; + } + + public OperationBuilder WordId { get; private set; } + + public OperationBuilder SearchContentId { get; private set; } + + public OperationBuilder WordSourceId { get; private set; } + + public OperationBuilder Count { get; private set; } + } +} diff --git a/Oqtane.Server/Migrations/EntityBuilders/SearchContentWordSourceEntityBuilder.cs b/Oqtane.Server/Migrations/EntityBuilders/SearchContentWordSourceEntityBuilder.cs new file mode 100644 index 00000000..0787a3a4 --- /dev/null +++ b/Oqtane.Server/Migrations/EntityBuilders/SearchContentWordSourceEntityBuilder.cs @@ -0,0 +1,31 @@ +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Migrations.Operations; +using Microsoft.EntityFrameworkCore.Migrations.Operations.Builders; +using Oqtane.Databases.Interfaces; + +namespace Oqtane.Migrations.EntityBuilders +{ + public class SearchContentWordSourceEntityBuilder : BaseEntityBuilder + { + private const string _entityTableName = "SearchContentWordSource"; + private readonly PrimaryKey _primaryKey = new("PK_SearchContentWordSource", x => x.WordSourceId); + + public SearchContentWordSourceEntityBuilder(MigrationBuilder migrationBuilder, IDatabase database) : base(migrationBuilder, database) + { + EntityTableName = _entityTableName; + PrimaryKey = _primaryKey; + } + + protected override SearchContentWordSourceEntityBuilder BuildTable(ColumnsBuilder table) + { + WordSourceId = AddAutoIncrementColumn(table, "WordSourceId"); + Word = AddStringColumn(table, "Word", 255); + + return this; + } + + public OperationBuilder WordSourceId { get; private set; } + + public OperationBuilder Word { get; private set; } + } +} diff --git a/Oqtane.Server/Migrations/EntityBuilders/SearchDocumentPropertyEntityBuilder.cs b/Oqtane.Server/Migrations/EntityBuilders/SearchDocumentPropertyEntityBuilder.cs deleted file mode 100644 index a591aec2..00000000 --- a/Oqtane.Server/Migrations/EntityBuilders/SearchDocumentPropertyEntityBuilder.cs +++ /dev/null @@ -1,40 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Migrations.Operations; -using Microsoft.EntityFrameworkCore.Migrations.Operations.Builders; -using Oqtane.Databases.Interfaces; - -namespace Oqtane.Migrations.EntityBuilders -{ - public class SearchDocumentPropertyEntityBuilder : BaseEntityBuilder - { - private const string _entityTableName = "SearchDocumentProperty"; - private readonly PrimaryKey _primaryKey = new("PK_SearchDocumentProperty", x => x.PropertyId); - private readonly ForeignKey _searchDocumentForeignKey = new("FK_SearchDocumentProperty_SearchDocument", x => x.SearchDocumentId, "SearchDocument", "SearchDocumentId", ReferentialAction.Cascade); - - public SearchDocumentPropertyEntityBuilder(MigrationBuilder migrationBuilder, IDatabase database) : base(migrationBuilder, database) - { - EntityTableName = _entityTableName; - PrimaryKey = _primaryKey; - - ForeignKeys.Add(_searchDocumentForeignKey); - } - - protected override SearchDocumentPropertyEntityBuilder BuildTable(ColumnsBuilder table) - { - PropertyId = AddAutoIncrementColumn(table, "PropertyId"); - SearchDocumentId = AddIntegerColumn(table, "SearchDocumentId"); - Name = AddStringColumn(table, "Name", 50); - Value = AddStringColumn(table, "Value", 50); - - return this; - } - - public OperationBuilder PropertyId { get; private set; } - - public OperationBuilder SearchDocumentId { get; private set; } - - public OperationBuilder Name { get; private set; } - - public OperationBuilder Value { get; private set; } - } -} diff --git a/Oqtane.Server/Migrations/EntityBuilders/SearchDocumentTagEntityBuilder.cs b/Oqtane.Server/Migrations/EntityBuilders/SearchDocumentTagEntityBuilder.cs deleted file mode 100644 index bd864f22..00000000 --- a/Oqtane.Server/Migrations/EntityBuilders/SearchDocumentTagEntityBuilder.cs +++ /dev/null @@ -1,37 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Migrations.Operations; -using Microsoft.EntityFrameworkCore.Migrations.Operations.Builders; -using Oqtane.Databases.Interfaces; - -namespace Oqtane.Migrations.EntityBuilders -{ - public class SearchDocumentTagEntityBuilder : BaseEntityBuilder - { - private const string _entityTableName = "SearchDocumentTag"; - private readonly PrimaryKey _primaryKey = new("PK_SearchDocumentTag", x => x.TagId); - private readonly ForeignKey _searchDocumentForeignKey = new("FK_SearchDocumentTag_SearchDocument", x => x.SearchDocumentId, "SearchDocument", "SearchDocumentId", ReferentialAction.Cascade); - - public SearchDocumentTagEntityBuilder(MigrationBuilder migrationBuilder, IDatabase database) : base(migrationBuilder, database) - { - EntityTableName = _entityTableName; - PrimaryKey = _primaryKey; - - ForeignKeys.Add(_searchDocumentForeignKey); - } - - protected override SearchDocumentTagEntityBuilder BuildTable(ColumnsBuilder table) - { - TagId = AddAutoIncrementColumn(table, "TagId"); - SearchDocumentId = AddIntegerColumn(table, "SearchDocumentId"); - Tag = AddStringColumn(table, "Tag", 50); - - return this; - } - - public OperationBuilder TagId { get; private set; } - - public OperationBuilder SearchDocumentId { get; private set; } - - public OperationBuilder Tag { get; private set; } - } -} diff --git a/Oqtane.Server/Migrations/Tenant/05020001_AddSearchTables.cs b/Oqtane.Server/Migrations/Tenant/05020001_AddSearchTables.cs index f32a8137..c66a4fe5 100644 --- a/Oqtane.Server/Migrations/Tenant/05020001_AddSearchTables.cs +++ b/Oqtane.Server/Migrations/Tenant/05020001_AddSearchTables.cs @@ -17,26 +17,34 @@ namespace Oqtane.Migrations.Tenant protected override void Up(MigrationBuilder migrationBuilder) { - var searchDocumentEntityBuilder = new SearchDocumentEntityBuilder(migrationBuilder, ActiveDatabase); - searchDocumentEntityBuilder.Create(); + var searchContentEntityBuilder = new SearchContentEntityBuilder(migrationBuilder, ActiveDatabase); + searchContentEntityBuilder.Create(); - var searchDocumentPropertyEntityBuilder = new SearchDocumentPropertyEntityBuilder(migrationBuilder, ActiveDatabase); - searchDocumentPropertyEntityBuilder.Create(); + var searchContentPropertyEntityBuilder = new SearchContentPropertyEntityBuilder(migrationBuilder, ActiveDatabase); + searchContentPropertyEntityBuilder.Create(); - var searchDocumentTagEntityBuilder = new SearchDocumentTagEntityBuilder(migrationBuilder, ActiveDatabase); - searchDocumentTagEntityBuilder.Create(); + var searchContentWordSourceEntityBuilder = new SearchContentWordSourceEntityBuilder(migrationBuilder, ActiveDatabase); + searchContentWordSourceEntityBuilder.Create(); + searchContentWordSourceEntityBuilder.AddIndex("IX_SearchContentWordSource", "Word", true); + + var searchContentWordsEntityBuilder = new SearchContentWordsEntityBuilder(migrationBuilder, ActiveDatabase); + searchContentWordsEntityBuilder.Create(); } protected override void Down(MigrationBuilder migrationBuilder) { - var searchDocumentPropertyEntityBuilder = new SearchDocumentPropertyEntityBuilder(migrationBuilder, ActiveDatabase); - searchDocumentPropertyEntityBuilder.Drop(); + var searchContentWordsEntityBuilder = new SearchContentWordsEntityBuilder(migrationBuilder, ActiveDatabase); + searchContentWordsEntityBuilder.Drop(); - var searchDocumentTagEntityBuilder = new SearchDocumentTagEntityBuilder(migrationBuilder, ActiveDatabase); - searchDocumentTagEntityBuilder.Drop(); + var searchContentWordSourceEntityBuilder = new SearchContentWordSourceEntityBuilder(migrationBuilder, ActiveDatabase); + searchContentWordSourceEntityBuilder.DropIndex("IX_SearchContentWordSource"); + searchContentWordSourceEntityBuilder.Drop(); - var searchDocumentEntityBuilder = new SearchDocumentEntityBuilder(migrationBuilder, ActiveDatabase); - searchDocumentEntityBuilder.Drop(); + var searchContentPropertyEntityBuilder = new SearchContentPropertyEntityBuilder(migrationBuilder, ActiveDatabase); + searchContentPropertyEntityBuilder.Drop(); + + var searchContentEntityBuilder = new SearchContentEntityBuilder(migrationBuilder, ActiveDatabase); + searchContentEntityBuilder.Drop(); } } } diff --git a/Oqtane.Server/Modules/HtmlText/Manager/HtmlTextManager.cs b/Oqtane.Server/Modules/HtmlText/Manager/HtmlTextManager.cs index 8cabf391..e5cfb8de 100644 --- a/Oqtane.Server/Modules/HtmlText/Manager/HtmlTextManager.cs +++ b/Oqtane.Server/Modules/HtmlText/Manager/HtmlTextManager.cs @@ -17,7 +17,7 @@ using System; namespace Oqtane.Modules.HtmlText.Manager { [PrivateApi("Mark HtmlText classes as private, since it's not very useful in the public docs")] - public class HtmlTextManager : MigratableModuleBase, IInstallable, IPortable, IModuleSearch + public class HtmlTextManager : MigratableModuleBase, IInstallable, IPortable, ISearchable { private readonly IServiceProvider _serviceProvider; private readonly IHtmlTextRepository _htmlText; @@ -48,25 +48,25 @@ namespace Oqtane.Modules.HtmlText.Manager return content; } - public IList GetSearchDocuments(Module module, DateTime startDate) + public IList GetSearchContentList(Module module, DateTime startDate) { - var searchDocuments = new List(); + var searchContentList = new List(); var htmltexts = _htmlText.GetHtmlTexts(module.ModuleId); if (htmltexts != null && htmltexts.Any(i => i.CreatedOn >= startDate)) { var htmltext = htmltexts.OrderByDescending(item => item.CreatedOn).First(); - searchDocuments.Add(new SearchDocument + searchContentList.Add(new SearchContent { Title = module.Title, Description = string.Empty, - Body = SearchUtils.Clean(htmltext.Content, true), + Body = htmltext.Content, ModifiedTime = htmltext.ModifiedOn }); } - return searchDocuments; + return searchContentList; } public void ImportModule(Module module, string content, string version) diff --git a/Oqtane.Server/Modules/SearchResults/Services/SearchResultsService.cs b/Oqtane.Server/Modules/SearchResults/Services/SearchResultsService.cs deleted file mode 100644 index 3823bac6..00000000 --- a/Oqtane.Server/Modules/SearchResults/Services/SearchResultsService.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System.Threading.Tasks; -using Microsoft.AspNetCore.Http; -using Oqtane.Documentation; -using Oqtane.Infrastructure; -using Oqtane.Models; -using Oqtane.Services; - -namespace Oqtane.Modules.SearchResults.Services -{ - [PrivateApi("Mark SearchResults classes as private, since it's not very useful in the public docs")] - public class ServerSearchResultsService : ISearchResultsService, ITransientService - { - private readonly ILogManager _logger; - private readonly IHttpContextAccessor _accessor; - private readonly Alias _alias; - private readonly ISearchService _searchService; - - public ServerSearchResultsService( - ITenantManager tenantManager, - ILogManager logger, - IHttpContextAccessor accessor, - ISearchService searchService) - { - _logger = logger; - _accessor = accessor; - _alias = tenantManager.GetAlias(); - _searchService = searchService; - } - - public async Task SearchAsync(int moduleId, SearchQuery searchQuery) - { - var results = await _searchService.SearchAsync(searchQuery); - return results; - } - } -} diff --git a/Oqtane.Server/Modules/SearchResults/Startup/ServerStartup.cs b/Oqtane.Server/Modules/SearchResults/Startup/ServerStartup.cs deleted file mode 100644 index 25d2d15f..00000000 --- a/Oqtane.Server/Modules/SearchResults/Startup/ServerStartup.cs +++ /dev/null @@ -1,24 +0,0 @@ -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.DependencyInjection; -using Oqtane.Infrastructure; -using Oqtane.Modules.SearchResults.Services; - -namespace Oqtane.Modules.SearchResults.Startup -{ - public class ServerStartup : IServerStartup - { - public void Configure(IApplicationBuilder app, IWebHostEnvironment env) - { - } - - public void ConfigureMvc(IMvcBuilder mvcBuilder) - { - } - - public void ConfigureServices(IServiceCollection services) - { - services.AddTransient(); - } - } -} diff --git a/Oqtane.Server/Oqtane.Server.csproj b/Oqtane.Server/Oqtane.Server.csproj index f661e715..52776903 100644 --- a/Oqtane.Server/Oqtane.Server.csproj +++ b/Oqtane.Server/Oqtane.Server.csproj @@ -33,6 +33,7 @@ + diff --git a/Oqtane.Server/Providers/DatabaseSearchProvider.cs b/Oqtane.Server/Providers/DatabaseSearchProvider.cs index ac2ed576..74884934 100644 --- a/Oqtane.Server/Providers/DatabaseSearchProvider.cs +++ b/Oqtane.Server/Providers/DatabaseSearchProvider.cs @@ -1,8 +1,11 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Net; using System.Text.RegularExpressions; using System.Threading.Tasks; +using System.Xml; +using HtmlAgilityPack; using Oqtane.Models; using Oqtane.Repository; using Oqtane.Services; @@ -13,27 +16,28 @@ namespace Oqtane.Providers { public class DatabaseSearchProvider : ISearchProvider { - private readonly ISearchDocumentRepository _searchDocumentRepository; + private readonly ISearchContentRepository _searchContentRepository; private const float TitleBoost = 100f; private const float DescriptionBoost = 10f; private const float BodyBoost = 10f; private const float AdditionalContentBoost = 5f; - + private const string IgnoreWords = "the,be,to,of,and,a,i,in,that,have,it,for,not,on,with,he,as,you,do,at,this,but,his,by,from,they,we,say,her,she,or,an,will,my,one,all,would,there,their,what,so,up,out,if,about,who,get,which,go,me,when,make,can,like,time,no,just,him,know,take,people,into,year,your,good,some,could,them,see,other,than,then,now,look,only,come,its,over,think,also,back,after,use,two,how,our,work,first,well,way,even,new,want,because,any,these,give,day,most,us"; + private const int WordMinLength = 3; public string Name => Constants.DefaultSearchProviderName; - public DatabaseSearchProvider(ISearchDocumentRepository searchDocumentRepository) + public DatabaseSearchProvider(ISearchContentRepository searchContentRepository) { - _searchDocumentRepository = searchDocumentRepository; + _searchContentRepository = searchContentRepository; } public void Commit() { } - public void DeleteDocument(string id) + public void DeleteSearchContent(string id) { - _searchDocumentRepository.DeleteSearchDocument(id); + _searchContentRepository.DeleteSearchContent(id); } public bool Optimize() @@ -43,25 +47,28 @@ namespace Oqtane.Providers public void ResetIndex() { - _searchDocumentRepository.DeleteAllSearchDocuments(); + _searchContentRepository.DeleteAllSearchContent(); } - public void SaveDocument(SearchDocument document, bool autoCommit = false) + public void SaveSearchContent(SearchContent searchContent, bool autoCommit = false) { //remove exist document - _searchDocumentRepository.DeleteSearchDocument(document.IndexerName, document.EntryId); + _searchContentRepository.DeleteSearchContent(searchContent.EntityName, searchContent.EntityId); - _searchDocumentRepository.AddSearchDocument(document); + _searchContentRepository.AddSearchContent(searchContent); + + //save the index words + AnalyzeSearchContent(searchContent); } - public async Task SearchAsync(SearchQuery searchQuery, Func validateFunc) + public async Task SearchAsync(SearchQuery searchQuery, Func validateFunc) { var totalResults = 0; - var documents = await _searchDocumentRepository.GetSearchDocumentsAsync(searchQuery); + var searchContentList = await _searchContentRepository.GetSearchContentListAsync(searchQuery); - //convert the search documents to search results. - var results = documents + //convert the search content to search results. + var results = searchContentList .Where(i => validateFunc(i, searchQuery)) .Select(i => ConvertToSearchResult(i, searchQuery)); @@ -99,7 +106,7 @@ namespace Oqtane.Providers //remove duplicated results based on page id for Page and Module types results = results.DistinctBy(i => { - if (i.IndexerName == Constants.PageSearchIndexManagerName || i.IndexerName == Constants.ModuleSearchIndexManagerName) + if (i.EntityName == EntityNames.Page || i.EntityName == EntityNames.Module) { var pageId = i.Properties.FirstOrDefault(p => p.Name == Constants.SearchPageIdPropertyName)?.Value ?? string.Empty; return !string.IsNullOrEmpty(pageId) ? pageId : i.UniqueKey; @@ -119,45 +126,44 @@ namespace Oqtane.Providers }; } - private SearchResult ConvertToSearchResult(SearchDocument searchDocument, SearchQuery searchQuery) + private SearchResult ConvertToSearchResult(SearchContent searchContent, SearchQuery searchQuery) { var searchResult = new SearchResult() { - SearchDocumentId = searchDocument.SearchDocumentId, - SiteId = searchDocument.SiteId, - IndexerName = searchDocument.IndexerName, - EntryId = searchDocument.EntryId, - Title = searchDocument.Title, - Description = searchDocument.Description, - Body = searchDocument.Body, - Url = searchDocument.Url, - ModifiedTime = searchDocument.ModifiedTime, - Tags = searchDocument.Tags, - Properties = searchDocument.Properties, - Snippet = BuildSnippet(searchDocument, searchQuery), - Score = CalculateScore(searchDocument, searchQuery) + SearchContentId = searchContent.SearchContentId, + SiteId = searchContent.SiteId, + EntityName = searchContent.EntityName, + EntityId = searchContent.EntityId, + Title = searchContent.Title, + Description = searchContent.Description, + Body = searchContent.Body, + Url = searchContent.Url, + ModifiedTime = searchContent.ModifiedTime, + Properties = searchContent.Properties, + Snippet = BuildSnippet(searchContent, searchQuery), + Score = CalculateScore(searchContent, searchQuery) }; return searchResult; } - private float CalculateScore(SearchDocument searchDocument, SearchQuery searchQuery) + private float CalculateScore(SearchContent searchContent, SearchQuery searchQuery) { var score = 0f; foreach (var keyword in SearchUtils.GetKeywordsList(searchQuery.Keywords)) { - score += Regex.Matches(searchDocument.Title, keyword, RegexOptions.IgnoreCase).Count * TitleBoost; - score += Regex.Matches(searchDocument.Description, keyword, RegexOptions.IgnoreCase).Count * DescriptionBoost; - score += Regex.Matches(searchDocument.Body, keyword, RegexOptions.IgnoreCase).Count * BodyBoost; - score += Regex.Matches(searchDocument.AdditionalContent, keyword, RegexOptions.IgnoreCase).Count * AdditionalContentBoost; + score += Regex.Matches(searchContent.Title, keyword, RegexOptions.IgnoreCase).Count * TitleBoost; + score += Regex.Matches(searchContent.Description, keyword, RegexOptions.IgnoreCase).Count * DescriptionBoost; + score += Regex.Matches(searchContent.Body, keyword, RegexOptions.IgnoreCase).Count * BodyBoost; + score += Regex.Matches(searchContent.AdditionalContent, keyword, RegexOptions.IgnoreCase).Count * AdditionalContentBoost; } return score / 100; } - private string BuildSnippet(SearchDocument searchDocument, SearchQuery searchQuery) + private string BuildSnippet(SearchContent searchContent, SearchQuery searchQuery) { - var content = $"{searchDocument.Title} {searchDocument.Description} {searchDocument.Body}"; + var content = $"{searchContent.Title} {searchContent.Description} {searchContent.Body}"; var snippet = string.Empty; foreach (var keyword in SearchUtils.GetKeywordsList(searchQuery.Keywords)) { @@ -198,5 +204,92 @@ namespace Oqtane.Providers return snippet; } + + private void AnalyzeSearchContent(SearchContent searchContent) + { + //analyze the search content and save the index words + var indexContent = $"{searchContent.Title} {searchContent.Description} {searchContent.Body} {searchContent.AdditionalContent}"; + var words = GetWords(indexContent, WordMinLength); + var existWords = _searchContentRepository.GetWords(searchContent.SearchContentId); + foreach (var kvp in words) + { + var word = existWords.FirstOrDefault(i => i.WordSource.Word == kvp.Key); + if (word != null) + { + word.Count = kvp.Value; + _searchContentRepository.UpdateSearchContentWords(word); + } + else + { + var wordSource = _searchContentRepository.GetSearchContentWordSource(kvp.Key); + if (wordSource == null) + { + wordSource = _searchContentRepository.AddSearchContentWordSource(new SearchContentWordSource { Word = kvp.Key }); + } + + word = new SearchContentWords + { + SearchContentId = searchContent.SearchContentId, + WordSourceId = wordSource.WordSourceId, + Count = kvp.Value + }; + + _searchContentRepository.AddSearchContentWords(word); + } + } + } + + private static Dictionary GetWords(string content, int minLength) + { + content = WebUtility.HtmlDecode(content); + + var words = new Dictionary(); + var ignoreWords = IgnoreWords.Split(','); + + var page = new HtmlDocument(); + page.LoadHtml(content); + + var phrases = page.DocumentNode.Descendants().Where(i => + i.NodeType == HtmlNodeType.Text && + i.ParentNode.Name != "script" && + i.ParentNode.Name != "style" && + !string.IsNullOrEmpty(i.InnerText.Trim()) + ).Select(i => FormatText(i.InnerText)); + + foreach (var phrase in phrases) + { + if (!string.IsNullOrEmpty(phrase)) + { + foreach (var word in phrase.Split(' ')) + { + if (word.Length >= minLength && !ignoreWords.Contains(word)) + { + if (!words.ContainsKey(word)) + { + words.Add(word, 1); + } + else + { + words[word] += 1; + } + } + } + } + } + + return words; + } + + private static string FormatText(string text) + { + text = HtmlEntity.DeEntitize(text); + foreach (var punctuation in ".?!,;:-_()[]{}'\"/\\".ToCharArray()) + { + text = text.Replace(punctuation, ' '); + } + text = text.Replace(" ", " ").ToLower().Trim(); + return text; + + } } } diff --git a/Oqtane.Server/Repository/Context/TenantDBContext.cs b/Oqtane.Server/Repository/Context/TenantDBContext.cs index cbe57631..cb0dcb28 100644 --- a/Oqtane.Server/Repository/Context/TenantDBContext.cs +++ b/Oqtane.Server/Repository/Context/TenantDBContext.cs @@ -29,8 +29,9 @@ namespace Oqtane.Repository public virtual DbSet Language { get; set; } public virtual DbSet Visitor { get; set; } public virtual DbSet UrlMapping { get; set; } - public virtual DbSet SearchDocument { get; set; } - public virtual DbSet SearchDocumentProperty { get; set; } - public virtual DbSet SearchDocumentTag { get; set; } + public virtual DbSet SearchContent { get; set; } + public virtual DbSet SearchContentProperty { get; set; } + public virtual DbSet SearchContentWords { get; set; } + public virtual DbSet SearchContentWordSource { get; set; } } } diff --git a/Oqtane.Server/Repository/Interfaces/ISearchContentRepository.cs b/Oqtane.Server/Repository/Interfaces/ISearchContentRepository.cs new file mode 100644 index 00000000..868265b0 --- /dev/null +++ b/Oqtane.Server/Repository/Interfaces/ISearchContentRepository.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Oqtane.Models; + +namespace Oqtane.Repository +{ + public interface ISearchContentRepository + { + Task> GetSearchContentListAsync(SearchQuery searchQuery); + SearchContent AddSearchContent(SearchContent searchContent); + void DeleteSearchContent(int searchContentId); + void DeleteSearchContent(string entityName, int entryId); + void DeleteSearchContent(string uniqueKey); + void DeleteAllSearchContent(); + + SearchContentWordSource GetSearchContentWordSource(string word); + SearchContentWordSource AddSearchContentWordSource(SearchContentWordSource wordSource); + + IEnumerable GetWords(int searchContentId); + SearchContentWords AddSearchContentWords(SearchContentWords word); + SearchContentWords UpdateSearchContentWords(SearchContentWords word); + } +} diff --git a/Oqtane.Server/Repository/Interfaces/ISearchDocumentRepository.cs b/Oqtane.Server/Repository/Interfaces/ISearchDocumentRepository.cs deleted file mode 100644 index 6128c40c..00000000 --- a/Oqtane.Server/Repository/Interfaces/ISearchDocumentRepository.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using Oqtane.Models; - -namespace Oqtane.Repository -{ - public interface ISearchDocumentRepository - { - Task> GetSearchDocumentsAsync(SearchQuery searchQuery); - SearchDocument AddSearchDocument(SearchDocument searchDocument); - void DeleteSearchDocument(int searchDocumentId); - void DeleteSearchDocument(string indexerName, int entryId); - void DeleteSearchDocument(string uniqueKey); - void DeleteAllSearchDocuments(); - } -} diff --git a/Oqtane.Server/Repository/SearchContentRepository.cs b/Oqtane.Server/Repository/SearchContentRepository.cs new file mode 100644 index 00000000..c1577d81 --- /dev/null +++ b/Oqtane.Server/Repository/SearchContentRepository.cs @@ -0,0 +1,167 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using Oqtane.Models; +using Oqtane.Shared; + +namespace Oqtane.Repository +{ + public class SearchContentRepository : ISearchContentRepository + { + private readonly IDbContextFactory _dbContextFactory; + + public SearchContentRepository(IDbContextFactory dbContextFactory) + { + _dbContextFactory = dbContextFactory; + } + + public async Task> GetSearchContentListAsync(SearchQuery searchQuery) + { + using var db = _dbContextFactory.CreateDbContext(); + var searchContentList = db.SearchContent.AsNoTracking() + .Include(i => i.Properties) + .Where(i => i.SiteId == searchQuery.SiteId && i.IsActive); + + if (searchQuery.EntityNames != null && searchQuery.EntityNames.Any()) + { + searchContentList = searchContentList.Where(i => searchQuery.EntityNames.Contains(i.EntityName)); + } + + if (searchQuery.BeginModifiedTimeUtc != DateTime.MinValue) + { + searchContentList = searchContentList.Where(i => i.ModifiedTime >= searchQuery.BeginModifiedTimeUtc); + } + + if (searchQuery.EndModifiedTimeUtc != DateTime.MinValue) + { + searchContentList = searchContentList.Where(i => i.ModifiedTime <= searchQuery.EndModifiedTimeUtc); + } + + if (searchQuery.Properties != null && searchQuery.Properties.Any()) + { + foreach (var property in searchQuery.Properties) + { + searchContentList = searchContentList.Where(i => i.Properties.Any(p => p.Name == property.Key && p.Value == property.Value)); + } + } + + var filteredContentList = new List(); + if (!string.IsNullOrEmpty(searchQuery.Keywords)) + { + foreach (var keyword in SearchUtils.GetKeywordsList(searchQuery.Keywords)) + { + filteredContentList.AddRange(await searchContentList.Where(i => i.Words.Any(w => w.WordSource.Word.StartsWith(keyword))).ToListAsync()); + } + } + + return filteredContentList.DistinctBy(i => i.UniqueKey); + } + + public SearchContent AddSearchContent(SearchContent searchContent) + { + using var context = _dbContextFactory.CreateDbContext(); + context.SearchContent.Add(searchContent); + + if(searchContent.Properties != null && searchContent.Properties.Any()) + { + foreach(var property in searchContent.Properties) + { + property.SearchContentId = searchContent.SearchContentId; + context.SearchContentProperty.Add(property); + } + } + + context.SaveChanges(); + + return searchContent; + } + + public void DeleteSearchContent(int searchContentId) + { + using var db = _dbContextFactory.CreateDbContext(); + var searchContent = db.SearchContent.Find(searchContentId); + db.SearchContent.Remove(searchContent); + db.SaveChanges(); + } + + public void DeleteSearchContent(string entityName, int entryId) + { + using var db = _dbContextFactory.CreateDbContext(); + var searchContent = db.SearchContent.FirstOrDefault(i => i.EntityName == entityName && i.EntityId == entryId); + if(searchContent != null) + { + db.SearchContent.Remove(searchContent); + db.SaveChanges(); + } + } + + public void DeleteSearchContent(string uniqueKey) + { + using var db = _dbContextFactory.CreateDbContext(); + var searchContent = db.SearchContent.FirstOrDefault(i => (i.EntityName + ":" + i.EntityId) == uniqueKey); + if (searchContent != null) + { + db.SearchContent.Remove(searchContent); + db.SaveChanges(); + } + } + + public void DeleteAllSearchContent() + { + using var db = _dbContextFactory.CreateDbContext(); + db.SearchContent.RemoveRange(db.SearchContent); + db.SaveChanges(); + } + + public SearchContentWordSource GetSearchContentWordSource(string word) + { + if(string.IsNullOrEmpty(word)) + { + return null; + } + + using var db = _dbContextFactory.CreateDbContext(); + return db.SearchContentWordSource.FirstOrDefault(i => i.Word == word); + } + + public SearchContentWordSource AddSearchContentWordSource(SearchContentWordSource wordSource) + { + using var db = _dbContextFactory.CreateDbContext(); + + db.SearchContentWordSource.Add(wordSource); + db.SaveChanges(); + + return wordSource; + } + + public IEnumerable GetWords(int searchContentId) + { + using var db = _dbContextFactory.CreateDbContext(); + return db.SearchContentWords + .Include(i => i.WordSource) + .Where(i => i.SearchContentId == searchContentId).ToList(); + } + + public SearchContentWords AddSearchContentWords(SearchContentWords word) + { + using var db = _dbContextFactory.CreateDbContext(); + + db.SearchContentWords.Add(word); + db.SaveChanges(); + + return word; + } + + public SearchContentWords UpdateSearchContentWords(SearchContentWords word) + { + using var db = _dbContextFactory.CreateDbContext(); + + db.Entry(word).State = EntityState.Modified; + db.SaveChanges(); + + return word; + } + } +} diff --git a/Oqtane.Server/Repository/SearchDocumentRepository.cs b/Oqtane.Server/Repository/SearchDocumentRepository.cs deleted file mode 100644 index bdceb52e..00000000 --- a/Oqtane.Server/Repository/SearchDocumentRepository.cs +++ /dev/null @@ -1,136 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.EntityFrameworkCore; -using Oqtane.Models; -using Oqtane.Shared; - -namespace Oqtane.Repository -{ - public class SearchDocumentRepository : ISearchDocumentRepository - { - private readonly IDbContextFactory _dbContextFactory; - - public SearchDocumentRepository(IDbContextFactory dbContextFactory) - { - _dbContextFactory = dbContextFactory; - } - - public async Task> GetSearchDocumentsAsync(SearchQuery searchQuery) - { - using var db = _dbContextFactory.CreateDbContext(); - var documents = db.SearchDocument.AsNoTracking() - .Include(i => i.Properties) - .Include(i => i.Tags) - .Where(i => i.SiteId == searchQuery.SiteId); - - if (searchQuery.Sources != null && searchQuery.Sources.Any()) - { - documents = documents.Where(i => searchQuery.Sources.Contains(i.IndexerName)); - } - - if (searchQuery.BeginModifiedTimeUtc != DateTime.MinValue) - { - documents = documents.Where(i => i.ModifiedTime >= searchQuery.BeginModifiedTimeUtc); - } - - if (searchQuery.EndModifiedTimeUtc != DateTime.MinValue) - { - documents = documents.Where(i => i.ModifiedTime <= searchQuery.EndModifiedTimeUtc); - } - - if (searchQuery.Tags != null && searchQuery.Tags.Any()) - { - foreach (var tag in searchQuery.Tags) - { - documents = documents.Where(i => i.Tags.Any(t => t.Tag == tag)); - } - } - - if (searchQuery.Properties != null && searchQuery.Properties.Any()) - { - foreach (var property in searchQuery.Properties) - { - documents = documents.Where(i => i.Properties.Any(p => p.Name == property.Key && p.Value == property.Value)); - } - } - - var filteredDocuments = new List(); - if (!string.IsNullOrEmpty(searchQuery.Keywords)) - { - foreach (var keyword in SearchUtils.GetKeywordsList(searchQuery.Keywords)) - { - filteredDocuments.AddRange(await documents.Where(i => i.Title.Contains(keyword) || i.Description.Contains(keyword) || i.Body.Contains(keyword)).ToListAsync()); - } - } - - return filteredDocuments.DistinctBy(i => i.UniqueKey); - } - - public SearchDocument AddSearchDocument(SearchDocument searchDocument) - { - using var context = _dbContextFactory.CreateDbContext(); - context.SearchDocument.Add(searchDocument); - - if(searchDocument.Properties != null && searchDocument.Properties.Any()) - { - foreach(var property in searchDocument.Properties) - { - property.SearchDocumentId = searchDocument.SearchDocumentId; - context.SearchDocumentProperty.Add(property); - } - } - - if (searchDocument.Tags != null && searchDocument.Tags.Any()) - { - foreach (var tag in searchDocument.Tags) - { - tag.SearchDocumentId = searchDocument.SearchDocumentId; - context.SearchDocumentTag.Add(tag); - } - } - - context.SaveChanges(); - - return searchDocument; - } - - public void DeleteSearchDocument(int searchDocumentId) - { - using var db = _dbContextFactory.CreateDbContext(); - var searchDocument = db.SearchDocument.Find(searchDocumentId); - db.SearchDocument.Remove(searchDocument); - db.SaveChanges(); - } - - public void DeleteSearchDocument(string indexerName, int entryId) - { - using var db = _dbContextFactory.CreateDbContext(); - var searchDocument = db.SearchDocument.FirstOrDefault(i => i.IndexerName == indexerName && i.EntryId == entryId); - if(searchDocument != null) - { - db.SearchDocument.Remove(searchDocument); - db.SaveChanges(); - } - } - - public void DeleteSearchDocument(string uniqueKey) - { - using var db = _dbContextFactory.CreateDbContext(); - var searchDocument = db.SearchDocument.FirstOrDefault(i => (i.IndexerName + ":" + i.EntryId) == uniqueKey); - if (searchDocument != null) - { - db.SearchDocument.Remove(searchDocument); - db.SaveChanges(); - } - } - - public void DeleteAllSearchDocuments() - { - using var db = _dbContextFactory.CreateDbContext(); - db.SearchDocument.RemoveRange(db.SearchDocument); - db.SaveChanges(); - } - } -} diff --git a/Oqtane.Server/Services/SearchService.cs b/Oqtane.Server/Services/SearchService.cs index caa9fce6..8fd9aab0 100644 --- a/Oqtane.Server/Services/SearchService.cs +++ b/Oqtane.Server/Services/SearchService.cs @@ -1,23 +1,30 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Net; using System.Threading.Tasks; +using HtmlAgilityPack; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Oqtane.Infrastructure; using Oqtane.Models; using Oqtane.Repository; +using Oqtane.Security; using Oqtane.Shared; namespace Oqtane.Services { public class SearchService : ISearchService { + private const string SearchProviderSettingName = "SearchProvider"; + private const string SearchEnabledSettingName = "SearchEnabled"; + private readonly IServiceProvider _serviceProvider; private readonly ITenantManager _tenantManager; private readonly IAliasRepository _aliasRepository; private readonly ISettingRepository _settingRepository; + private readonly IPermissionRepository _permissionRepository; private readonly ILogger _logger; private readonly IMemoryCache _cache; @@ -26,12 +33,14 @@ namespace Oqtane.Services ITenantManager tenantManager, IAliasRepository aliasRepository, ISettingRepository settingRepository, + IPermissionRepository permissionRepository, ILogger logger, IMemoryCache cache) { _tenantManager = tenantManager; _aliasRepository = aliasRepository; _settingRepository = settingRepository; + _permissionRepository = permissionRepository; _serviceProvider = serviceProvider; _logger = logger; _cache = cache; @@ -68,8 +77,8 @@ namespace Oqtane.Services { _logger.LogDebug($"Search: Begin Index {searchIndexManager.Name}"); - var count = searchIndexManager.IndexDocuments(siteId, startTime, SaveIndexDocuments, handleError); - logNote($"Search: Indexer {searchIndexManager.Name} processed {count} documents.
"); + var count = searchIndexManager.IndexContent(siteId, startTime, SaveSearchContent, handleError); + logNote($"Search: Indexer {searchIndexManager.Name} processed {count} search content.
"); _logger.LogDebug($"Search: End Index {searchIndexManager.Name}"); } @@ -79,7 +88,7 @@ namespace Oqtane.Services public async Task SearchAsync(SearchQuery searchQuery) { var searchProvider = GetSearchProvider(searchQuery.SiteId); - var searchResults = await searchProvider.SearchAsync(searchQuery, HasViewPermission); + var searchResults = await searchProvider.SearchAsync(searchQuery, Visible); //generate the document url if it's not set. foreach (var result in searchResults.Results) @@ -108,7 +117,7 @@ namespace Oqtane.Services private string GetSearchProviderSetting(int siteId) { - var setting = _settingRepository.GetSetting(EntityNames.Site, siteId, Constants.SearchProviderSettingName); + var setting = _settingRepository.GetSetting(EntityNames.Site, siteId, SearchProviderSettingName); if(!string.IsNullOrEmpty(setting?.SettingValue)) { return setting.SettingValue; @@ -119,7 +128,7 @@ namespace Oqtane.Services private bool SearchEnabled(int siteId) { - var setting = _settingRepository.GetSetting(EntityNames.Site, siteId, Constants.SearchEnabledSettingName); + var setting = _settingRepository.GetSetting(EntityNames.Site, siteId, SearchEnabledSettingName); if (!string.IsNullOrEmpty(setting?.SettingValue)) { return bool.TryParse(setting.SettingValue, out bool enabled) && enabled; @@ -167,21 +176,22 @@ namespace Oqtane.Services return managers.ToList(); } - private void SaveIndexDocuments(IList searchDocuments) + private void SaveSearchContent(IList searchContentList) { - if(searchDocuments.Any()) + if(searchContentList.Any()) { - var searchProvider = GetSearchProvider(searchDocuments.First().SiteId); + var searchProvider = GetSearchProvider(searchContentList.First().SiteId); - foreach (var searchDocument in searchDocuments) + foreach (var searchContent in searchContentList) { try { - searchProvider.SaveDocument(searchDocument); + CleanSearchContent(searchContent); + searchProvider.SaveSearchContent(searchContent); } catch(Exception ex) { - _logger.LogError(ex, $"Search: Save search document {searchDocument.UniqueKey} failed."); + _logger.LogError(ex, $"Search: Save search content {searchContent.UniqueKey} failed."); } } @@ -190,19 +200,30 @@ namespace Oqtane.Services } } - private bool HasViewPermission(SearchDocument searchDocument, SearchQuery searchQuery) + private bool Visible(SearchContent searchContent, SearchQuery searchQuery) { - var searchResultManager = GetSearchResultManagers().FirstOrDefault(i => i.Name == searchDocument.IndexerName); + if(!HasViewPermission(searchQuery.SiteId, searchQuery.User, searchContent.EntityName, searchContent.EntityId)) + { + return false; + } + + var searchResultManager = GetSearchResultManagers().FirstOrDefault(i => i.Name == searchContent.EntityName); if (searchResultManager != null) { - return searchResultManager.Visible(searchDocument, searchQuery); + return searchResultManager.Visible(searchContent, searchQuery); } return true; } + private bool HasViewPermission(int siteId, User user, string entityName, int entityId) + { + var permissions = _permissionRepository.GetPermissions(siteId, entityName, entityId).ToList(); + return UserSecurity.IsAuthorized(user, PermissionNames.View, permissions); + } + private string GetDocumentUrl(SearchResult result, SearchQuery searchQuery) { - var searchResultManager = GetSearchResultManagers().FirstOrDefault(i => i.Name == result.IndexerName); + var searchResultManager = GetSearchResultManagers().FirstOrDefault(i => i.Name == result.EntityName); if(searchResultManager != null) { return searchResultManager.GetUrl(result, searchQuery); @@ -210,5 +231,35 @@ namespace Oqtane.Services return string.Empty; } + + private void CleanSearchContent(SearchContent searchContent) + { + searchContent.Title = GetCleanContent(searchContent.Title); + searchContent.Description = GetCleanContent(searchContent.Description); + searchContent.Body = GetCleanContent(searchContent.Body); + searchContent.AdditionalContent = GetCleanContent(searchContent.AdditionalContent); + } + + private string GetCleanContent(string content) + { + if(string.IsNullOrWhiteSpace(content)) + { + return string.Empty; + } + + content = WebUtility.HtmlDecode(content); + + var page = new HtmlDocument(); + page.LoadHtml(content); + + var phrases = page.DocumentNode.Descendants().Where(i => + i.NodeType == HtmlNodeType.Text && + i.ParentNode.Name != "script" && + i.ParentNode.Name != "style" && + !string.IsNullOrEmpty(i.InnerText.Trim()) + ).Select(i => i.InnerText); + + return string.Join(" ", phrases); + } } } diff --git a/Oqtane.Server/wwwroot/Modules/Oqtane.Modules.SearchResults/Module.css b/Oqtane.Server/wwwroot/Modules/Oqtane.Modules.Admin.SearchResults/Module.css similarity index 100% rename from Oqtane.Server/wwwroot/Modules/Oqtane.Modules.SearchResults/Module.css rename to Oqtane.Server/wwwroot/Modules/Oqtane.Modules.Admin.SearchResults/Module.css diff --git a/Oqtane.Shared/Interfaces/ISearchIndexManager.cs b/Oqtane.Shared/Interfaces/ISearchIndexManager.cs index 9a322ab5..0a4123f8 100644 --- a/Oqtane.Shared/Interfaces/ISearchIndexManager.cs +++ b/Oqtane.Shared/Interfaces/ISearchIndexManager.cs @@ -15,6 +15,6 @@ namespace Oqtane.Services bool IsIndexEnabled(int siteId); - int IndexDocuments(int siteId, DateTime? startTime, Action> processSearchDocuments, Action handleError); + int IndexContent(int siteId, DateTime? startTime, Action> processSearchContent, Action handleError); } } diff --git a/Oqtane.Shared/Interfaces/ISearchProvider.cs b/Oqtane.Shared/Interfaces/ISearchProvider.cs index e1e9fb16..cb075d3c 100644 --- a/Oqtane.Shared/Interfaces/ISearchProvider.cs +++ b/Oqtane.Shared/Interfaces/ISearchProvider.cs @@ -11,11 +11,11 @@ namespace Oqtane.Services { string Name { get; } - void SaveDocument(SearchDocument document, bool autoCommit = false); + void SaveSearchContent(SearchContent searchContent, bool autoCommit = false); - void DeleteDocument(string id); + void DeleteSearchContent(string id); - Task SearchAsync(SearchQuery searchQuery, Func validateFunc); + Task SearchAsync(SearchQuery searchQuery, Func validateFunc); bool Optimize(); diff --git a/Oqtane.Shared/Interfaces/ISearchResultManager.cs b/Oqtane.Shared/Interfaces/ISearchResultManager.cs index 2dc459b0..b6f6cd32 100644 --- a/Oqtane.Shared/Interfaces/ISearchResultManager.cs +++ b/Oqtane.Shared/Interfaces/ISearchResultManager.cs @@ -7,7 +7,7 @@ namespace Oqtane.Services { string Name { get; } - bool Visible(SearchDocument searchResult, SearchQuery searchQuery); + bool Visible(SearchContent searchResult, SearchQuery searchQuery); string GetUrl(SearchResult searchResult, SearchQuery searchQuery); } diff --git a/Oqtane.Shared/Interfaces/IModuleSearch.cs b/Oqtane.Shared/Interfaces/ISearchable.cs similarity index 58% rename from Oqtane.Shared/Interfaces/IModuleSearch.cs rename to Oqtane.Shared/Interfaces/ISearchable.cs index af94a300..c0a4c00e 100644 --- a/Oqtane.Shared/Interfaces/IModuleSearch.cs +++ b/Oqtane.Shared/Interfaces/ISearchable.cs @@ -7,8 +7,8 @@ using Oqtane.Models; namespace Oqtane.Interfaces { - public interface IModuleSearch + public interface ISearchable { - public IList GetSearchDocuments(Module module, DateTime startTime); + public IList GetSearchContentList(Module module, DateTime startTime); } } diff --git a/Oqtane.Shared/Models/SearchDocument.cs b/Oqtane.Shared/Models/SearchContent.cs similarity index 57% rename from Oqtane.Shared/Models/SearchDocument.cs rename to Oqtane.Shared/Models/SearchContent.cs index 67161b3b..36f4c664 100644 --- a/Oqtane.Shared/Models/SearchDocument.cs +++ b/Oqtane.Shared/Models/SearchContent.cs @@ -4,16 +4,16 @@ using System.ComponentModel.DataAnnotations.Schema; using System.Text.Json; namespace Oqtane.Models { - public class SearchDocument : ModelBase + public class SearchContent : ModelBase { - public int SearchDocumentId { get; set; } + public int SearchContentId { get; set; } [NotMapped] - public string UniqueKey => $"{IndexerName}:{EntryId}"; + public string UniqueKey => $"{EntityName}:{EntityId}"; - public int EntryId { get; set; } + public string EntityName { get; set; } - public string IndexerName { get; set; } + public int EntityId { get; set; } public int SiteId { get; set; } @@ -27,15 +27,13 @@ namespace Oqtane.Models public DateTime ModifiedTime { get; set; } - public bool IsActive { get; set; } + public bool IsActive { get; set; } = true; public string AdditionalContent { get; set; } - public string LanguageCode { get; set; } + public IList Properties { get; set; } - public IList Tags { get; set; } - - public IList Properties { get; set; } + public IList Words { get; set; } public override string ToString() { diff --git a/Oqtane.Shared/Models/SearchDocumentProperty.cs b/Oqtane.Shared/Models/SearchContentProperty.cs similarity index 74% rename from Oqtane.Shared/Models/SearchDocumentProperty.cs rename to Oqtane.Shared/Models/SearchContentProperty.cs index 4febae43..0c2b7f9b 100644 --- a/Oqtane.Shared/Models/SearchDocumentProperty.cs +++ b/Oqtane.Shared/Models/SearchContentProperty.cs @@ -3,12 +3,12 @@ using Microsoft.EntityFrameworkCore; namespace Oqtane.Models { - public class SearchDocumentProperty + public class SearchContentProperty { [Key] public int PropertyId { get; set; } - public int SearchDocumentId { get; set; } + public int SearchContentId { get; set; } public string Name { get; set; } diff --git a/Oqtane.Shared/Models/SearchContentWordSource.cs b/Oqtane.Shared/Models/SearchContentWordSource.cs new file mode 100644 index 00000000..ec814757 --- /dev/null +++ b/Oqtane.Shared/Models/SearchContentWordSource.cs @@ -0,0 +1,12 @@ +using System.ComponentModel.DataAnnotations; + +namespace Oqtane.Models +{ + public class SearchContentWordSource + { + [Key] + public int WordSourceId { get; set; } + + public string Word { get; set; } + } +} diff --git a/Oqtane.Shared/Models/SearchContentWords.cs b/Oqtane.Shared/Models/SearchContentWords.cs new file mode 100644 index 00000000..c7630815 --- /dev/null +++ b/Oqtane.Shared/Models/SearchContentWords.cs @@ -0,0 +1,19 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Oqtane.Models +{ + public class SearchContentWords + { + [Key] + public int WordId { get; set; } + + public int SearchContentId { get; set; } + + public int WordSourceId { get; set; } + + public int Count { get; set; } + + public SearchContentWordSource WordSource { get; set; } + } +} diff --git a/Oqtane.Shared/Models/SearchDocumentTag.cs b/Oqtane.Shared/Models/SearchDocumentTag.cs deleted file mode 100644 index ee929efc..00000000 --- a/Oqtane.Shared/Models/SearchDocumentTag.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System.ComponentModel.DataAnnotations; - -namespace Oqtane.Models -{ - public class SearchDocumentTag - { - [Key] - public int TagId { get; set; } - - public int SearchDocumentId { get; set; } - - public string Tag { get; set; } - } -} diff --git a/Oqtane.Shared/Models/SearchQuery.cs b/Oqtane.Shared/Models/SearchQuery.cs index f148252d..cf03e8c1 100644 --- a/Oqtane.Shared/Models/SearchQuery.cs +++ b/Oqtane.Shared/Models/SearchQuery.cs @@ -14,14 +14,12 @@ namespace Oqtane.Models public string Keywords { get; set; } - public IList Sources { get; set; } = new List(); + public IList EntityNames { get; set; } = new List(); public DateTime BeginModifiedTimeUtc { get; set; } public DateTime EndModifiedTimeUtc { get; set; } - public IList Tags { get; set; } = new List(); - public IDictionary Properties { get; set; } = new Dictionary(); public int PageIndex { get; set; } diff --git a/Oqtane.Shared/Models/SearchResult.cs b/Oqtane.Shared/Models/SearchResult.cs index d13cafa1..dd878d42 100644 --- a/Oqtane.Shared/Models/SearchResult.cs +++ b/Oqtane.Shared/Models/SearchResult.cs @@ -1,6 +1,6 @@ namespace Oqtane.Models { - public class SearchResult : SearchDocument + public class SearchResult : SearchContent { public float Score { get; set; } diff --git a/Oqtane.Shared/Shared/Constants.cs b/Oqtane.Shared/Shared/Constants.cs index f1a14cd4..979f0872 100644 --- a/Oqtane.Shared/Shared/Constants.cs +++ b/Oqtane.Shared/Shared/Constants.cs @@ -77,22 +77,10 @@ namespace Oqtane.Shared public static readonly string VisitorCookiePrefix = "APP_VISITOR_"; - public const string SearchIndexManagerEnabledSettingFormat = "SearchIndexManager_{0}_Enabled"; - public const string SearchIndexStartTimeSettingName = "SearchIndex_StartTime"; - public const string SearchResultManagersCacheName = "SearchResultManagers"; - public const int SearchDefaultPageSize = 10; + public const string DefaultSearchProviderName = "Database"; public const string SearchPageIdPropertyName = "PageId"; public const string SearchModuleIdPropertyName = "ModuleId"; - public const string DefaultSearchProviderName = "Database"; - public const string SearchProviderSettingName = "SearchProvider"; - public const string SearchEnabledSettingName = "SearchEnabled"; - - public const string ModuleSearchIndexManagerName = "Module"; - public const string PageSearchIndexManagerName = "Page"; - - public const int PageSearchIndexManagerPriority = 100; - public const int ModuleSearchIndexManagerPriority = 200; - + // Obsolete constants const string RoleObsoleteMessage = "Use the corresponding member from Oqtane.Shared.RoleNames"; diff --git a/Oqtane.Shared/Shared/SearchUtils.cs b/Oqtane.Shared/Shared/SearchUtils.cs index b7116d4a..745770a5 100644 --- a/Oqtane.Shared/Shared/SearchUtils.cs +++ b/Oqtane.Shared/Shared/SearchUtils.cs @@ -1,40 +1,14 @@ -using System.Collections; using System.Collections.Generic; -using System.Net; -using System.Text.RegularExpressions; namespace Oqtane.Shared { public sealed class SearchUtils { - private const string PunctuationMatch = "[~!#\\$%\\^&*\\(\\)-+=\\{\\[\\}\\]\\|;:\\x22'<,>\\.\\?\\\\\\t\\r\\v\\f\\n]"; - private static readonly Regex _stripWhiteSpaceRegex = new Regex("\\s+", RegexOptions.Compiled); - private static readonly Regex _stripTagsRegex = new Regex("<[^<>]*>", RegexOptions.Compiled); - private static readonly Regex _afterRegEx = new Regex(PunctuationMatch + "\\s", RegexOptions.Compiled); - private static readonly Regex _beforeRegEx = new Regex("\\s" + PunctuationMatch, RegexOptions.Compiled); + private static readonly IList _systemPages; - public static string Clean(string html, bool removePunctuation) + static SearchUtils() { - if (string.IsNullOrWhiteSpace(html)) - { - return string.Empty; - } - - if (html.Contains("<")) - { - html = WebUtility.HtmlDecode(html); - } - - html = StripTags(html, true); - html = WebUtility.HtmlDecode(html); - - if (removePunctuation) - { - html = StripPunctuation(html, true); - html = StripWhiteSpace(html, true); - } - - return html; + _systemPages = new List { "login", "register", "profile", "404", "search" }; } public static IList GetKeywordsList(string keywords) @@ -54,42 +28,9 @@ namespace Oqtane.Shared return keywordsList; } - private static string StripTags(string html, bool retainSpace) + public static bool IsSystemPage(Models.Page page) { - return _stripTagsRegex.Replace(html, retainSpace ? " " : string.Empty); - } - - private static string StripPunctuation(string html, bool retainSpace) - { - if (string.IsNullOrWhiteSpace(html)) - { - return string.Empty; - } - - string retHTML = html + " "; - - var repString = retainSpace ? " " : string.Empty; - while (_beforeRegEx.IsMatch(retHTML)) - { - retHTML = _beforeRegEx.Replace(retHTML, repString); - } - - while (_afterRegEx.IsMatch(retHTML)) - { - retHTML = _afterRegEx.Replace(retHTML, repString); - } - - return retHTML.Trim('"'); - } - - private static string StripWhiteSpace(string html, bool retainSpace) - { - if (string.IsNullOrWhiteSpace(html)) - { - return string.Empty; - } - - return _stripWhiteSpaceRegex.Replace(html, retainSpace ? " " : string.Empty); + return page.Path.Contains("admin") || _systemPages.Contains(page.Path); } } }