Merge pull request #2901 from abpframework/maliming/docs-search

Add document full text search function (elastic search).
pull/3029/head
Yunus Emre Kalkan 5 years ago committed by GitHub
commit 86c09b5a6f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<Project Sdk="Microsoft.NET.Sdk.Web">
<Import Project="..\..\..\..\configureawait.props" />
@ -8,6 +8,7 @@
<GenerateBindingRedirectsOutputType>true</GenerateBindingRedirectsOutputType>
<MvcRazorExcludeRefAssembliesFromPublish>false</MvcRazorExcludeRefAssembliesFromPublish>
<RuntimeIdentifiers>win-x64;osx-x64;linux-x64</RuntimeIdentifiers>
<UserSecretsId>5f11b41f-0025-4fe6-ab97-60ec1bd4e8c2</UserSecretsId>
</PropertyGroup>
<ItemGroup>

@ -33,6 +33,7 @@ using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Hosting;
using Volo.Abp.Account;
using Volo.Abp.Validation.Localization;
using Volo.Docs.Documents.FullSearch.Elastic;
namespace VoloDocs.Web
{
@ -71,6 +72,11 @@ namespace VoloDocs.Web
options.RoutePrefix = null;
});
Configure<DocsElasticSearchOptions>(options =>
{
options.Enable = true;
});
Configure<AbpDbConnectionOptions>(options =>
{
options.ConnectionStrings.Default = configuration["ConnectionString"];

@ -1,4 +1,7 @@
{
"ConnectionString": "Server=localhost;Database=VoloDocs;Trusted_Connection=True;MultipleActiveResultSets=true",
"LogoUrl": "/assets/images/Logo.png"
"LogoUrl": "/assets/images/Logo.png",
"ElasticSearch": {
"Url": "http://localhost:9200"
}
}

@ -8,5 +8,7 @@ namespace Volo.Docs.Admin.Documents
Task PullAllAsync(PullAllDocumentInput input);
Task PullAsync(PullDocumentInput input);
Task ReindexAsync();
}
}

@ -7,6 +7,7 @@ using Newtonsoft.Json;
using Volo.Abp.Application.Services;
using Volo.Abp.Caching;
using Volo.Docs.Documents;
using Volo.Docs.Documents.FullSearch.Elastic;
using Volo.Docs.Projects;
namespace Volo.Docs.Admin.Documents
@ -18,16 +19,19 @@ namespace Volo.Docs.Admin.Documents
private readonly IDocumentRepository _documentRepository;
private readonly IDocumentSourceFactory _documentStoreFactory;
private readonly IDistributedCache<DocumentUpdateInfo> _documentUpdateCache;
private readonly IDocumentFullSearch _documentFullSearch;
public DocumentAdminAppService(IProjectRepository projectRepository,
IDocumentRepository documentRepository,
IDocumentSourceFactory documentStoreFactory,
IDistributedCache<DocumentUpdateInfo> documentUpdateCache)
IDistributedCache<DocumentUpdateInfo> documentUpdateCache,
IDocumentFullSearch documentFullSearch)
{
_projectRepository = projectRepository;
_documentRepository = documentRepository;
_documentStoreFactory = documentStoreFactory;
_documentUpdateCache = documentUpdateCache;
_documentFullSearch = documentFullSearch;
}
public async Task PullAllAsync(PullAllDocumentInput input)
@ -80,6 +84,22 @@ namespace Volo.Docs.Admin.Documents
await UpdateDocumentUpdateInfoCache(sourceDocument);
}
public async Task ReindexAsync()
{
var docs = await _documentRepository.GetListAsync();
var projects = await _projectRepository.GetListAsync();
foreach (var doc in docs)
{
var project = projects.FirstOrDefault(x => x.Id == doc.ProjectId);
if (project != null && (doc.FileName == project.NavigationDocumentName || doc.FileName == project.ParametersDocumentName))
{
continue;
}
await _documentFullSearch.AddOrUpdateAsync(doc);
}
}
private async Task UpdateDocumentUpdateInfoCache(Document document)
{
var cacheKey = $"DocumentUpdateInfo{document.ProjectId}#{document.Name}#{document.LanguageCode}#{document.Version}";

@ -32,5 +32,12 @@ namespace Volo.Docs.Admin
{
return _documentAdminAppService.PullAsync(input);
}
[HttpPost]
[Route("Reindex")]
public Task ReindexAsync()
{
return _documentAdminAppService.ReindexAsync();
}
}
}

