mirror of https://github.com/abpframework/abp.git
24 changed files with 611 additions and 8 deletions
@ -1,4 +1,8 @@ |
|||||
{ |
{ |
||||
"ConnectionString": "Server=localhost;Database=VoloDocs;Trusted_Connection=True;MultipleActiveResultSets=true", |
"ConnectionString": "Server=localhost;Database=VoloDocs;Trusted_Connection=True;MultipleActiveResultSets=true", |
||||
"LogoUrl": "/assets/images/Logo.png" |
"LogoUrl": "/assets/images/Logo.png", |
||||
|
|
||||
|
"ElasticSearch": { |
||||
|
"Url": "http://172.25.134.11:9200" |
||||
|
} |
||||
} |
} |
||||
@ -0,0 +1,19 @@ |
|||||
|
using System; |
||||
|
|
||||
|
namespace Volo.Docs.Documents |
||||
|
{ |
||||
|
public class DocumentSearchInput |
||||
|
{ |
||||
|
public string Context { get; set; } |
||||
|
|
||||
|
public Guid ProjectId { get; set; } |
||||
|
|
||||
|
public string LanguageCode { get; set; } |
||||
|
|
||||
|
public string Version { get; set; } |
||||
|
|
||||
|
public int? SkipCount { get; set; } |
||||
|
|
||||
|
public int? MaxResultCount { get; set; } |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,24 @@ |
|||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Text; |
||||
|
|
||||
|
namespace Volo.Docs.Documents |
||||
|
{ |
||||
|
public class DocumentSearchOutput |
||||
|
{ |
||||
|
public DocumentSearchOutput() |
||||
|
{ |
||||
|
Highlight = new List<string>(); |
||||
|
} |
||||
|
|
||||
|
public string Name { get; set; } |
||||
|
|
||||
|
public string FileName { get; set; } |
||||
|
|
||||
|
public string Version { get; set; } |
||||
|
|
||||
|
public string LanguageCode { get; set; } |
||||
|
|
||||
|
public List<string> Highlight { get; set; } |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,27 @@ |
|||||
|
using System; |
||||
|
using Microsoft.Extensions.Configuration; |
||||
|
using Microsoft.Extensions.Options; |
||||
|
using Nest; |
||||
|
using Volo.Abp.DependencyInjection; |
||||
|
|
||||
|
namespace Volo.Docs.Documents.FullSearch.Elastic |
||||
|
{ |
||||
|
public class DefaultElasticClientProvider : IElasticClientProvider, ISingletonDependency |
||||
|
{ |
||||
|
protected readonly DocsElasticSearchOptions Options; |
||||
|
protected readonly IConfiguration Configuration; |
||||
|
|
||||
|
public DefaultElasticClientProvider(IOptions<DocsElasticSearchOptions> options, IConfiguration configuration) |
||||
|
{ |
||||
|
Configuration = configuration; |
||||
|
Options = options.Value; |
||||
|
} |
||||
|
|
||||
|
public virtual IElasticClient GetClient() |
||||
|
{ |
||||
|
var node = new Uri(Configuration["ElasticSearch:Url"]); |
||||
|
var settings = new ConnectionSettings(node).DefaultIndex(Options.IndexName); |
||||
|
return new ElasticClient(settings); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,15 @@ |
|||||
|
namespace Volo.Docs.Documents.FullSearch.Elastic |
||||
|
{ |
||||
|
public class DocsElasticSearchOptions |
||||
|
{ |
||||
|
public DocsElasticSearchOptions() |
||||
|
{ |
||||
|
Enable = false; |
||||
|
IndexName = "abp_documents"; |
||||
|
} |
||||
|
|
||||
|
public bool Enable { get; set; } |
||||
|
|
||||
|
public string IndexName { get; set; } |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,49 @@ |
|||||
|
using System.Threading.Tasks; |
||||
|
using Microsoft.Extensions.Options; |
||||
|
using Volo.Abp.DependencyInjection; |
||||
|
using Volo.Abp.Domain.Entities.Events; |
||||
|
using Volo.Abp.EventBus; |
||||
|
|
||||
|
namespace Volo.Docs.Documents.FullSearch.Elastic |
||||
|
{ |
||||
|
public class DocumentChangedEventHandler : ILocalEventHandler<EntityCreatedEventData<Document>>, |
||||
|
ILocalEventHandler<EntityUpdatedEventData<Document>>, |
||||
|
ILocalEventHandler<EntityDeletedEventData<Document>>, |
||||
|
ITransientDependency |
||||
|
{ |
||||
|
private readonly DocsElasticSearchOptions _options; |
||||
|
private readonly IDocumentFullSearch _documentFullSearch; |
||||
|
|
||||
|
public DocumentChangedEventHandler(IDocumentFullSearch documentFullSearch, IOptions<DocsElasticSearchOptions> options) |
||||
|
{ |
||||
|
_documentFullSearch = documentFullSearch; |
||||
|
_options = options.Value; |
||||
|
} |
||||
|
|
||||
|
public async Task HandleEventAsync(EntityCreatedEventData<Document> eventData) |
||||
|
{ |
||||
|
await AddOrUpdate(eventData.Entity); |
||||
|
} |
||||
|
|
||||
|
public async Task HandleEventAsync(EntityUpdatedEventData<Document> eventData) |
||||
|
{ |
||||
|
await AddOrUpdate(eventData.Entity); |
||||
|
} |
||||
|
|
||||
|
private async Task AddOrUpdate(Document document) |
||||
|
{ |
||||
|
if (_options.Enable) |
||||
|
{ |
||||
|
await _documentFullSearch.AddOrUpdateAsync(document); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public async Task HandleEventAsync(EntityDeletedEventData<Document> eventData) |
||||
|
{ |
||||
|
if (_options.Enable) |
||||
|
{ |
||||
|
await _documentFullSearch.DeleteAsync(eventData.Entity.Id); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,179 @@ |
|||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Threading; |
||||
|
using System.Threading.Tasks; |
||||
|
using Elasticsearch.Net; |
||||
|
using Microsoft.Extensions.Options; |
||||
|
using Nest; |
||||
|
using Volo.Abp; |
||||
|
using Volo.Abp.Domain.Services; |
||||
|
|
||||
|
namespace Volo.Docs.Documents.FullSearch.Elastic |
||||
|
{ |
||||
|
public class ElasticDocumentFullSearch : DomainService, IDocumentFullSearch |
||||
|
{ |
||||
|
private readonly IElasticClientProvider _clientProvider; |
||||
|
private readonly DocsElasticSearchOptions _options; |
||||
|
|
||||
|
public ElasticDocumentFullSearch(IElasticClientProvider clientProvider, IOptions<DocsElasticSearchOptions> options) |
||||
|
{ |
||||
|
_clientProvider = clientProvider; |
||||
|
_options = options.Value; |
||||
|
|
||||
|
if (!_options.Enable) |
||||
|
{ |
||||
|
//TODO: localize message
|
||||
|
throw new BusinessException("", ""); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public async Task CreateIndexIfNeededAsync(CancellationToken cancellationToken = default) |
||||
|
{ |
||||
|
var client = _clientProvider.GetClient(); |
||||
|
|
||||
|
var existsResponse = await client.Indices.ExistsAsync(_options.IndexName, ct: cancellationToken); |
||||
|
|
||||
|
HandleError(existsResponse); |
||||
|
|
||||
|
if (!existsResponse.Exists) |
||||
|
{ |
||||
|
HandleError(await client.Indices.CreateAsync(_options.IndexName, c => c |
||||
|
.Map<EsDocument>(m => |
||||
|
{ |
||||
|
return m |
||||
|
.Properties(p => p |
||||
|
.Keyword(x => x.Name(d => d.Id)) |
||||
|
.Keyword(x => x.Name(d => d.ProjectId)) |
||||
|
.Keyword(x => x.Name(d => d.Name)) |
||||
|
.Keyword(x => x.Name(d => d.FileName)) |
||||
|
.Keyword(x => x.Name(d => d.Version)) |
||||
|
.Keyword(x => x.Name(d => d.LanguageCode)) |
||||
|
.Text(x => x.Name(d => d.Content))); |
||||
|
}), cancellationToken)); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public async Task AddOrUpdateAsync(Document document, CancellationToken cancellationToken = default) |
||||
|
{ |
||||
|
var client = _clientProvider.GetClient(); |
||||
|
|
||||
|
var existsResponse = await client.DocumentExistsAsync<EsDocument>(DocumentPath<EsDocument>.Id(document.Id), |
||||
|
x => x.Index(_options.IndexName), cancellationToken); |
||||
|
|
||||
|
HandleError(existsResponse); |
||||
|
|
||||
|
var esDocument = new EsDocument |
||||
|
{ |
||||
|
Id = document.Id, |
||||
|
ProjectId = document.ProjectId, |
||||
|
Name = document.Name, |
||||
|
FileName = document.FileName, |
||||
|
Content = document.Content, |
||||
|
LanguageCode = document.LanguageCode, |
||||
|
Version = document.Version |
||||
|
}; |
||||
|
|
||||
|
if (!existsResponse.Exists) |
||||
|
{ |
||||
|
HandleError(await client.IndexAsync<EsDocument>(esDocument, |
||||
|
x => x.Id(document.Id).Index(_options.IndexName), cancellationToken)); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
HandleError(await client.UpdateAsync<EsDocument>(DocumentPath<EsDocument>.Id(document.Id), |
||||
|
x => x.Doc(esDocument).Index(_options.IndexName), cancellationToken)); |
||||
|
} |
||||
|
|
||||
|
} |
||||
|
|
||||
|
public async Task DeleteAsync(Guid id, CancellationToken cancellationToken = default) |
||||
|
{ |
||||
|
HandleError(await _clientProvider.GetClient() |
||||
|
.DeleteAsync(DocumentPath<Document>.Id(id), x => x.Index(_options.IndexName), cancellationToken)); |
||||
|
} |
||||
|
|
||||
|
public async Task<List<EsDocument>> SearchAsync(string context, Guid projectId, string languageCode, |
||||
|
string version, int? skipCount = null, int? maxResultCount = null, |
||||
|
CancellationToken cancellationToken = default) |
||||
|
{ |
||||
|
var request = new SearchRequest |
||||
|
{ |
||||
|
Size = maxResultCount ?? 10, |
||||
|
From = skipCount ?? 0, |
||||
|
Query = new BoolQuery |
||||
|
{ |
||||
|
Must = new QueryContainer[] |
||||
|
{ |
||||
|
new MatchQuery |
||||
|
{ |
||||
|
Field = "content", |
||||
|
Query = context |
||||
|
} |
||||
|
}, |
||||
|
Filter = new QueryContainer[] |
||||
|
{ |
||||
|
new BoolQuery |
||||
|
{ |
||||
|
Must = new QueryContainer[] |
||||
|
{ |
||||
|
new TermQuery |
||||
|
{ |
||||
|
Field = "projectId", |
||||
|
Value = projectId |
||||
|
}, |
||||
|
new TermQuery |
||||
|
{ |
||||
|
Field = "version", |
||||
|
Value = version |
||||
|
}, |
||||
|
new TermQuery |
||||
|
{ |
||||
|
Field = "languageCode", |
||||
|
Value = languageCode |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
}, |
||||
|
Highlight = new Highlight |
||||
|
{ |
||||
|
PreTags = new[] { "<highlight>" }, |
||||
|
PostTags = new[] { "</highlight>" }, |
||||
|
Fields = new Dictionary<Field, IHighlightField> |
||||
|
{ |
||||
|
{ |
||||
|
"content", new HighlightField() |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
//var json = _clientProvider.GetClient().RequestResponseSerializer.SerializeToString(request);
|
||||
|
var response = await _clientProvider.GetClient().SearchAsync<EsDocument>(request, cancellationToken); |
||||
|
|
||||
|
HandleError(response); |
||||
|
|
||||
|
var docs = new List<EsDocument>(); |
||||
|
foreach (var hit in response.Hits) |
||||
|
{ |
||||
|
var doc = hit.Source; |
||||
|
if (hit.Highlight.ContainsKey("content")) |
||||
|
{ |
||||
|
doc.Highlight = new List<string>(); |
||||
|
doc.Highlight.AddRange(hit.Highlight["content"]); |
||||
|
} |
||||
|
docs.Add(doc); |
||||
|
} |
||||
|
|
||||
|
return docs; |
||||
|
} |
||||
|
|
||||
|
protected void HandleError(IElasticsearchResponse response) |
||||
|
{ |
||||
|
if (!response.ApiCall.Success) |
||||
|
{ |
||||
|
throw response.ApiCall.OriginalException; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,29 @@ |
|||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
|
||||
|
namespace Volo.Docs.Documents.FullSearch.Elastic |
||||
|
{ |
||||
|
public class EsDocument |
||||
|
{ |
||||
|
public EsDocument() |
||||
|
{ |
||||
|
Highlight = new List<string>(); |
||||
|
} |
||||
|
|
||||
|
public Guid Id { get; set; } |
||||
|
|
||||
|
public Guid ProjectId { get; set; } |
||||
|
|
||||
|
public string Name { get; set; } |
||||
|
|
||||
|
public string FileName { get; set; } |
||||
|
|
||||
|
public string Version { get; set; } |
||||
|
|
||||
|
public string LanguageCode { get; set; } |
||||
|
|
||||
|
public string Content { get; set; } |
||||
|
|
||||
|
public List<string> Highlight { get; set; } |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,20 @@ |
|||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Threading; |
||||
|
using System.Threading.Tasks; |
||||
|
|
||||
|
namespace Volo.Docs.Documents.FullSearch.Elastic |
||||
|
{ |
||||
|
public interface IDocumentFullSearch |
||||
|
{ |
||||
|
Task CreateIndexIfNeededAsync(CancellationToken cancellationToken = default); |
||||
|
|
||||
|
Task AddOrUpdateAsync(Document document, CancellationToken cancellationToken = default); |
||||
|
|
||||
|
Task DeleteAsync(Guid id, CancellationToken cancellationToken = default); |
||||
|
|
||||
|
Task<List<EsDocument>> SearchAsync(string context, Guid projectId, string languageCode, |
||||
|
string version, int? skipCount = null, int? maxResultCount = null, |
||||
|
CancellationToken cancellationToken = default); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,9 @@ |
|||||
|
using Nest; |
||||
|
|
||||
|
namespace Volo.Docs.Documents.FullSearch.Elastic |
||||
|
{ |
||||
|
public interface IElasticClientProvider |
||||
|
{ |
||||
|
IElasticClient GetClient(); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,67 @@ |
|||||
|
@page |
||||
|
@using Microsoft.AspNetCore.Mvc.Localization |
||||
|
@using Volo.Abp.AspNetCore.Mvc.UI.Layout |
||||
|
@using Volo.Abp.AspNetCore.Mvc.UI.Theming |
||||
|
@using Volo.Docs.Localization |
||||
|
@using Volo.Docs.Pages.Documents |
||||
|
@inject IHtmlLocalizer<DocsResource> L |
||||
|
@inject IThemeManager ThemeManager |
||||
|
@inject IPageLayout PageLayout |
||||
|
@model SearchModel |
||||
|
@{ |
||||
|
Layout = ThemeManager.CurrentTheme.GetEmptyLayout(); |
||||
|
} |
||||
|
@section styles { |
||||
|
<style> |
||||
|
highlight { |
||||
|
font-weight: bold; |
||||
|
color: red; |
||||
|
font-style: italic; |
||||
|
} |
||||
|
</style> |
||||
|
} |
||||
|
|
||||
|
<div class="container"> |
||||
|
<form method="get" action="/search/@Model.LanguageCode/@Model.ProjectName/@Model.Version/" class="mt-4"> |
||||
|
<input type="text" asp-for="@Model.KeyWord" class="form-control" /> |
||||
|
<button type="submit" class="btn-block btn-primary btn-lg mt-3">Search</button> |
||||
|
</form> |
||||
|
|
||||
|
<div class="my-3 p-3 bg-white rounded"> |
||||
|
<h6 class="border-bottom pb-4 mb-0">Search results</h6> |
||||
|
|
||||
|
@foreach (var docs in Model.SearchOutputs) |
||||
|
{ |
||||
|
<div class="media text-muted pt-3"> |
||||
|
<div class="media-body pb-3 mb-0 small border-bottom"> |
||||
|
<div class="list-group"> |
||||
|
|
||||
|
@functions |
||||
|
{ |
||||
|
string RemoveFileExtensionFromPath(string path) |
||||
|
{ |
||||
|
if (path == null) |
||||
|
{ |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
return path.EndsWith("." + @Model.Project.Format) |
||||
|
? path.Left(path.Length - Model.Project.Format.Length - 1) |
||||
|
: path; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
<a href="/@Model.LanguageCode/@Model.ProjectName/@Model.Version/@RemoveFileExtensionFromPath(docs.Name)"> |
||||
|
<h3>@RemoveFileExtensionFromPath(docs.Name)</h3></a> |
||||
|
|
||||
|
@foreach (var highlight in docs.Highlight) |
||||
|
{ |
||||
|
<p class="list-group-item list-group-item-action">@Html.Raw(highlight)</p> |
||||
|
} |
||||
|
|
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
} |
||||
|
</div> |
||||
|
</div> |
||||
@ -0,0 +1,68 @@ |
|||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Linq; |
||||
|
using System.Threading.Tasks; |
||||
|
using Microsoft.AspNetCore.Mvc; |
||||
|
using Microsoft.AspNetCore.Mvc.RazorPages; |
||||
|
using Volo.Docs.Documents; |
||||
|
using Volo.Docs.HtmlConverting; |
||||
|
using Volo.Docs.Models; |
||||
|
using Volo.Docs.Projects; |
||||
|
|
||||
|
namespace Volo.Docs.Pages.Documents |
||||
|
{ |
||||
|
public class SearchModel : PageModel |
||||
|
{ |
||||
|
[BindProperty(SupportsGet = true)] |
||||
|
public string ProjectName { get; set; } |
||||
|
|
||||
|
[BindProperty(SupportsGet = true)] |
||||
|
public string Version { get; set; } |
||||
|
|
||||
|
[BindProperty(SupportsGet = true)] |
||||
|
public string LanguageCode { get; set; } |
||||
|
|
||||
|
[BindProperty(SupportsGet = true)] |
||||
|
public string KeyWord { get; set; } |
||||
|
|
||||
|
public ProjectDto Project { get; set; } |
||||
|
|
||||
|
private readonly IProjectAppService _projectAppService; |
||||
|
private readonly IDocumentAppService _documentAppService; |
||||
|
|
||||
|
public SearchModel(IProjectAppService projectAppService, |
||||
|
IDocumentAppService documentAppService) |
||||
|
{ |
||||
|
_projectAppService = projectAppService; |
||||
|
_documentAppService = documentAppService; |
||||
|
} |
||||
|
|
||||
|
public List<DocumentSearchOutput> SearchOutputs { get; set; } = new List<DocumentSearchOutput>(); |
||||
|
|
||||
|
public async Task<IActionResult> OnGetAsync(string keyword) |
||||
|
{ |
||||
|
KeyWord = keyword; |
||||
|
|
||||
|
Project = await _projectAppService.GetAsync(ProjectName); |
||||
|
|
||||
|
var output = await _projectAppService.GetVersionsAsync(Project.ShortName); |
||||
|
|
||||
|
var versions = output.Items.ToList(); |
||||
|
|
||||
|
if (versions.Any() && string.Equals(Version, DocsAppConsts.Latest, StringComparison.OrdinalIgnoreCase)) |
||||
|
{ |
||||
|
Version = versions.First().Name; |
||||
|
} |
||||
|
|
||||
|
SearchOutputs = await _documentAppService.SearchAsync(new DocumentSearchInput |
||||
|
{ |
||||
|
ProjectId = Project.Id, |
||||
|
Context = KeyWord, |
||||
|
LanguageCode = LanguageCode, |
||||
|
Version = Version |
||||
|
}); |
||||
|
|
||||
|
return Page(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue