Managing multiple indexes

--HG--
branch : 1.x
extra : rebase_source : 930ebcb3cc0c50b45beac39a81658ace562c8e04
This commit is contained in:
Sebastien Ros
2013-03-04 16:00:55 -08:00
parent 6c24f4a57f
commit 2015448c44
15 changed files with 256 additions and 82 deletions

View File

@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Autofac;
@@ -45,16 +46,30 @@ namespace Orchard.Tests.Modules.Indexing {
_provider = _container.Resolve<IIndexProvider>();
}
private string[] Indexes() {
return new DirectoryInfo(Path.Combine(_basePath, "Sites", "My Site", "Indexes")).GetDirectories().Select(d => d.Name).ToArray();
private IEnumerable<string> Indexes() {
return _provider.List();
}
[Test]
public void IndexProviderShouldCreateNewIndex() {
Assert.That(Indexes().Length, Is.EqualTo(0));
Assert.That(Indexes().Count(), Is.EqualTo(0));
_provider.CreateIndex("default");
Assert.That(Indexes().Length, Is.EqualTo(1));
Assert.That(Indexes().Count(), Is.EqualTo(1));
}
[Test]
public void IndexProviderShouldCreateMultipleIndexesAndListThem() {
Assert.That(Indexes().Count(), Is.EqualTo(0));
_provider.CreateIndex("default");
_provider.CreateIndex("search");
_provider.CreateIndex("admin");
Assert.That(Indexes().Count(), Is.EqualTo(3));
Assert.That(Indexes().Contains("default"));
Assert.That(Indexes().Contains("search"));
Assert.That(Indexes().Contains("admin"));
}
[Test]
@@ -69,25 +84,25 @@ namespace Orchard.Tests.Modules.Indexing {
[Test]
public void IndexProviderShouldDeleteExistingIndex() {
Assert.That(Indexes().Length, Is.EqualTo(0));
Assert.That(Indexes().Count(), Is.EqualTo(0));
_provider.CreateIndex("default");
Assert.That(Indexes().Length, Is.EqualTo(1));
Assert.That(Indexes().Count(), Is.EqualTo(1));
_provider.DeleteIndex("default");
Assert.That(Indexes().Length, Is.EqualTo(0));
Assert.That(Indexes().Count(), Is.EqualTo(0));
}
[Test]
public void IndexProviderShouldListExistingIndexes() {
Assert.That(Indexes().Length, Is.EqualTo(0));
Assert.That(Indexes().Count(), Is.EqualTo(0));
_provider.CreateIndex("default");
Assert.That(Indexes().Length, Is.EqualTo(1));
Assert.That(Indexes()[0], Is.EqualTo("default"));
Assert.That(Indexes().Count(), Is.EqualTo(1));
Assert.That(Indexes().ElementAt(0), Is.EqualTo("default"));
_provider.CreateIndex("foo");
Assert.That(Indexes().Length, Is.EqualTo(2));
Assert.That(Indexes().Count(), Is.EqualTo(2));
}
[Test]

View File

@@ -80,6 +80,10 @@ namespace Lucene.Services {
return new DirectoryInfo(_appDataFolder.MapPath(_appDataFolder.Combine(_basePath, indexName))).Exists;
}
public IEnumerable<string> List() {
return _appDataFolder.ListDirectories(_basePath).Select(Path.GetFileNameWithoutExtension);
}
public bool IsEmpty(string indexName) {
if ( !Exists(indexName) ) {
return true;
@@ -131,7 +135,7 @@ namespace Lucene.Services {
}
public void Store(string indexName, IEnumerable<LuceneDocumentIndex> indexDocuments) {
if (indexDocuments.AsQueryable().Count() == 0) {
if (!indexDocuments.Any()) {
return;
}

View File

@@ -9,7 +9,7 @@ namespace Orchard.Indexing {
public void GetNavigation(NavigationBuilder builder) {
builder.Add(T("Settings"),
menu => menu.Add(T("Search Index"), "5", item => item.Action("Index", "Admin", new {area = "Orchard.Indexing"})
menu => menu.Add(T("Indexes"), "5", item => item.Action("Index", "Admin", new {area = "Orchard.Indexing"})
.Permission(StandardPermissions.SiteOwner)));
}
}

View File

@@ -1,19 +1,25 @@
using System;
using System.Linq;
using System.Web.Mvc;
using Orchard.Indexing.Services;
using Orchard.Localization;
using Orchard.Logging;
using Orchard.Security;
using Orchard.UI.Notify;
using Orchard.Indexing.ViewModels;
using Orchard.UI.Notify;
using Orchard.Utility.Extensions;
namespace Orchard.Indexing.Controllers {
public class AdminController : Controller {
private readonly IIndexingService _indexingService;
private const string DefaultIndexName = "Search";
private readonly IIndexManager _indexManager;
public AdminController(IIndexingService indexingService, IOrchardServices services) {
public AdminController(
IIndexingService indexingService,
IOrchardServices services,
IIndexManager indexManager) {
_indexingService = indexingService;
_indexManager = indexManager;
Services = services;
T = NullLocalizer.Instance;
Logger = NullLogger.Instance;
@@ -24,38 +30,93 @@ namespace Orchard.Indexing.Controllers {
public ILogger Logger { get; set; }
public ActionResult Index() {
var viewModel = new IndexViewModel();
var viewModel = new IndexViewModel {
IndexEntries = Enumerable.Empty<IndexEntry>(),
IndexProvider = _indexManager.GetSearchIndexProvider()
};
try {
viewModel.IndexEntry = _indexingService.GetIndexEntry(DefaultIndexName);
if (viewModel.IndexEntry == null)
Services.Notifier.Information(T("There is no search index to manage for this site."));
}
catch(Exception e) {
Logger.Error(e, "Search index couldn't be read.");
Services.Notifier.Information(T("The index might be corrupted. If you can't recover click on Rebuild."));
if (_indexManager.HasIndexProvider()) {
viewModel.IndexEntries = _indexManager.GetSearchIndexProvider().List().Select(x => {
try {
return _indexingService.GetIndexEntry(x);
}
catch(Exception e) {
Logger.Error(e, "Index couldn't be read: " + x);
return new IndexEntry {
IndexName = x,
IndexingStatus = IndexingStatus.Unavailable
};
}
});
}
// Services.Notifier.Information(T("The index might be corrupted. If you can't recover click on Rebuild."));
return View(viewModel);
}
[HttpPost]
public ActionResult Update() {
public ActionResult Create() {
if (!Services.Authorizer.Authorize(StandardPermissions.SiteOwner, T("Not allowed to manage the search index.")))
return new HttpUnauthorizedResult();
_indexingService.UpdateIndex(DefaultIndexName);
return View("Create", String.Empty);
}
[HttpPost, ActionName("Create")]
public ActionResult CreatePOST(string id) {
if (!Services.Authorizer.Authorize(StandardPermissions.SiteOwner, T("Not allowed to manage the search index.")))
return new HttpUnauthorizedResult();
var provider = _indexManager.GetSearchIndexProvider();
if (String.IsNullOrWhiteSpace(id) || id.ToSafeName() != id) {
Services.Notifier.Error(T("Invalid index name."));
return View("Create", id);
}
if (provider.Exists(id)) {
Services.Notifier.Error(T("An index with the same name already exists: {0}", id));
return View("Create", id);
}
try {
provider.CreateIndex(id);
Services.Notifier.Information(T("Index named {0} created succeffully", id));
}
catch(Exception e) {
Services.Notifier.Error(T("An error occured while creating the index: {0}", id));
Logger.Error("An error occured while creatign the index " + id, e);
return View("Create", id);
}
return RedirectToAction("Index");
}
[HttpPost]
public ActionResult Rebuild() {
public ActionResult Update(string id) {
if (!Services.Authorizer.Authorize(StandardPermissions.SiteOwner, T("Not allowed to manage the search index.")))
return new HttpUnauthorizedResult();
_indexingService.RebuildIndex(DefaultIndexName);
_indexingService.UpdateIndex(id);
return RedirectToAction("Index");
}
[HttpPost]
public ActionResult Rebuild(string id) {
if (!Services.Authorizer.Authorize(StandardPermissions.SiteOwner, T("Not allowed to manage the search index.")))
return new HttpUnauthorizedResult();
_indexingService.RebuildIndex(id);
return RedirectToAction("Index");
}
[HttpPost]
public ActionResult Delete(string id) {
if (!Services.Authorizer.Authorize(StandardPermissions.SiteOwner, T("Not allowed to manage the search index.")))
return new HttpUnauthorizedResult();
_indexingService.DeleteIndex(id);
return RedirectToAction("Index");
}

View File

@@ -19,6 +19,11 @@
<UpgradeBackupLocation>
</UpgradeBackupLocation>
<OldToolsVersion>4.0</OldToolsVersion>
<UseIISExpress>false</UseIISExpress>
<IISExpressSSLPort />
<IISExpressAnonymousAuthentication />
<IISExpressWindowsAuthentication />
<IISExpressUseClassicPipelineMode />
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
@@ -102,6 +107,9 @@
<ItemGroup>
<Content Include="web.config" />
</ItemGroup>
<ItemGroup>
<Content Include="Views\Admin\Create.cshtml" />
</ItemGroup>
<PropertyGroup>
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">10.0</VisualStudioVersion>
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>

View File

@@ -11,6 +11,7 @@ namespace Orchard.Indexing.Services {
}
public interface IIndexingService : IDependency {
void DeleteIndex(string indexName);
void RebuildIndex(string indexName);
void UpdateIndex(string indexName);
IndexEntry GetIndexEntry(string indexName);

View File

@@ -4,7 +4,8 @@ namespace Orchard.Indexing.Services {
public enum IndexingStatus {
Rebuilding,
Updating,
Idle
Idle,
Unavailable
}
public interface IIndexStatisticsProvider : IDependency {
DateTime GetLastIndexedUtc(string indexName);

View File

@@ -1,6 +1,7 @@
namespace Orchard.Indexing.Services {
public interface IIndexingTaskExecutor : IDependency {
bool DeleteIndex(string indexName);
bool RebuildIndex(string indexName);
bool UpdateIndexBatch(string indexName);
}
}

View File

@@ -1,6 +1,7 @@
using System.Collections.Generic;
using Orchard.Localization;
using Orchard.UI.Notify;
using Orchard.Validation;
namespace Orchard.Indexing.Services
{
@@ -28,12 +29,14 @@ namespace Orchard.Indexing.Services
public Localizer T { get; set; }
public void RebuildIndex(string indexName) {
Argument.ThrowIfNullOrEmpty(indexName, "indexName");
if (!_indexManager.HasIndexProvider()) {
Services.Notifier.Warning(T("There is no search index to rebuild."));
Services.Notifier.Warning(T("There is no index to rebuild."));
return;
}
if(_indexingTaskExecutor.DeleteIndex(indexName)) {
if(_indexingTaskExecutor.RebuildIndex(indexName)) {
Services.Notifier.Information(T("The index {0} has been rebuilt.", indexName));
UpdateIndex(indexName);
}
@@ -42,7 +45,24 @@ namespace Orchard.Indexing.Services
}
}
public void DeleteIndex(string indexName) {
Argument.ThrowIfNullOrEmpty(indexName, "indexName");
if (!_indexManager.HasIndexProvider()) {
Services.Notifier.Warning(T("There is no index to delete."));
return;
}
if (_indexingTaskExecutor.DeleteIndex(indexName)) {
Services.Notifier.Information(T("The index {0} has been deleted.", indexName));
}
else {
Services.Notifier.Warning(T("The index {0} could not be deleted. It might already be in use, please try again later.", indexName));
}
}
public void UpdateIndex(string indexName) {
Argument.ThrowIfNullOrEmpty(indexName, "indexName");
foreach(var handler in _indexNotifierHandlers) {
handler.UpdateIndex(indexName);
@@ -52,6 +72,8 @@ namespace Orchard.Indexing.Services
}
IndexEntry IIndexingService.GetIndexEntry(string indexName) {
Argument.ThrowIfNullOrEmpty(indexName, "indexName");
var provider = _indexManager.GetSearchIndexProvider();
if (provider == null)
return null;

View File

@@ -9,18 +9,26 @@ namespace Orchard.Indexing.Services {
[UsedImplicitly]
public class IndexingBackgroundTask : IBackgroundTask {
private readonly IIndexNotifierHandler _indexNotifierHandler;
private const string SearchIndexName = "Search";
private readonly IIndexManager _indexManager;
public IndexingBackgroundTask(
IIndexNotifierHandler indexNotifierHandler) {
IIndexNotifierHandler indexNotifierHandler,
IIndexManager indexManager) {
_indexNotifierHandler = indexNotifierHandler;
_indexManager = indexManager;
Logger = NullLogger.Instance;
}
public ILogger Logger { get; set; }
public void Sweep() {
_indexNotifierHandler.UpdateIndex(SearchIndexName);
if (!_indexManager.HasIndexProvider()) {
return;
}
foreach (var index in _indexManager.GetSearchIndexProvider().List()) {
_indexNotifierHandler.UpdateIndex(index);
}
}
}
}

View File

@@ -58,6 +58,17 @@ namespace Orchard.Indexing.Services {
public ILogger Logger { get; set; }
public bool RebuildIndex(string indexName) {
if (DeleteIndex(indexName)) {
var searchProvider = _indexManager.GetSearchIndexProvider();
searchProvider.CreateIndex(indexName);
return true;
}
return false;
}
public bool DeleteIndex(string indexName) {
ILockFile lockFile = null;
var settingsFilename = GetSettingsFileName(indexName);

View File

@@ -1,7 +1,9 @@
using Orchard.Indexing.Services;
using System.Collections.Generic;
using Orchard.Indexing.Services;
namespace Orchard.Indexing.ViewModels {
public class IndexViewModel {
public IndexEntry IndexEntry { get; set;}
public IIndexProvider IndexProvider { get; set; }
public IEnumerable<IndexEntry> IndexEntries { get; set;}
}
}

View File

@@ -0,0 +1,13 @@
@model string
@{ Layout.Title = T("Create Index").ToString(); }
@using(Html.BeginFormAntiForgeryPost()) {
<fieldset>
@Html.TextBox("id", Model, new { @class= "textMedium"})
<span class="hint">@T("The technical name of the index to create. Must contain letters and numbers only.")</span>
</fieldset>
<fieldset>
<button type="submit">@T("Create")</button>
</fieldset>
}

View File

@@ -1,50 +1,72 @@
@model Orchard.Indexing.ViewModels.IndexViewModel
@using Orchard.Indexing.Services;
@{ Layout.Title = T("Settings").ToString(); }
@{ Layout.Title = T("Indexes").ToString(); }
@using (Html.BeginForm("update", "admin", FormMethod.Post, new {area = "Orchard.Indexing"})) {
<fieldset>
<legend>@T("Search Index")</legend>
<ol class="decimal">
@if (Model.IndexEntry == null) {
@if (Model.IndexEntries.Any()) {
using (Html.BeginFormAntiForgeryPost()) {
<fieldset class="contentItems bulk-items">
<ul>
@foreach (var index in Model.IndexEntries) {
<li>
<div class="summary">
<div class="properties">
@*<input type="checkbox" value="@contentItem.Id" name="itemIds"/>*@
<h2>@index.IndexName</h2>
<div class="metadata">
<ul class="pageStatus">
<li>@T("{0} document(s)", index.DocumentCount)</li>
@if (index.LastUpdateUtc != DateTime.MinValue) {
<li>&nbsp;|&nbsp;</li>
<li>@T("Updated {0}", Display.DateTimeRelative(dateTimeUtc: index.LastUpdateUtc))</li>
}
</ul>
</div>
</div>
<div class="related">
@Html.ActionLink(T("Update").Text, "Update", "Admin", new {area = "Orchard.Indexing", id = index.IndexName} , new { itemprop = "UnsafeUrl"}) |
@Html.ActionLink(T("Rebuild").Text, "Rebuild", "Admin", new {area = "Orchard.Indexing", id = index.IndexName} , new { itemprop = "UnsafeUrl"}) |
@Html.ActionLink(T("Delete").Text, "Delete", "Admin", new {area = "Orchard.Indexing", id = index.IndexName} , new { itemprop = "RemoveUrl UnsafeUrl"})
</div>
<div class="primary">
@if (index.LastUpdateUtc == DateTime.MinValue) {
<p>@T("The search index has not been built yet.")</p>
}
<li>@T("There is currently no search index")</li>
} else if (Model.IndexEntry.LastUpdateUtc == DateTime.MinValue) {
<li>@T("The search index has not been built yet.")</li>
} else {
if (Model.IndexEntry.DocumentCount == 0) {
<li>@T("The search index does not contain any document.")</li>
} else {
<li>@T("The search index contains {0} document(s).", Model.IndexEntry.DocumentCount)</li>
}
if (!Model.IndexEntry.Fields.Any()) {
<li>@T("The search index does not contain any field.")</li>
} else {
<li>@T("The search index contains the following fields: {0}.", string.Join(T(", ").Text, Model.IndexEntry.Fields))</li>
}
<li>@T("The search index was last updated {0}.", Display.DateTimeRelative(dateTimeUtc: Model.IndexEntry.LastUpdateUtc))</li>
switch(Model.IndexEntry.IndexingStatus) {
case IndexingStatus.Rebuilding:
<li>@T("The indexing process is currently being rebuilt.");</li>
break;
case IndexingStatus.Updating:
<li>@T("The indexing process is currently being updated.");</li>
break;
@if (index.Fields == null || !index.Fields.Any()) {
<p>@T("No fields.")</p>
}
else {
<p>@T("Fields: {0}.", string.Join(T(", ").Text, index.Fields))</p>
}
@switch (index.IndexingStatus) {
case IndexingStatus.Rebuilding:
<p>@T("The indexing process is currently being rebuilt.");</p>
break;
case IndexingStatus.Updating:
<p>@T("The indexing process is currently being updated.");</p>
break;
case IndexingStatus.Unavailable:
<p>@T("The index is currently not available. Try to rebuild it.")</p>
break;
}
</div>
</div>
</li>
}
}
</ol>
<label>@T("Update the search index now:")</label><button type="submit" title="@T("Update the search index.")" class="primaryAction">@T("Update")</button>
@Html.AntiForgeryTokenOrchard()
</ul>
</fieldset>
}
@using (Html.BeginForm("rebuild", "admin", FormMethod.Post, new {area = "Orchard.Search"})) {
<fieldset>
<label>@T("Rebuild the search index for a fresh start:")</label>
<button type="submit" title="@T("Rebuild the search index.")">@T("Rebuild")</button>
@Html.AntiForgeryTokenOrchard()
</fieldset>
}
}
} else
{
<div>@T("There are no indexes.")</div>
}
@if (Model.IndexProvider != null) {
<fieldset>
@Html.ActionLink(T("Create").Text, "Create", "Admin", new {area = "Orchard.Indexing", @class = "button"})
</fieldset>
}

View File

@@ -12,6 +12,11 @@ namespace Orchard.Indexing {
/// </summary>
bool Exists(string name);
/// <summary>
/// Lists all existing indexes
/// </summary>
IEnumerable<string> List();
/// <summary>
/// Deletes an existing index
/// </summary>