@ -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; }
}
}

@ -1,3 +1,4 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Volo.Abp.Application.Services;
@ -14,5 +15,9 @@ namespace Volo.Docs.Documents
Task<DocumentParametersDto> GetParametersAsync(GetParametersDocumentInput input);
Task<DocumentResourceDto> GetResourceAsync(GetDocumentResourceInput input);
Task<List<DocumentSearchOutput>> SearchAsync(DocumentSearchInput input);
Task<bool> FullSearchEnabledAsync();
}
}

@ -5,8 +5,10 @@ using System.Threading.Tasks;
using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Newtonsoft.Json;
using Volo.Abp.Caching;
using Volo.Docs.Documents.FullSearch.Elastic;
using Volo.Docs.Projects;
namespace Volo.Docs.Documents
@ -20,6 +22,9 @@ namespace Volo.Docs.Documents
protected IDistributedCache<DocumentResourceDto> ResourceCache { get; }
protected IDistributedCache<DocumentUpdateInfo> DocumentUpdateCache { get; }
protected IHostEnvironment HostEnvironment { get; }
private readonly IDocumentFullSearch _documentFullSearch;
private readonly DocsElasticSearchOptions _docsElasticSearchOptions;
public DocumentAppService(
IProjectRepository projectRepository,
IDocumentRepository documentRepository,
@ -27,7 +32,9 @@ namespace Volo.Docs.Documents
IDistributedCache<LanguageConfig> languageCache,
IDistributedCache<DocumentResourceDto> resourceCache,
IDistributedCache<DocumentUpdateInfo> documentUpdateCache,
IHostEnvironment hostEnvironment)
IHostEnvironment hostEnvironment,
IDocumentFullSearch documentFullSearch,
IOptions<DocsElasticSearchOptions> docsElasticSearchOptions)
{
_projectRepository = projectRepository;
_documentRepository = documentRepository;
@ -36,6 +43,8 @@ namespace Volo.Docs.Documents
ResourceCache = resourceCache;
DocumentUpdateCache = documentUpdateCache;
HostEnvironment = hostEnvironment;
_documentFullSearch = documentFullSearch;
_docsElasticSearchOptions = docsElasticSearchOptions.Value;
}
public virtual async Task<DocumentWithDetailsDto> GetAsync(GetDocumentInput input)
@ -124,6 +133,27 @@ namespace Volo.Docs.Documents
);
}
public async Task<List<DocumentSearchOutput>> SearchAsync(DocumentSearchInput input)
{
var project = await _projectRepository.GetAsync(input.ProjectId);
var esDocs = await _documentFullSearch.SearchAsync(input.Context, project.Id, input.LanguageCode, input.Version);
return esDocs.Select(esDoc => new DocumentSearchOutput//TODO: auto map
{
Name = esDoc.Name,
FileName = esDoc.FileName,
Version = esDoc.Version,
LanguageCode = esDoc.LanguageCode,
Highlight = esDoc.Highlight
}).ToList();
}
public async Task<bool> FullSearchEnabledAsync()
{
return await Task.FromResult(_docsElasticSearchOptions.Enable);
}
public async Task<DocumentParametersDto> GetParametersAsync(GetParametersDocumentInput input)
{
var project = await _projectRepository.GetAsync(input.ProjectId);

@ -0,0 +1,7 @@
namespace Volo.Docs
{
public static class DocsDomainErrorCodes
{
public const string ElasticSearchNotEnabled = "Volo.Docs.Domain:010001";
}
}

@ -18,6 +18,7 @@
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Http" Version="3.1.0" />
<PackageReference Include="Octokit" Version="0.36.0" />
<PackageReference Include="NEST" Version="7.5.1" />
</ItemGroup>
<ItemGroup>

@ -1,10 +1,14 @@
using System;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Volo.Abp;
using Volo.Abp.Domain;
using Volo.Abp.Localization;
using Volo.Abp.Modularity;
using Volo.Abp.Threading;
using Volo.Abp.VirtualFileSystem;
using Volo.Docs.Documents;
using Volo.Docs.Documents.FullSearch.Elastic;
using Volo.Docs.FileSystem.Documents;
using Volo.Docs.GitHub.Documents;
using Volo.Docs.Localization;
@ -43,5 +47,17 @@ namespace Volo.Docs
client.Timeout = TimeSpan.FromMilliseconds(15000);
});
}
public override void OnApplicationInitialization(ApplicationInitializationContext context)
{
using (var scope = context.ServiceProvider.CreateScope())
{
if (scope.ServiceProvider.GetRequiredService<IOptions<DocsElasticSearchOptions>>().Value.Enable)
{
var documentFullSearch = scope.ServiceProvider.GetRequiredService<IDocumentFullSearch>();
AsyncHelper.RunSync(() => documentFullSearch.CreateIndexIfNeededAsync());
}
}
}
}
}

