search modifications

This commit is contained in:
sbwalker 2024-07-12 10:33:17 -04:00
parent 90b0f04b3c
commit bb79b9ed74
9 changed files with 144 additions and 306 deletions

View File

@ -4,7 +4,6 @@ using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Oqtane.Documentation;
using Oqtane.Enums;
using Oqtane.Infrastructure;
using Oqtane.Services;
@ -28,7 +27,7 @@ namespace Oqtane.Controllers
{
try
{
return await _searchService.SearchAsync(searchQuery);
return await _searchService.GetSearchResultsAsync(searchQuery);
}
catch (Exception ex)
{

View File

@ -58,6 +58,8 @@ namespace Oqtane.Infrastructure
var currentTime = DateTime.UtcNow;
var lastIndexedOn = Convert.ToDateTime(siteSettings.GetValue(SearchLastIndexedOnSetting, DateTime.MinValue.ToString()));
log += $"Index Date: {lastIndexedOn}<br />";
var ignorePaths = siteSettings.GetValue(SearchIgnorePathsSetting, "").Split(',');
var ignoreEntities = siteSettings.GetValue(SearchIgnoreEntitiesSetting, "").Split(',');
@ -68,7 +70,7 @@ namespace Oqtane.Infrastructure
// index pages
foreach (var page in pages)
{
if (Constants.InternalPagePaths.Contains(page.Path) || ignorePaths.Contains(page.Path))
if (!string.IsNullOrEmpty(page.Path) && (Constants.InternalPagePaths.Contains(page.Path) || ignorePaths.Contains(page.Path)))
{
continue;
}
@ -169,9 +171,8 @@ namespace Oqtane.Infrastructure
}
}
// save search content
await searchService.SaveSearchContentAsync(searchContents, siteSettings);
log += $"Index Date: {lastIndexedOn}<br />";
// save search contents
log += await searchService.SaveSearchContentsAsync(searchContents, siteSettings);
log += $"Items Indexed: {searchContents.Count}<br />";
// update last indexed on

View File

@ -71,7 +71,7 @@ namespace Oqtane.SiteTemplates
Content = "<p>Copyright (c) 2018-2024 .NET Foundation</p>" +
"<p>Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:</p>" +
"<p>The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.</p>" +
"<p>THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.</p>"
"<p>THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.</p>"
},
new PageTemplateModule { ModuleDefinitionName = "Oqtane.Modules.HtmlText, Oqtane.Client", Title = "Secure Content", Pane = PaneNames.Default,
PermissionList = new List<Permission> {

View File

@ -1,60 +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 ModuleSearchResultManager : ISearchResultManager
{
public string Name => EntityNames.Module;
private readonly IServiceProvider _serviceProvider;
public ModuleSearchResultManager(
IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
public string GetUrl(SearchResult searchResult, SearchQuery searchQuery)
{
var pageRepository = _serviceProvider.GetRequiredService<IPageRepository>();
var pageIdValue = searchResult.SearchContentProperties?.FirstOrDefault(i => i.Name == Constants.SearchPageIdPropertyName)?.Value ?? string.Empty;
if(!string.IsNullOrEmpty(pageIdValue) && int.TryParse(pageIdValue, out int pageId))
{
var page = pageRepository.GetPage(pageId);
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(SearchContent searchResult, SearchQuery searchQuery)
{
var pageIdValue = searchResult.SearchContentProperties?.FirstOrDefault(i => i.Name == Constants.SearchPageIdPropertyName)?.Value ?? string.Empty;
if (!string.IsNullOrEmpty(pageIdValue) && int.TryParse(pageIdValue, out int pageId))
{
return CanViewPage(pageId, searchQuery.User);
}
return false;
}
private bool CanViewPage(int pageId, User user)
{
var pageRepository = _serviceProvider.GetRequiredService<IPageRepository>();
var page = pageRepository.GetPage(pageId);
return page != null && !page.IsDeleted && UserSecurity.IsAuthorized(user, PermissionNames.View, page.PermissionList)
&& (Utilities.IsEffectiveOrExpired(page.EffectiveDate, page.ExpiryDate) || UserSecurity.IsAuthorized(user, PermissionNames.Edit, page.PermissionList));
}
}
}

View File

@ -28,105 +28,10 @@ namespace Oqtane.Providers
_searchContentRepository = searchContentRepository;
}
public void Commit()
public async Task<List<SearchResult>> GetSearchResultsAsync(SearchQuery searchQuery)
{
}
public void DeleteSearchContent(string id)
{
_searchContentRepository.DeleteSearchContent(id);
}
public bool Optimize()
{
return true;
}
public void ResetIndex()
{
_searchContentRepository.DeleteAllSearchContent();
}
public void SaveSearchContent(SearchContent searchContent, Dictionary<string, string> siteSettings, bool autoCommit = false)
{
// remove existing search content
_searchContentRepository.DeleteSearchContent(searchContent.EntityName, searchContent.EntityId);
if (!searchContent.IsDeleted)
{
// clean the search content to remove html tags
CleanSearchContent(searchContent);
_searchContentRepository.AddSearchContent(searchContent);
// save the index words
AnalyzeSearchContent(searchContent, siteSettings);
}
}
public async Task<SearchResults> SearchAsync(SearchQuery searchQuery, Func<SearchContent, SearchQuery, bool> validateFunc)
{
var totalResults = 0;
var searchContentList = await _searchContentRepository.GetSearchContentsAsync(searchQuery);
// convert the search content to search results
var results = searchContentList
.Where(i => validateFunc(i, searchQuery))
.Select(i => ConvertToSearchResult(i, searchQuery));
if (searchQuery.SortDirection == SearchSortDirections.Descending)
{
switch (searchQuery.SortField)
{
case SearchSortFields.Relevance:
results = results.OrderByDescending(i => i.Score).ThenByDescending(i => i.ContentModifiedOn);
break;
case SearchSortFields.Title:
results = results.OrderByDescending(i => i.Title).ThenByDescending(i => i.ContentModifiedOn);
break;
default:
results = results.OrderByDescending(i => i.ContentModifiedOn);
break;
}
}
else
{
switch (searchQuery.SortField)
{
case SearchSortFields.Relevance:
results = results.OrderBy(i => i.Score).ThenByDescending(i => i.ContentModifiedOn);
break;
case SearchSortFields.Title:
results = results.OrderBy(i => i.Title).ThenByDescending(i => i.ContentModifiedOn);
break;
default:
results = results.OrderBy(i => i.ContentModifiedOn);
break;
}
}
// remove duplicated results based on page id for Page and Module types
results = results.DistinctBy(i =>
{
if (i.EntityName == EntityNames.Page || i.EntityName == EntityNames.Module)
{
var pageId = i.SearchContentProperties.FirstOrDefault(p => p.Name == Constants.SearchPageIdPropertyName)?.Value ?? string.Empty;
return !string.IsNullOrEmpty(pageId) ? pageId : i.UniqueKey;
}
else
{
return i.UniqueKey;
}
});
totalResults = results.Count();
return new SearchResults
{
Results = results.Skip(searchQuery.PageIndex * searchQuery.PageSize).Take(searchQuery.PageSize).ToList(),
TotalResults = totalResults
};
var searchContents = await _searchContentRepository.GetSearchContentsAsync(searchQuery);
return searchContents.Select(item => ConvertToSearchResult(item, searchQuery)).ToList();
}
private SearchResult ConvertToSearchResult(SearchContent searchContent, SearchQuery searchQuery)
@ -152,17 +57,6 @@ namespace Oqtane.Providers
return searchResult;
}
private float CalculateScore(SearchContent searchContent, SearchQuery searchQuery)
{
var score = 0f;
foreach (var keyword in SearchUtils.GetKeywords(searchQuery.Keywords))
{
score += searchContent.SearchContentWords.Where(i => i.SearchWord.Word.StartsWith(keyword)).Sum(i => i.Count);
}
return score / 100;
}
private string BuildSnippet(SearchContent searchContent, SearchQuery searchQuery)
{
var content = $"{searchContent.Title} {searchContent.Description} {searchContent.Body}";
@ -189,8 +83,6 @@ namespace Oqtane.Providers
snippet = $"{prefix}{content.Substring(start, length)}{suffix}";
break;
}
}
@ -207,6 +99,36 @@ namespace Oqtane.Providers
return snippet;
}
private float CalculateScore(SearchContent searchContent, SearchQuery searchQuery)
{
var score = 0f;
foreach (var keyword in SearchUtils.GetKeywords(searchQuery.Keywords))
{
score += searchContent.SearchContentWords.Where(i => i.SearchWord.Word.StartsWith(keyword)).Sum(i => i.Count);
}
return score / 100;
}
public Task SaveSearchContent(SearchContent searchContent, Dictionary<string, string> siteSettings)
{
// remove existing search content
_searchContentRepository.DeleteSearchContent(searchContent.EntityName, searchContent.EntityId);
if (!searchContent.IsDeleted)
{
// clean the search content to remove html tags
CleanSearchContent(searchContent);
_searchContentRepository.AddSearchContent(searchContent);
// save the index words
AnalyzeSearchContent(searchContent, siteSettings);
}
return Task.CompletedTask;
}
private void AnalyzeSearchContent(SearchContent searchContent, Dictionary<string, string> siteSettings)
{
var ignoreWords = IgnoreWords.Split(',');
@ -324,5 +246,11 @@ namespace Oqtane.Providers
return string.Join(" ", phrases);
}
public Task ResetIndex()
{
_searchContentRepository.DeleteAllSearchContent();
return Task.CompletedTask;
}
}
}

