using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Examine; using Examine.Search; using Lucene.Net.QueryParsers.Classic; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Models.ContentEditing; using Umbraco.Cms.Infrastructure.Examine; using Umbraco.Cms.Web.Common.Attributes; using Umbraco.Extensions; using Constants = Umbraco.Cms.Core.Constants; using SearchResult = Umbraco.Cms.Core.Models.ContentEditing.SearchResult; namespace Umbraco.Cms.Web.BackOffice.Controllers { [PluginController(Constants.Web.Mvc.BackOfficeApiArea)] public class ExamineManagementController : UmbracoAuthorizedJsonController { private readonly IExamineManager _examineManager; private readonly ILogger _logger; private readonly IIndexDiagnosticsFactory _indexDiagnosticsFactory; private readonly IAppPolicyCache _runtimeCache; private readonly IIndexRebuilder _indexRebuilder; public ExamineManagementController( IExamineManager examineManager, ILogger logger, IIndexDiagnosticsFactory indexDiagnosticsFactory, AppCaches appCaches, IIndexRebuilder indexRebuilder) { _examineManager = examineManager; _logger = logger; _indexDiagnosticsFactory = indexDiagnosticsFactory; _runtimeCache = appCaches.RuntimeCache; _indexRebuilder = indexRebuilder; } /// /// Get the details for indexers /// /// public IEnumerable GetIndexerDetails() => _examineManager.Indexes .Select(index => CreateModel(index)) .OrderBy(examineIndexModel => examineIndexModel.Name.TrimEnd("Indexer")); /// /// Get the details for searchers /// /// public IEnumerable GetSearcherDetails() { var model = new List( _examineManager.RegisteredSearchers.Select(searcher => new ExamineSearcherModel { Name = searcher.Name }) .OrderBy(x => x.Name.TrimEnd("Searcher"))); //order by name , but strip the "Searcher" from the end if it exists return model; } public ActionResult GetSearchResults(string searcherName, string query, int pageIndex = 0, int pageSize = 20) { query = query.Trim(); if (query.IsNullOrWhiteSpace()) { return SearchResults.Empty(); } var msg = ValidateSearcher(searcherName, out var searcher); if (!msg.IsSuccessStatusCode()) return msg; ISearchResults results; // NativeQuery will work for a single word/phrase too (but depends on the implementation) the lucene one will work. try { results = searcher .CreateQuery() .NativeQuery(query) .Execute(QueryOptions.SkipTake(pageSize * pageIndex, pageSize)); } catch (ParseException) { // will occur if the query parser cannot parse this (i.e. starts with a *) return SearchResults.Empty(); } var pagedResults = results.Skip(pageIndex * pageSize); return new SearchResults { TotalRecords = results.TotalItemCount, Results = pagedResults.Select(x => new SearchResult { Id = x.Id, Score = x.Score, Values = x.AllValues.OrderBy(y => y.Key).ToDictionary(y => y.Key, y => y.Value) }) }; } /// /// Check if the index has been rebuilt /// /// /// /// /// This is kind of rudimentary since there's no way we can know that the index has rebuilt, we /// have a listener for the index op complete so we'll just check if that key is no longer there in the runtime cache /// public ActionResult PostCheckRebuildIndex(string indexName) { var validate = ValidateIndex(indexName, out var index); if (!validate.IsSuccessStatusCode()) { return validate; } validate = ValidatePopulator(index); if (!validate.IsSuccessStatusCode()) { return validate; } var cacheKey = "temp_indexing_op_" + indexName; var found = _runtimeCache.Get(cacheKey); //if its still there then it's not done return found != null ? null : CreateModel(index); } /// /// Rebuilds the index /// /// /// public IActionResult PostRebuildIndex(string indexName) { var validate = ValidateIndex(indexName, out var index); if (!validate.IsSuccessStatusCode()) return validate; validate = ValidatePopulator(index); if (!validate.IsSuccessStatusCode()) return validate; _logger.LogInformation("Rebuilding index '{IndexName}'", indexName); //remove it in case there's a handler there already index.IndexOperationComplete -= Indexer_IndexOperationComplete; //now add a single handler index.IndexOperationComplete += Indexer_IndexOperationComplete; try { var cacheKey = "temp_indexing_op_" + index.Name; //put temp val in cache which is used as a rudimentary way to know when the indexing is done _runtimeCache.Insert(cacheKey, () => "tempValue", TimeSpan.FromMinutes(5)); _indexRebuilder.RebuildIndex(indexName); return new OkResult(); } catch (Exception ex) { //ensure it's not listening index.IndexOperationComplete -= Indexer_IndexOperationComplete; _logger.LogError(ex, "An error occurred rebuilding index"); var response = new ConflictObjectResult("The index could not be rebuilt at this time, most likely there is another thread currently writing to the index. Error: {ex}"); HttpContext.SetReasonPhrase("Could Not Rebuild"); return response; } } private ExamineIndexModel CreateModel(IIndex index) { var indexName = index.Name; var indexDiag = _indexDiagnosticsFactory.Create(index); var isHealth = indexDiag.IsHealthy(); var properties = new Dictionary { ["DocumentCount"] = indexDiag.GetDocumentCount(), ["FieldCount"] = indexDiag.GetFieldNames().Count(), }; foreach (KeyValuePair p in indexDiag.Metadata) { properties[p.Key] = p.Value; } var indexerModel = new ExamineIndexModel { Name = indexName, HealthStatus = isHealth.Success ? (isHealth.Result ?? "Healthy") : (isHealth.Result ?? "Unhealthy"), ProviderProperties = properties, CanRebuild = _indexRebuilder.CanRebuild(index.Name) }; return indexerModel; } private ActionResult ValidateSearcher(string searcherName, out ISearcher searcher) { //try to get the searcher from the indexes if (_examineManager.TryGetIndex(searcherName, out IIndex index)) { searcher = index.Searcher; return new OkResult(); } //if we didn't find anything try to find it by an explicitly declared searcher if (_examineManager.TryGetSearcher(searcherName, out searcher)) { return new OkResult(); } var response1 = new BadRequestObjectResult($"No searcher found with name = {searcherName}"); HttpContext.SetReasonPhrase("Searcher Not Found"); return response1; } private ActionResult ValidatePopulator(IIndex index) { if (_indexRebuilder.CanRebuild(index.Name)) { return new OkResult(); } var response = new BadRequestObjectResult($"The index {index.Name} cannot be rebuilt because it does not have an associated {typeof(IIndexPopulator)}"); HttpContext.SetReasonPhrase("Index cannot be rebuilt"); return response; } private ActionResult ValidateIndex(string indexName, out IIndex index) { index = null; if (_examineManager.TryGetIndex(indexName, out index)) { //return Ok! return new OkResult(); } var response = new BadRequestObjectResult($"No index found with name = {indexName}"); HttpContext.SetReasonPhrase("Index Not Found"); return response; } private void Indexer_IndexOperationComplete(object sender, EventArgs e) { var indexer = (IIndex)sender; _logger.LogDebug("Logging operation completed for index {IndexName}", indexer.Name); //ensure it's not listening anymore indexer.IndexOperationComplete -= Indexer_IndexOperationComplete; _logger.LogInformation($"Rebuilding index '{indexer.Name}' done."); var cacheKey = "temp_indexing_op_" + indexer.Name; _runtimeCache.Clear(cacheKey); } } }