@ -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,189 @@
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;
}
public async Task CreateIndexIfNeededAsync(CancellationToken cancellationToken = default)
{
CheckEsEnabled();
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)
{
CheckEsEnabled();
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)
{
CheckEsEnabled();
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)
{
CheckEsEnabled();
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;
}
}
protected void CheckEsEnabled()
{
if (!_options.Enable)
{
throw new BusinessException(DocsDomainErrorCodes.ElasticSearchNotEnabled);
}
}
}
}

@ -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();
}
}

@ -17,6 +17,8 @@
"NavigationDocumentNotFound": "This version does not have a navigation document!",
"DocumentNotFoundInSelectedLanguage": "Document in the language you wanted is not found. Document in the default language is shown.",
"FilterTopics": "Filter topics",
"FullSearch": "Search in documents",
"Volo.Docs.Domain:010001": "Elastic search is not enabled.",
"MultipleVersionDocumentInfo": "This document has multiple versions. Select the options best fit for you.",
"New": "New",
"Upd": "Upd",

@ -17,6 +17,8 @@
"NavigationDocumentNotFound": "这个版本没有导航文件!",
"DocumentNotFoundInSelectedLanguage": "本文档不适用于所选语言, 将以默认语言显示文档.",
"FilterTopics": "过滤主题",
"FullSearch": "搜索文档",
"Volo.Docs.Domain:010001": "Elastic search未启用.",
"New": "新文档",
"Upd": "更新",
"NewExplanation": "在最近两周内创建.",

@ -17,6 +17,7 @@
"NavigationDocumentNotFound": "這個版本沒有導覽文件!",
"DocumentNotFoundInSelectedLanguage": "本文件不適用於所選語系,將以預設語系顯示.",
"FilterTopics": "過濾主題",
"FullSearch": "搜索文件",
"New": "新文檔",
"Upd": "更新",
"NewExplanation": "在最近兩周內創建.",