View File

@ -2,10 +2,8 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
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;
@ -16,134 +14,89 @@ 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<SearchService> _logger;
private readonly IMemoryCache _cache;
public SearchService(
IServiceProvider serviceProvider,
ITenantManager tenantManager,
IAliasRepository aliasRepository,
ISettingRepository settingRepository,
IPermissionRepository permissionRepository,
ILogger<SearchService> logger,
IMemoryCache cache)
ILogger<SearchService> logger)
{
_tenantManager = tenantManager;
_aliasRepository = aliasRepository;
_settingRepository = settingRepository;
_permissionRepository = permissionRepository;
_serviceProvider = serviceProvider;
_logger = logger;
_cache = cache;
}
public async Task<SearchResults> SearchAsync(SearchQuery searchQuery)
public async Task<SearchResults> GetSearchResultsAsync(SearchQuery searchQuery)
{
var searchProvider = GetSearchProvider(searchQuery.SiteId);
var searchResults = await searchProvider.SearchAsync(searchQuery, Visible);
var searchResults = await searchProvider.GetSearchResultsAsync(searchQuery);
//generate the document url if it's not set.
foreach (var result in searchResults.Results)
var totalResults = 0;
// trim results based on permissions
var results = searchResults.Where(i => IsVisible(i, searchQuery));
if (searchQuery.SortDirection == SearchSortDirections.Descending)
{
if(string.IsNullOrEmpty(result.Url))
switch (searchQuery.SortField)
{
result.Url = GetDocumentUrl(result, searchQuery);
case SearchSortFields.Relevance:
results = results.OrderByDescending(i => i.Score).ThenByDescending(i => i.ContentModifiedOn);
break;
case SearchSortFields.Title:
results = results.OrderByDescending(i => i.Title).ThenByDescending(i => i.ContentModifiedOn);
break;
default:
results = results.OrderByDescending(i => i.ContentModifiedOn);
break;
}
}
else
{
switch (searchQuery.SortField)
{
case SearchSortFields.Relevance:
results = results.OrderBy(i => i.Score).ThenByDescending(i => i.ContentModifiedOn);
break;
case SearchSortFields.Title:
results = results.OrderBy(i => i.Title).ThenByDescending(i => i.ContentModifiedOn);
break;
default:
results = results.OrderBy(i => i.ContentModifiedOn);
break;
}
}
return searchResults;
}
private ISearchProvider GetSearchProvider(int siteId)
{
var providerName = GetSearchProviderSetting(siteId);
var searchProviders = _serviceProvider.GetServices<ISearchProvider>();
var provider = searchProviders.FirstOrDefault(i => i.Name == providerName);
if(provider == null)
// remove duplicated results based on page id for Page and Module types
results = results.DistinctBy(i =>
{
provider = searchProviders.FirstOrDefault(i => i.Name == Constants.DefaultSearchProviderName);
}
return provider;
}
private string GetSearchProviderSetting(int siteId)
{
var setting = _settingRepository.GetSetting(EntityNames.Site, siteId, SearchProviderSettingName);
if(!string.IsNullOrEmpty(setting?.SettingValue))
{
return setting.SettingValue;
}
return Constants.DefaultSearchProviderName;
}
private bool SearchEnabled(int siteId)
{
var setting = _settingRepository.GetSetting(EntityNames.Site, siteId, SearchEnabledSettingName);
if (!string.IsNullOrEmpty(setting?.SettingValue))
{
return bool.TryParse(setting.SettingValue, out bool enabled) && enabled;
}
return true;
}
private void SetTenant(int siteId)
{
var alias = _aliasRepository.GetAliases().OrderBy(i => i.SiteId).ThenByDescending(i => i.IsDefault).FirstOrDefault(i => i.SiteId == siteId);
_tenantManager.SetAlias(alias);
}
private List<ISearchResultManager> GetSearchResultManagers()
{
var managers = new List<ISearchResultManager>();
var managerTypes = AppDomain.CurrentDomain.GetAssemblies()
.SelectMany(s => s.GetTypes())
.Where(p => typeof(ISearchResultManager).IsAssignableFrom(p) && !p.IsInterface && !p.IsAbstract);
foreach (var type in managerTypes)
{
var manager = (ISearchResultManager)ActivatorUtilities.CreateInstance(_serviceProvider, type);
managers.Add(manager);
}
return managers.ToList();
}
public async Task SaveSearchContentAsync(List<SearchContent> searchContents, Dictionary<string, string> siteSettings)
{
if(searchContents.Any())
{
var searchProvider = GetSearchProvider(searchContents.First().SiteId);
foreach (var searchContent in searchContents)
if (i.EntityName == EntityNames.Page || i.EntityName == EntityNames.Module)
{
try
{
searchProvider.SaveSearchContent(searchContent, siteSettings);
}
catch(Exception ex)
{
_logger.LogError(ex, $"Search: Save search content {searchContent.UniqueKey} failed.");
}
var pageId = i.SearchContentProperties.FirstOrDefault(p => p.Name == Constants.SearchPageIdPropertyName)?.Value ?? string.Empty;
return !string.IsNullOrEmpty(pageId) ? pageId : i.UniqueKey;
}
else
{
return i.UniqueKey;
}
});
await Task.CompletedTask;
totalResults = results.Count();
//commit the index changes
searchProvider.Commit();
}
return new SearchResults
{
Results = results.Skip(searchQuery.PageIndex * searchQuery.PageSize).Take(searchQuery.PageSize).ToList(),
TotalResults = totalResults
};
}
private bool Visible(SearchContent searchContent, SearchQuery searchQuery)
private bool IsVisible(SearchContent searchContent, SearchQuery searchQuery)
{
var visible = true;
foreach (var permission in searchContent.Permissions.Split(','))
@ -165,15 +118,52 @@ namespace Oqtane.Services
return UserSecurity.IsAuthorized(user, PermissionNames.View, permissions);
}
private string GetDocumentUrl(SearchResult result, SearchQuery searchQuery)
public async Task<string> SaveSearchContentsAsync(List<SearchContent> searchContents, Dictionary<string, string> siteSettings)
{
var searchResultManager = GetSearchResultManagers().FirstOrDefault(i => i.Name == result.EntityName);
if(searchResultManager != null)
var result = "";
if (searchContents.Any())
{
return searchResultManager.GetUrl(result, searchQuery);
var searchProvider = GetSearchProvider(searchContents.First().SiteId);
foreach (var searchContent in searchContents)
{
try
{
await searchProvider.SaveSearchContent(searchContent, siteSettings);
}
catch (Exception ex)
{
result += $"Error Saving Search Content With UniqueKey {searchContent.UniqueKey} - {ex.Message}<br />";
}
}
}
return string.Empty;
return result;
}
private ISearchProvider GetSearchProvider(int siteId)
{
var providerName = GetSearchProviderSetting(siteId);
var searchProviders = _serviceProvider.GetServices<ISearchProvider>();
var provider = searchProviders.FirstOrDefault(i => i.Name == providerName);
if (provider == null)
{
provider = searchProviders.FirstOrDefault(i => i.Name == Constants.DefaultSearchProviderName);
}
return provider;
}
private string GetSearchProviderSetting(int siteId)
{
var setting = _settingRepository.GetSetting(EntityNames.Site, siteId, SearchProviderSettingName);
if (!string.IsNullOrEmpty(setting?.SettingValue))
{
return setting.SettingValue;
}
return Constants.DefaultSearchProviderName;
}
}
}

