#4303: add search function.
This commit is contained in:
10
Oqtane.Shared/Enums/SearchSortDirections.cs
Normal file
10
Oqtane.Shared/Enums/SearchSortDirections.cs
Normal file
@ -0,0 +1,10 @@
|
||||
using System;
|
||||
|
||||
namespace Oqtane.Shared
|
||||
{
|
||||
public enum SearchSortDirections
|
||||
{
|
||||
Ascending,
|
||||
Descending
|
||||
}
|
||||
}
|
11
Oqtane.Shared/Enums/SearchSortFields.cs
Normal file
11
Oqtane.Shared/Enums/SearchSortFields.cs
Normal file
@ -0,0 +1,11 @@
|
||||
using System;
|
||||
|
||||
namespace Oqtane.Shared
|
||||
{
|
||||
public enum SearchSortFields
|
||||
{
|
||||
Relevance,
|
||||
Title,
|
||||
LastModified
|
||||
}
|
||||
}
|
14
Oqtane.Shared/Interfaces/IModuleSearch.cs
Normal file
14
Oqtane.Shared/Interfaces/IModuleSearch.cs
Normal file
@ -0,0 +1,14 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Oqtane.Models;
|
||||
|
||||
namespace Oqtane.Interfaces
|
||||
{
|
||||
public interface IModuleSearch
|
||||
{
|
||||
public IList<SearchDocument> GetSearchDocuments(Module module, DateTime startTime);
|
||||
}
|
||||
}
|
20
Oqtane.Shared/Interfaces/ISearchIndexManager.cs
Normal file
20
Oqtane.Shared/Interfaces/ISearchIndexManager.cs
Normal file
@ -0,0 +1,20 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using System.Web;
|
||||
using Oqtane.Models;
|
||||
|
||||
namespace Oqtane.Services
|
||||
{
|
||||
public interface ISearchIndexManager
|
||||
{
|
||||
int Priority { get; }
|
||||
|
||||
string Name { get; }
|
||||
|
||||
bool IsIndexEnabled(int siteId);
|
||||
|
||||
int IndexDocuments(int siteId, DateTime? startTime, Action<IList<SearchDocument>> processSearchDocuments, Action<string> handleError);
|
||||
}
|
||||
}
|
26
Oqtane.Shared/Interfaces/ISearchProvider.cs
Normal file
26
Oqtane.Shared/Interfaces/ISearchProvider.cs
Normal file
@ -0,0 +1,26 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Oqtane.Models;
|
||||
|
||||
namespace Oqtane.Services
|
||||
{
|
||||
public interface ISearchProvider
|
||||
{
|
||||
string Name { get; }
|
||||
|
||||
void SaveDocument(SearchDocument document, bool autoCommit = false);
|
||||
|
||||
void DeleteDocument(string id);
|
||||
|
||||
Task<SearchResults> SearchAsync(SearchQuery searchQuery, Func<SearchDocument, SearchQuery, bool> validateFunc);
|
||||
|
||||
bool Optimize();
|
||||
|
||||
void Commit();
|
||||
|
||||
void ResetIndex();
|
||||
}
|
||||
}
|
14
Oqtane.Shared/Interfaces/ISearchResultManager.cs
Normal file
14
Oqtane.Shared/Interfaces/ISearchResultManager.cs
Normal file
@ -0,0 +1,14 @@
|
||||
using System;
|
||||
using Oqtane.Models;
|
||||
|
||||
namespace Oqtane.Services
|
||||
{
|
||||
public interface ISearchResultManager
|
||||
{
|
||||
string Name { get; }
|
||||
|
||||
bool Visible(SearchDocument searchResult, SearchQuery searchQuery);
|
||||
|
||||
string GetUrl(SearchResult searchResult, SearchQuery searchQuery);
|
||||
}
|
||||
}
|
15
Oqtane.Shared/Interfaces/ISearchService.cs
Normal file
15
Oqtane.Shared/Interfaces/ISearchService.cs
Normal file
@ -0,0 +1,15 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Oqtane.Models;
|
||||
|
||||
namespace Oqtane.Services
|
||||
{
|
||||
public interface ISearchService
|
||||
{
|
||||
void IndexContent(int siteId, DateTime? startTime, Action<string> logNote, Action<string> handleError);
|
||||
|
||||
Task<SearchResults> SearchAsync(SearchQuery searchQuery);
|
||||
}
|
||||
}
|
45
Oqtane.Shared/Models/SearchDocument.cs
Normal file
45
Oqtane.Shared/Models/SearchDocument.cs
Normal file
@ -0,0 +1,45 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using System.Text.Json;
|
||||
namespace Oqtane.Models
|
||||
{
|
||||
public class SearchDocument : ModelBase
|
||||
{
|
||||
public int SearchDocumentId { get; set; }
|
||||
|
||||
[NotMapped]
|
||||
public string UniqueKey => $"{IndexerName}:{EntryId}";
|
||||
|
||||
public int EntryId { get; set; }
|
||||
|
||||
public string IndexerName { get; set; }
|
||||
|
||||
public int SiteId { get; set; }
|
||||
|
||||
public string Title { get; set; }
|
||||
|
||||
public string Description { get; set; }
|
||||
|
||||
public string Body { get; set; }
|
||||
|
||||
public string Url { get; set; }
|
||||
|
||||
public DateTime ModifiedTime { get; set; }
|
||||
|
||||
public bool IsActive { get; set; }
|
||||
|
||||
public string AdditionalContent { get; set; }
|
||||
|
||||
public string LanguageCode { get; set; }
|
||||
|
||||
public IList<SearchDocumentTag> Tags { get; set; }
|
||||
|
||||
public IList<SearchDocumentProperty> Properties { get; set; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return JsonSerializer.Serialize(this);
|
||||
}
|
||||
}
|
||||
}
|
17
Oqtane.Shared/Models/SearchDocumentProperty.cs
Normal file
17
Oqtane.Shared/Models/SearchDocumentProperty.cs
Normal file
@ -0,0 +1,17 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Oqtane.Models
|
||||
{
|
||||
public class SearchDocumentProperty
|
||||
{
|
||||
[Key]
|
||||
public int PropertyId { get; set; }
|
||||
|
||||
public int SearchDocumentId { get; set; }
|
||||
|
||||
public string Name { get; set; }
|
||||
|
||||
public string Value { get; set; }
|
||||
}
|
||||
}
|
14
Oqtane.Shared/Models/SearchDocumentTag.cs
Normal file
14
Oqtane.Shared/Models/SearchDocumentTag.cs
Normal file
@ -0,0 +1,14 @@
|
||||
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; }
|
||||
}
|
||||
}
|
37
Oqtane.Shared/Models/SearchQuery.cs
Normal file
37
Oqtane.Shared/Models/SearchQuery.cs
Normal file
@ -0,0 +1,37 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Oqtane.Shared;
|
||||
|
||||
namespace Oqtane.Models
|
||||
{
|
||||
public class SearchQuery
|
||||
{
|
||||
public int SiteId { get; set; }
|
||||
|
||||
public Alias Alias { get; set; }
|
||||
|
||||
public User User { get; set; }
|
||||
|
||||
public string Keywords { get; set; }
|
||||
|
||||
public IList<string> Sources { get; set; } = new List<string>();
|
||||
|
||||
public DateTime BeginModifiedTimeUtc { get; set; }
|
||||
|
||||
public DateTime EndModifiedTimeUtc { get; set; }
|
||||
|
||||
public IList<string> Tags { get; set; } = new List<string>();
|
||||
|
||||
public IDictionary<string, string> Properties { get; set; } = new Dictionary<string, string>();
|
||||
|
||||
public int PageIndex { get; set; }
|
||||
|
||||
public int PageSize { get; set; }
|
||||
|
||||
public SearchSortFields SortField { get; set; }
|
||||
|
||||
public SearchSortDirections SortDirection { get; set; }
|
||||
|
||||
public int BodySnippetLength { get; set;} = 255;
|
||||
}
|
||||
}
|
11
Oqtane.Shared/Models/SearchResult.cs
Normal file
11
Oqtane.Shared/Models/SearchResult.cs
Normal file
@ -0,0 +1,11 @@
|
||||
namespace Oqtane.Models
|
||||
{
|
||||
public class SearchResult : SearchDocument
|
||||
{
|
||||
public float Score { get; set; }
|
||||
|
||||
public string DisplayScore { get; set; }
|
||||
|
||||
public string Snippet { get; set; }
|
||||
}
|
||||
}
|
11
Oqtane.Shared/Models/SearchResults.cs
Normal file
11
Oqtane.Shared/Models/SearchResults.cs
Normal file
@ -0,0 +1,11 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Oqtane.Models
|
||||
{
|
||||
public class SearchResults
|
||||
{
|
||||
public IList<SearchResult> Results { get; set; }
|
||||
|
||||
public int TotalResults { get; set; }
|
||||
}
|
||||
}
|
@ -77,6 +77,22 @@ 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 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";
|
||||
|
95
Oqtane.Shared/Shared/SearchUtils.cs
Normal file
95
Oqtane.Shared/Shared/SearchUtils.cs
Normal file
@ -0,0 +1,95 @@
|
||||
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);
|
||||
|
||||
public static string Clean(string html, bool removePunctuation)
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
public static IList<string> GetKeywordsList(string keywords)
|
||||
{
|
||||
var keywordsList = new List<string>();
|
||||
if(!string.IsNullOrEmpty(keywords))
|
||||
{
|
||||
foreach (var keyword in keywords.Split(' '))
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(keyword.Trim()))
|
||||
{
|
||||
keywordsList.Add(keyword.Trim());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return keywordsList;
|
||||
}
|
||||
|
||||
private static string StripTags(string html, bool retainSpace)
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user