@ -1,4 +1,5 @@
using System.Threading.Tasks;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Volo.Abp;
using Volo.Abp.AspNetCore.Mvc;
@ -46,6 +47,20 @@ namespace Volo.Docs.Documents
return DocumentAppService.GetResourceAsync(input);
}
[HttpPost]
[Route("search")]
public Task<List<DocumentSearchOutput>> SearchAsync(DocumentSearchInput input)
{
return DocumentAppService.SearchAsync(input);
}
[HttpGet]
[Route("full-search-enabled")]
public Task<bool> FullSearchEnabledAsync()
{
return DocumentAppService.FullSearchEnabledAsync();
}
[HttpGet]
[Route("parameters")]
public Task<DocumentParametersDto> GetParametersAsync(GetParametersDocumentInput input)

@ -1,4 +1,4 @@
using System;
using System;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
@ -61,6 +61,7 @@ namespace Volo.Docs
options.Conventions.AddPageRoute("/Documents/Project/Index", routePrefix + "{projectName}");
options.Conventions.AddPageRoute("/Documents/Project/Index", routePrefix + "{languageCode}/{projectName}");
options.Conventions.AddPageRoute("/Documents/Project/Index", routePrefix + "{languageCode}/{projectName}/{version}/{*documentName}");
options.Conventions.AddPageRoute("/Documents/Search", routePrefix + "search/{languageCode}/{projectName}/{version}/{*keyword}");
});
context.Services.AddAutoMapperObjectMapper<DocsWebModule>();

@ -135,18 +135,32 @@
}
</div>
<div class="docs-version mb-4">
<div class="docs-version">
<div class="version-select">
<div class="input-group">
<div class="input-group-prepend">
<label class="input-group-text"><i class="fa fa-filter"></i></label>
</div>
<input class="form-control" type="search" placeholder="@L["FilterTopics"].Value" aria-label="Filter">
<input class="form-control" id="filter" type="search" data-search-url="@Model." placeholder="@L["FilterTopics"].Value" aria-label="Filter">
</div>
</div>
</div>
@if (Model.FullSearchEnabled)
{
<div class="docs-version mb-4">
<div class="version-select">
<div class="input-group">
<div class="input-group-prepend">
<label class="input-group-text"><i class="fa fa-filter"></i></label>
</div>
<input class="form-control" id="fullsearch" type="search" data-fullsearch-url="/search/@Model.LanguageCode/@Model.ProjectName/@Model.Version/" placeholder="@L["FullSearch"].Value" aria-label="Filter">
</div>
</div>
</div>
}
@if (Model.Navigation == null || !Model.Navigation.HasChildItems)
{

@ -64,6 +64,8 @@ namespace Volo.Docs.Pages.Documents.Project
public DocumentParametersDto DocumentPreferences { get; set; }
public DocumentRenderParameters UserPreferences { get; set; } = new DocumentRenderParameters();
public bool FullSearchEnabled { get; set; }
private readonly IDocumentAppService _documentAppService;
private readonly IDocumentToHtmlConverterFactory _documentToHtmlConverterFactory;
@ -106,6 +108,7 @@ namespace Volo.Docs.Pages.Documents.Project
{
DocumentsUrlPrefix = _uiOptions.RoutePrefix;
ShowProjectsCombobox = _uiOptions.ShowProjectsCombobox;
FullSearchEnabled = await _documentAppService.FullSearchEnabledAsync();
try
{

@ -1,4 +1,4 @@
(function ($) {
(function ($) {
$(function () {
var initNavigationFilter = function (navigationContainerId) {
@ -56,13 +56,19 @@
});
};
$(".docs-page .docs-tree-list input[type='search']").keyup(function (e) {
$("#filter").keyup(function (e) {
filterDocumentItems(e.target.value);
if (e.key === "Enter") {
gotoFilteredDocumentIfThereIsOnlyOne();
}
});
$("#fullsearch").keyup(function (e) {
if (e.key === "Enter") {
window.open($(this).data("fullsearch-url") + this.value);
}
});
};
var initAnchorTags = function (container) {

@ -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,73 @@
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)
{
if (!await _documentAppService.FullSearchEnabledAsync())
{
return RedirectToPage("Index");
}
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…
Cancel
Save