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", |
|||
"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