View File

@ -11,16 +11,10 @@ namespace Oqtane.Services
{
string Name { get; }
void SaveSearchContent(SearchContent searchContent, Dictionary<string, string> siteSettings, bool autoCommit = false);
Task<List<SearchResult>> GetSearchResultsAsync(SearchQuery searchQuery);
void DeleteSearchContent(string id);
Task<SearchResults> SearchAsync(SearchQuery searchQuery, Func<SearchContent, SearchQuery, bool> validateFunc);
Task SaveSearchContent(SearchContent searchContent, Dictionary<string, string> siteSettings);
bool Optimize();
void Commit();
void ResetIndex();
Task ResetIndex();
}
}

View File

@ -1,14 +0,0 @@
using System;
using Oqtane.Models;
namespace Oqtane.Services
{
public interface ISearchResultManager
{
string Name { get; }
bool Visible(SearchContent searchResult, SearchQuery searchQuery);
string GetUrl(SearchResult searchResult, SearchQuery searchQuery);
}
}

View File

@ -6,8 +6,8 @@ namespace Oqtane.Services
{
public interface ISearchService
{
Task SaveSearchContentAsync(List<SearchContent> searchContents, Dictionary<string, string> siteSettings);
Task<SearchResults> GetSearchResultsAsync(SearchQuery searchQuery);
Task<SearchResults> SearchAsync(SearchQuery searchQuery);
Task<string> SaveSearchContentsAsync(List<SearchContent> searchContents, Dictionary<string, string> siteSettings);
}
}