Implementing revised indexing loop design

--HG--
branch : indexing
This commit is contained in:
Sebastien Ros
2011-03-04 11:05:19 -08:00
parent 32efbc19cc
commit 6d3dffd77e
15 changed files with 306 additions and 275 deletions

View File

@@ -26,7 +26,6 @@ using Orchard.Indexing.Models;
using Orchard.Indexing.Services;
using Orchard.Logging;
using Orchard.Security;
using Orchard.Services;
using Orchard.Tasks.Indexing;
using Orchard.Tests.FileSystems.AppData;
using Orchard.Tests.Stubs;
@@ -84,7 +83,6 @@ namespace Orchard.Tests.Modules.Indexing {
builder.RegisterType<StubExtensionManager>().As<IExtensionManager>();
builder.RegisterType<DefaultLockFileManager>().As<ILockFileManager>();
builder.RegisterInstance<IClock>(_clock = new StubClock());
// setting up a ShellSettings instance
_shellSettings = new ShellSettings { Name = "My Site" };
@@ -126,9 +124,6 @@ namespace Orchard.Tests.Modules.Indexing {
public void IndexShouldBeEmptyWhenThereIsNoContent() {
_indexNotifier.UpdateIndex(IndexName);
Assert.That(_provider.NumDocs(IndexName), Is.EqualTo(0));
Assert.That(_logger.LogEntries.Count(), Is.EqualTo(2));
Assert.That(_logger.LogEntries, Has.Some.Matches<LogEntry>(entry => entry.LogFormat == "Rebuild index started"));
Assert.That(_logger.LogEntries, Has.Some.Matches<LogEntry>(entry => entry.LogFormat == "Index update requested, nothing to do"));
}
[Test]
@@ -145,9 +140,6 @@ namespace Orchard.Tests.Modules.Indexing {
_indexNotifier.UpdateIndex(IndexName);
Assert.That(_provider.NumDocs(IndexName), Is.EqualTo(0));
Assert.That(_logger.LogEntries.Count(), Is.EqualTo(2));
Assert.That(_logger.LogEntries, Has.Some.Matches<LogEntry>(entry => entry.LogFormat == "Rebuild index started"));
Assert.That(_logger.LogEntries, Has.Some.Matches<LogEntry>(entry => entry.LogFormat == "Index update requested, nothing to do"));
}
[Test]
@@ -165,9 +157,6 @@ namespace Orchard.Tests.Modules.Indexing {
_indexNotifier.UpdateIndex(IndexName);
Assert.That(_provider.NumDocs(IndexName), Is.EqualTo(0));
Assert.That(_logger.LogEntries.Count(), Is.EqualTo(2));
Assert.That(_logger.LogEntries, Has.Some.Matches<LogEntry>(entry => entry.LogFormat == "Rebuild index started"));
Assert.That(_logger.LogEntries, Has.Some.Matches<LogEntry>(entry => entry.LogFormat == "Index update requested, nothing to do"));
}
[Test]
@@ -177,10 +166,6 @@ namespace Orchard.Tests.Modules.Indexing {
_indexNotifier.UpdateIndex(IndexName);
Assert.That(_provider.NumDocs(IndexName), Is.EqualTo(1));
Assert.That(_logger.LogEntries.Count(), Is.EqualTo(3));
Assert.That(_logger.LogEntries, Has.Some.Matches<LogEntry>(entry => entry.LogFormat == "Rebuild index started"));
Assert.That(_logger.LogEntries, Has.Some.Matches<LogEntry>(entry => entry.LogFormat == "Processing {0} indexing tasks"));
Assert.That(_logger.LogEntries, Has.Some.Matches<LogEntry>(entry => entry.LogFormat == "Added content items to index: {0}"));
}
[Test]
@@ -188,82 +173,47 @@ namespace Orchard.Tests.Modules.Indexing {
_contentManager.Create<Thing>(ThingDriver.ContentTypeName).Text = "Lorem ipsum";
_indexNotifier.UpdateIndex(IndexName);
Assert.That(_provider.NumDocs(IndexName), Is.EqualTo(1));
Assert.That(_logger.LogEntries, Has.Some.Matches<LogEntry>(entry => entry.LogFormat == "Rebuild index started"));
_logger.Clear();
// there should be nothing done
_indexNotifier.UpdateIndex(IndexName);
_contentManager.Create<Thing>(ThingDriver.ContentTypeName).Text = "Lorem ipsum";
_indexNotifier.UpdateIndex(IndexName);
Assert.That(_provider.NumDocs(IndexName), Is.EqualTo(2));
Assert.That(_logger.LogEntries, Has.None.Matches<LogEntry>(entry => entry.LogFormat == "Rebuild index started"));
}
[Test]
public void IndexingTaskExecutorShouldBeReEntrant() {
public void IndexingTaskExecutorShouldNotBeReEntrant() {
ILockFile lockFile = null;
_lockFileManager.TryAcquireLock("Sites/My Site/Search.settings.xml.lock", ref lockFile);
using (lockFile) {
_indexNotifier.UpdateIndex(IndexName);
Assert.That(_logger.LogEntries.Count, Is.EqualTo(1));
Assert.That(_logger.LogEntries, Has.Some.Matches<LogEntry>(entry => entry.LogFormat == "Index was requested but was already running"));
Assert.That(_logger.LogEntries, Has.Some.Matches<LogEntry>(entry => entry.LogFormat == "Index was requested but is already running"));
}
_logger.LogEntries.Clear();
_indexNotifier.UpdateIndex(IndexName);
Assert.That(_logger.LogEntries, Has.None.Matches<LogEntry>(entry => entry.LogFormat == "Index was requested but was already running"));
Assert.That(_logger.LogEntries, Has.None.Matches<LogEntry>(entry => entry.LogFormat == "Index was requested but is already running"));
}
[Test]
public void ShouldUpdateTheIndexWhenContentIsUnPublished() {
_contentManager.Create<Thing>(ThingDriver.ContentTypeName).Text = "Lorem ipsum";
_clock.Advance(TimeSpan.FromSeconds(1));
_indexNotifier.UpdateIndex(IndexName);
Assert.That(_provider.NumDocs(IndexName), Is.EqualTo(1));
Assert.That(_logger.LogEntries, Has.Some.Matches<LogEntry>(entry => entry.LogFormat == "Rebuild index started"));
_logger.Clear();
var content = _contentManager.Create<Thing>(ThingDriver.ContentTypeName);
content.Text = "Lorem ipsum";
_clock.Advance(TimeSpan.FromSeconds(1));
_indexNotifier.UpdateIndex(IndexName);
Assert.That(_provider.NumDocs(IndexName), Is.EqualTo(2));
Assert.That(_logger.LogEntries, Has.None.Matches<LogEntry>(entry => entry.LogFormat == "Rebuild index started"));
_clock.Advance(TimeSpan.FromSeconds(1));
_contentManager.Unpublish(content.ContentItem);
_clock.Advance(TimeSpan.FromSeconds(1));
_indexNotifier.UpdateIndex(IndexName);
Assert.That(_provider.NumDocs(IndexName), Is.EqualTo(1));
Assert.That(_logger.LogEntries, Has.None.Matches<LogEntry>(entry => entry.LogFormat == "Rebuild index started"));
}
[Test]
public void ShouldRemoveFromIndexEvenIfPublishedAndUnpublishedInTheSameSecond() {
// This test is to ensure that when a task is created, all previous tasks for the same content item
// are also removed, and thus that multiple tasks don't conflict while updating the index
_contentManager.Create<Thing>(ThingDriver.ContentTypeName).Text = "Lorem ipsum";
_clock.Advance(TimeSpan.FromSeconds(1));
_indexNotifier.UpdateIndex(IndexName);
Assert.That(_provider.NumDocs(IndexName), Is.EqualTo(1));
Assert.That(_logger.LogEntries, Has.Some.Matches<LogEntry>(entry => entry.LogFormat == "Rebuild index started"));
_logger.Clear();
var content = _contentManager.Create<Thing>(ThingDriver.ContentTypeName);
content.Text = "Lorem ipsum";
_indexNotifier.UpdateIndex(IndexName);
Assert.That(_provider.NumDocs(IndexName), Is.EqualTo(2));
Assert.That(_logger.LogEntries, Has.None.Matches<LogEntry>(entry => entry.LogFormat == "Rebuild index started"));
_contentManager.Unpublish(content.ContentItem);
_indexNotifier.UpdateIndex(IndexName);
Assert.That(_provider.NumDocs(IndexName), Is.EqualTo(1));
Assert.That(_logger.LogEntries, Has.None.Matches<LogEntry>(entry => entry.LogFormat == "Rebuild index started"));
}
#region Stubs

View File

@@ -7,7 +7,6 @@ using NUnit.Framework;
using Orchard.Environment.Configuration;
using Orchard.FileSystems.AppData;
using Orchard.Indexing;
using Orchard.Indexing.Services;
using Orchard.Tests.FileSystems.AppData;
namespace Orchard.Tests.Modules.Indexing {
@@ -198,18 +197,6 @@ namespace Orchard.Tests.Modules.Indexing {
Assert.That(searchBuilder.Get(3).ContentItemId, Is.EqualTo(3));
}
[Test]
public void ProviderShouldStoreSettings() {
_provider.CreateIndex("default");
Assert.That(_provider.GetLastIndexUtc("default"), Is.Null);
_provider.SetLastIndexUtc("default", new DateTime(2010, 1, 1, 1, 1, 1, 1));
Assert.That(_provider.GetLastIndexUtc("default"), Is.EqualTo(new DateTime(2010, 1, 1, 1, 1, 1, 0)));
_provider.SetLastIndexUtc("default", new DateTime(1901, 1, 1, 1, 1, 1, 1));
Assert.That(_provider.GetLastIndexUtc("default"), Is.EqualTo(LuceneIndexProvider.DefaultMinDateTime));
}
[Test]
public void IsEmptyShouldBeTrueForNoneExistingIndexes() {
_provider.IsEmpty("dummy");
@@ -238,9 +225,7 @@ namespace Orchard.Tests.Modules.Indexing {
[Test]
public void IsDirtyShouldBeTrueWhenIndexIsModified() {
IDocumentIndex doc;
doc = _provider.New(1);
IDocumentIndex doc = _provider.New(1);
doc.Add("foo", "value");
Assert.That(doc.IsDirty, Is.True);
@@ -281,7 +266,7 @@ namespace Orchard.Tests.Modules.Indexing {
Assert.That(searchBuilder.Get(11).ContentItemId, Is.EqualTo(11));
Assert.That(searchBuilder.Get(111).ContentItemId, Is.EqualTo(111));
_provider.Delete("default", new int[] {1, 11, 111 });
_provider.Delete("default", new [] {1, 11, 111 });
Assert.That(searchBuilder.Get(1), Is.Null);
Assert.That(searchBuilder.Get(11), Is.Null);

View File

@@ -9,7 +9,6 @@ using Lucene.Net.Documents;
using Lucene.Net.Index;
using Lucene.Net.Search;
using Lucene.Net.Store;
using System.Xml.Linq;
using Orchard.Environment.Configuration;
using Orchard.FileSystems.AppData;
using Orchard.Indexing;
@@ -29,8 +28,6 @@ namespace Lucene.Services {
private readonly Analyzer _analyzer ;
private readonly string _basePath;
public static readonly DateTime DefaultMinDateTime = new DateTime(1980, 1, 1);
public static readonly string Settings = "Settings";
public static readonly string LastIndexUtc = "LastIndexedUtc";
public LuceneIndexProvider(IAppDataFolder appDataFolder, ShellSettings shellSettings) {
_appDataFolder = appDataFolder;
@@ -123,11 +120,6 @@ namespace Lucene.Services {
public void DeleteIndex(string indexName) {
new DirectoryInfo(_appDataFolder.MapPath(_appDataFolder.Combine(_basePath, indexName)))
.Delete(true);
var settingsFileName = GetSettingsFileName(indexName);
if (File.Exists(settingsFileName)) {
File.Delete(settingsFileName);
}
}
public void Store(string indexName, IDocumentIndex indexDocument) {
@@ -206,36 +198,6 @@ namespace Lucene.Services {
return new LuceneSearchBuilder(GetDirectory(indexName)) { Logger = Logger };
}
public DateTime? GetLastIndexUtc(string indexName) {
var settingsFileName = GetSettingsFileName(indexName);
if (!File.Exists(settingsFileName))
return null;
return DateTime.Parse(XDocument.Load(settingsFileName).Descendants(LastIndexUtc).First().Value);
}
public void SetLastIndexUtc(string indexName, DateTime lastIndexUtc) {
if ( lastIndexUtc < DefaultMinDateTime ) {
lastIndexUtc = DefaultMinDateTime;
}
XDocument doc;
var settingsFileName = GetSettingsFileName(indexName);
if ( !File.Exists(settingsFileName) ) {
EnsureDirectoryExists();
doc = new XDocument(
new XElement(Settings,
new XElement(LastIndexUtc, lastIndexUtc.ToString("s"))));
}
else {
doc = XDocument.Load(settingsFileName);
doc.Element(Settings).Element(LastIndexUtc).Value = lastIndexUtc.ToString("s");
}
doc.Save(settingsFileName);
}
public IEnumerable<string> GetFields(string indexName) {
if ( !Exists(indexName) ) {
return Enumerable.Empty<string>();
@@ -250,9 +212,5 @@ namespace Lucene.Services {
reader.Close();
}
}
private string GetSettingsFileName(string indexName) {
return _appDataFolder.MapPath(_appDataFolder.Combine(_basePath, indexName + ".settings.xml"));
}
}
}

View File

@@ -1,5 +1,4 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Orchard.Commands;
using Orchard.ContentManagement;
@@ -35,7 +34,7 @@ namespace Orchard.Indexing.Commands {
[CommandName("index update")]
[CommandHelp("index update\r\n\t" + "Updates the search index")]
public string Update() {
_indexingService.UpdateIndex();
_indexingService.UpdateIndex(SearchIndexName);
return T("Index is now being updated...").Text;
}
@@ -43,8 +42,8 @@ namespace Orchard.Indexing.Commands {
[CommandName("index rebuild")]
[CommandHelp("index rebuild \r\n\t" + "Rebuilds the search index")]
public string Rebuild() {
_indexingService.RebuildIndex();
_indexingService.UpdateIndex();
_indexingService.RebuildIndex(SearchIndexName);
_indexingService.UpdateIndex(SearchIndexName);
return T("Index is now being rebuilt...").Text;
}

View File

@@ -8,6 +8,7 @@ using Orchard.Indexing.ViewModels;
namespace Orchard.Indexing.Controllers {
public class AdminController : Controller {
private readonly IIndexingService _indexingService;
private const string DefaultIndexName = "Search";
public AdminController(IIndexingService indexingService, IOrchardServices services) {
_indexingService = indexingService;
@@ -19,7 +20,7 @@ namespace Orchard.Indexing.Controllers {
public Localizer T { get; set; }
public ActionResult Index() {
var viewModel = new IndexViewModel { IndexEntry = _indexingService.GetIndexEntry() };
var viewModel = new IndexViewModel { IndexEntry = _indexingService.GetIndexEntry(DefaultIndexName) };
if (viewModel.IndexEntry == null)
Services.Notifier.Information(T("There is no search index to manage for this site."));
@@ -32,7 +33,7 @@ namespace Orchard.Indexing.Controllers {
if (!Services.Authorizer.Authorize(StandardPermissions.SiteOwner, T("Not allowed to manage the search index.")))
return new HttpUnauthorizedResult();
_indexingService.UpdateIndex();
_indexingService.UpdateIndex(DefaultIndexName);
return RedirectToAction("Index");
}
@@ -42,8 +43,8 @@ namespace Orchard.Indexing.Controllers {
if (!Services.Authorizer.Authorize(StandardPermissions.SiteOwner, T("Not allowed to manage the search index.")))
return new HttpUnauthorizedResult();
_indexingService.RebuildIndex();
_indexingService.UpdateIndex();
_indexingService.RebuildIndex(DefaultIndexName);
_indexingService.UpdateIndex(DefaultIndexName);
return RedirectToAction("Index");
}

View File

@@ -0,0 +1,57 @@
using System;
using System.Linq;
using System.Xml.Linq;
namespace Orchard.Indexing.Models
{
public enum IndexingMode {
Rebuild,
Update
}
public class IndexSettings {
public IndexingMode Mode { get; set; }
public int LastIndexedId { get; set; }
public int LastContentId { get; set; }
public DateTime LastIndexedUtc { get; set; }
public static readonly string TagSettings = "Settings";
public static readonly string TagMode = "Mode";
public static readonly string TagLastIndexedId = "LastIndexedId";
public static readonly string TagLastContentId = "LastContentId";
public static readonly string TagLastIndexedUtc = "LastIndexedUtc";
public IndexSettings() {
Mode = IndexingMode.Rebuild;
LastIndexedId = 0;
LastContentId = 0;
LastIndexedUtc = DateTime.MinValue;
}
public static IndexSettings Parse(string content) {
var doc = XDocument.Parse(content);
try {
return new IndexSettings {
Mode = (IndexingMode) Enum.Parse(typeof (IndexingMode), doc.Descendants(TagMode).First().Value),
LastIndexedId = Int32.Parse(doc.Descendants(TagLastIndexedId).First().Value),
LastContentId = Int32.Parse(doc.Descendants(TagLastContentId).First().Value),
LastIndexedUtc = DateTime.Parse(doc.Descendants(TagLastIndexedUtc).First().Value)
};
}
catch {
return new IndexSettings();
}
}
public override string ToString() {
return new XDocument(
new XElement(TagSettings,
new XElement(TagMode, Mode),
new XElement(TagLastIndexedId, LastIndexedId),
new XElement(TagLastContentId, LastContentId),
new XElement(TagLastIndexedUtc, LastIndexedUtc.ToString("s"))
)).ToString();
}
}
}

View File

@@ -41,6 +41,8 @@
<HintPath>..\..\..\..\lib\aspnetmvc\System.Web.Mvc.dll</HintPath>
</Reference>
<Reference Include="System.Web" />
<Reference Include="System.XML" />
<Reference Include="System.Xml.Linq" />
</ItemGroup>
<ItemGroup>
<Content Include="Module.txt" />
@@ -55,6 +57,10 @@
<Compile Include="Handlers\InfosetFieldIndexingHandler.cs" />
<Compile Include="Models\IndexingTask.cs" />
<Compile Include="Models\IndexingTaskRecord.cs" />
<Compile Include="Models\IndexSettings.cs">
<SubType>Code</SubType>
</Compile>
<Compile Include="Services\IIndexStatisticsProvider.cs" />
<Compile Include="Services\IndexServiceNotificationProvider.cs" />
<Compile Include="Services\IndexingBackgroundTask.cs" />
<Compile Include="Services\IndexingTaskExecutor.cs" />

View File

@@ -5,13 +5,14 @@ namespace Orchard.Indexing.Services {
public class IndexEntry {
public string IndexName { get; set; }
public int DocumentCount { get; set; }
public DateTime? LastUpdateUtc { get; set; }
public DateTime LastUpdateUtc { get; set; }
public IEnumerable<string> Fields { get; set; }
public IndexingStatus IndexingStatus { get; set; }
}
public interface IIndexingService : IDependency {
void RebuildIndex();
void UpdateIndex();
IndexEntry GetIndexEntry();
void RebuildIndex(string indexName);
void UpdateIndex(string indexName);
IndexEntry GetIndexEntry(string indexName);
}
}

View File

@@ -0,0 +1,13 @@
using System;
namespace Orchard.Indexing.Services {
public enum IndexingStatus {
Rebuilding,
Updating,
Idle
}
public interface IIndexStatisticsProvider : IDependency {
DateTime GetLastIndexedUtc(string indexName);
IndexingStatus GetIndexingStatus(string indexName);
}
}

View File

@@ -1,59 +1,65 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using Orchard.Localization;
using Orchard.Localization.Services;
using Orchard.UI.Notify;
namespace Orchard.Indexing.Services
{
public class IndexingService : IIndexingService {
private const string SearchIndexName = "Search";
private readonly IIndexManager _indexManager;
private readonly IEnumerable<IIndexNotifierHandler> _indexNotifierHandlers;
private readonly IIndexStatisticsProvider _indexStatisticsProvider;
public IndexingService(IOrchardServices services, IIndexManager indexManager, IEnumerable<IIndexNotifierHandler> indexNotifierHandlers, ICultureManager cultureManager) {
public IndexingService(
IOrchardServices services,
IIndexManager indexManager,
IEnumerable<IIndexNotifierHandler> indexNotifierHandlers,
IIndexStatisticsProvider indexStatisticsProvider) {
Services = services;
_indexManager = indexManager;
_indexNotifierHandlers = indexNotifierHandlers;
_indexStatisticsProvider = indexStatisticsProvider;
T = NullLocalizer.Instance;
}
public IOrchardServices Services { get; set; }
public Localizer T { get; set; }
void IIndexingService.RebuildIndex() {
void IIndexingService.RebuildIndex(string indexName) {
if (!_indexManager.HasIndexProvider()) {
Services.Notifier.Warning(T("There is no search index to rebuild."));
return;
}
var searchProvider = _indexManager.GetSearchIndexProvider();
if (searchProvider.Exists(SearchIndexName))
searchProvider.DeleteIndex(SearchIndexName);
if (searchProvider.Exists(indexName))
searchProvider.DeleteIndex(indexName);
searchProvider.CreateIndex(SearchIndexName); // or just reset the updated date and let the background process recreate the index
searchProvider.CreateIndex(indexName); // or just reset the updated date and let the background process recreate the index
Services.Notifier.Information(T("The search index has been rebuilt."));
Services.Notifier.Information(T("The index {0} has been rebuilt.", indexName));
}
void IIndexingService.UpdateIndex() {
void IIndexingService.UpdateIndex(string indexName) {
foreach(var handler in _indexNotifierHandlers) {
handler.UpdateIndex(SearchIndexName);
handler.UpdateIndex(indexName);
}
Services.Notifier.Information(T("The search index has been updated."));
}
IndexEntry IIndexingService.GetIndexEntry() {
IndexEntry IIndexingService.GetIndexEntry(string indexName) {
var provider = _indexManager.GetSearchIndexProvider();
if (provider == null)
return null;
return new IndexEntry {
IndexName = SearchIndexName,
DocumentCount = provider.NumDocs(SearchIndexName),
Fields = provider.GetFields(SearchIndexName),
LastUpdateUtc = provider.GetLastIndexUtc(SearchIndexName)
IndexName = indexName,
DocumentCount = provider.NumDocs(indexName),
Fields = provider.GetFields(indexName),
LastUpdateUtc = _indexStatisticsProvider.GetLastIndexedUtc(indexName),
IndexingStatus = _indexStatisticsProvider.GetIndexingStatus(indexName)
};
}
}

View File

@@ -11,7 +11,6 @@ using Orchard.Indexing.Models;
using Orchard.Indexing.Settings;
using Orchard.Logging;
using Orchard.Services;
using Orchard.Tasks.Indexing;
namespace Orchard.Indexing.Services {
/// <summary>
@@ -22,35 +21,33 @@ namespace Orchard.Indexing.Services {
/// and singleton locks would not be shared accross those two.
/// </remarks>
[UsedImplicitly]
public class IndexingTaskExecutor : IIndexNotifierHandler {
private readonly IClock _clock;
public class IndexingTaskExecutor : IIndexNotifierHandler, IIndexStatisticsProvider {
private readonly IRepository<IndexingTaskRecord> _repository;
private IIndexProvider _indexProvider;
private readonly IIndexManager _indexManager;
private readonly IIndexingTaskManager _indexingTaskManager;
private readonly IContentManager _contentManager;
private readonly IAppDataFolder _appDataFolder;
private readonly ShellSettings _shellSettings;
private readonly ILockFileManager _lockFileManager;
private readonly IClock _clock;
private const int ContentItemsPerLoop = 100;
private IndexingStatus _indexingStatus = IndexingStatus.Idle;
public IndexingTaskExecutor(
IClock clock,
IRepository<IndexingTaskRecord> repository,
IIndexManager indexManager,
IIndexingTaskManager indexingTaskManager,
IContentManager contentManager,
IAppDataFolder appDataFolder,
ShellSettings shellSettings,
ILockFileManager lockFileManager) {
_clock = clock;
ILockFileManager lockFileManager,
IClock clock) {
_repository = repository;
_indexManager = indexManager;
_indexingTaskManager = indexingTaskManager;
_contentManager = contentManager;
_appDataFolder = appDataFolder;
_shellSettings = shellSettings;
_lockFileManager = lockFileManager;
_clock = clock;
Logger = NullLogger.Instance;
}
@@ -61,120 +58,192 @@ namespace Orchard.Indexing.Services {
var settingsFilename = GetSettingsFileName(indexName);
var lockFilename = settingsFilename + ".lock";
// acquire a lock file on the index
if (!_lockFileManager.TryAcquireLock(lockFilename, ref lockFile)) {
Logger.Information("Index was requested but was already running");
Logger.Information("Index was requested but is already running");
return;
}
using (lockFile) {
using (lockFile)
{
if (!_indexManager.HasIndexProvider()) {
return;
}
// load index settings to know what is the current state of indexing
var indexSettings = LoadSettings(indexName);
_indexProvider = _indexManager.GetSearchIndexProvider();
var updateIndexDocuments = new List<IDocumentIndex>();
var addedContentItemIds = new List<string>();
DateTime? lastIndexUtc;
// Do we need to rebuild the full index (first time module is used, or rebuild index requested) ?
if (_indexProvider.IsEmpty(indexName)) {
Logger.Information("Rebuild index started");
// mark current last task, as we should process older ones (in case of rebuild index only)
lastIndexUtc = _indexingTaskManager.GetLastTaskDateTime();
// get every existing content item to index it
foreach (var contentItem in _contentManager.Query(VersionOptions.Published).List()) {
try {
// skip items which are not indexed
var settings = GetTypeIndexingSettings(contentItem);
if (!settings.Included)
continue;
var documentIndex = _indexProvider.New(contentItem.Id);
_contentManager.Index(contentItem, documentIndex);
if (documentIndex.IsDirty) {
updateIndexDocuments.Add(documentIndex);
addedContentItemIds.Add(contentItem.Id.ToString());
}
}
catch (Exception ex) {
Logger.Warning(ex, "Unable to index content item #{0} during rebuild", contentItem.Id);
}
}
}
else {
// retrieve last processed index time
lastIndexUtc = _indexProvider.GetLastIndexUtc(indexName);
}
_indexProvider.SetLastIndexUtc(indexName, _clock.UtcNow);
// retrieve not yet processed tasks
var taskRecords = lastIndexUtc == null
? _repository.Fetch(x => true).ToArray()
: _repository.Fetch(x => x.CreatedUtc >= lastIndexUtc).ToArray(); // CreatedUtc and lastIndexUtc might be equal if a content item is created in a background task
// nothing to do ?)))
if (taskRecords.Length + updateIndexDocuments.Count == 0) {
Logger.Information("Index update requested, nothing to do");
return;
}
Logger.Information("Processing {0} indexing tasks", taskRecords.Length);
// should the index be rebuilt
if (!_indexProvider.Exists(indexName)) {
_indexProvider.CreateIndex(indexName);
indexSettings = new IndexSettings();
}
// process Delete tasks
try {
var deleteIds = taskRecords.Where(t => t.Action == IndexingTaskRecord.Delete).Select(t => t.ContentItemRecord.Id).ToArray();
if (deleteIds.Length > 0) {
_indexProvider.Delete(indexName, deleteIds);
Logger.Information("Deleted content items from index: {0}", String.Join(", ", deleteIds));
}
}
catch (Exception ex) {
Logger.Warning(ex, "An error occured while removing a document from the index");
}
// execute indexing commands by batch of [ContentItemsPerLoop] content items
for (; ; ){
var addToIndex = new List<IDocumentIndex>();
var deleteFromIndex = new List<int>();
// process Update tasks
foreach (var taskRecord in taskRecords.Where(t => t.Action == IndexingTaskRecord.Update)) {
var task = new IndexingTask(_contentManager, taskRecord);
// Rebuilding the index ?
if (indexSettings.Mode == IndexingMode.Rebuild) {
Logger.Information("Rebuilding index");
_indexingStatus = IndexingStatus.Rebuilding;
// skip items which are not indexed
var settings = GetTypeIndexingSettings(task.ContentItem);
if (!settings.Included)
continue;
// store the last inserted task
var lastIndexId = _repository
.Fetch(x => true)
.OrderByDescending(x => x.Id)
.Select(x => x.Id)
.FirstOrDefault();
try {
var documentIndex = _indexProvider.New(task.ContentItem.Id);
_contentManager.Index(task.ContentItem, documentIndex);
if (!addedContentItemIds.Contains(task.ContentItem.Id.ToString()) && documentIndex.IsDirty) {
updateIndexDocuments.Add(documentIndex);
// load all content items
var contentItemIds = _contentManager
.Query(VersionOptions.Published)
.List()
.Where(x => x.Id > indexSettings.LastContentId)
.OrderBy(x => x.Id)
.Select(x => x.Id)
.Distinct()
.Take(ContentItemsPerLoop)
.ToArray();
indexSettings.LastIndexedId = lastIndexId;
// if no more elements to index, switch to update mode
if (contentItemIds.Length == 0) {
indexSettings.Mode = IndexingMode.Update;
}
}
catch (Exception ex) {
Logger.Warning(ex, "Unable to process indexing task #{0}", taskRecord.Id);
}
}
foreach (var id in contentItemIds) {
try {
IDocumentIndex documentIndex = ExtractDocumentIndex(id);
if (updateIndexDocuments.Count > 0) {
if (documentIndex != null && documentIndex.IsDirty) {
addToIndex.Add(documentIndex);
}
// store the last processed element
indexSettings.LastContentId = contentItemIds.LastOrDefault();
}
catch (Exception ex) {
Logger.Warning(ex, "Unable to index content item #{0} during rebuild", id);
}
}
}
if (indexSettings.Mode == IndexingMode.Update) {
Logger.Information("Updating index");
_indexingStatus = IndexingStatus.Updating;
// load next content items to index, by filtering and ordering on the task id
var lastIndexId = _repository
.Fetch(x => x.Id > indexSettings.LastIndexedId)
.OrderByDescending(x => x.Id)
.Select(x => x.Id)
.FirstOrDefault();
var contentItemIds = _repository
.Fetch(x => x.Id > indexSettings.LastIndexedId)
.OrderBy(x => x.Id)
.Take(ContentItemsPerLoop)
.Select(x => x.ContentItemRecord.Id)
.Distinct() // don't process the same content item twice
.ToArray();
indexSettings.LastIndexedId = lastIndexId;
foreach (var id in contentItemIds) {
try {
IDocumentIndex documentIndex = ExtractDocumentIndex(id);
if (documentIndex == null) {
deleteFromIndex.Add(id);
}
else if (documentIndex.IsDirty) {
addToIndex.Add(documentIndex);
}
}
catch (Exception ex) {
Logger.Warning(ex, "Unable to index content item #{0} during rebuild", id);
}
}
}
// save current state of the index
indexSettings.LastIndexedUtc = _clock.UtcNow;
_appDataFolder.CreateFile(settingsFilename, indexSettings.ToString());
if (deleteFromIndex.Count == 0 && addToIndex.Count == 0) {
// nothing more to do
_indexingStatus = IndexingStatus.Idle;
return;
}
// save new and updated documents to the index
try {
_indexProvider.Store(indexName, updateIndexDocuments);
Logger.Information("Added content items to index: {0}", String.Join(", ", addedContentItemIds));
if (addToIndex.Count > 0) {
_indexProvider.Store(indexName, addToIndex);
Logger.Information("Added content items to index: {0}", addToIndex.Count);
}
}
catch (Exception ex) {
Logger.Warning(ex, "An error occured while adding a document to the index");
}
// removing documents from the index
try {
if (deleteFromIndex.Count > 0) {
_indexProvider.Delete(indexName, deleteFromIndex);
Logger.Information("Added content items to index: {0}", addToIndex.Count);
}
}
catch (Exception ex) {
Logger.Warning(ex, "An error occured while removing a document from the index");
}
}
}
}
/// <summary>
/// Loads the settings file or create a new default one if it doesn't exist
/// </summary>
public IndexSettings LoadSettings(string indexName) {
var indexSettings = new IndexSettings();
var settingsFilename = GetSettingsFileName(indexName);
if (_appDataFolder.FileExists(settingsFilename)) {
var content = _appDataFolder.ReadFile(settingsFilename);
indexSettings = IndexSettings.Parse(content);
}
return indexSettings;
}
/// <summary>
/// Creates a IDocumentIndex instance for a specific content item id. If the content
/// item is no more published, it returns null.
/// </summary>
private IDocumentIndex ExtractDocumentIndex(int id) {
var contentItem = _contentManager.Get(id, VersionOptions.Published);
// ignore deleted or unpublished items
if(contentItem == null || !contentItem.IsPublished()) {
return null;
}
// skip items from types which are not indexed
var settings = GetTypeIndexingSettings(contentItem);
if (!settings.Included)
return null;
var documentIndex = _indexProvider.New(contentItem.Id);
// call all handlers to add content to index
_contentManager.Index(contentItem, documentIndex);
return documentIndex;
}
static TypeIndexing GetTypeIndexingSettings(ContentItem contentItem) {
if (contentItem == null ||
contentItem.TypeDefinition == null ||
@@ -187,5 +256,14 @@ namespace Orchard.Indexing.Services {
private string GetSettingsFileName(string indexName) {
return _appDataFolder.Combine("Sites", _shellSettings.Name, indexName + ".settings.xml");
}
public DateTime GetLastIndexedUtc(string indexName) {
var indexSettings = LoadSettings(indexName);
return indexSettings.LastIndexedUtc;
}
public IndexingStatus GetIndexingStatus(string indexName) {
return _indexingStatus;
}
}
}

View File

@@ -30,8 +30,6 @@ namespace Orchard.Indexing.Services {
throw new ArgumentNullException("contentItem");
}
DeleteTasks(contentItem);
var taskRecord = new IndexingTaskRecord {
CreatedUtc = _clock.UtcNow,
ContentItemRecord = contentItem.Record,
@@ -57,19 +55,5 @@ namespace Orchard.Indexing.Services {
public DateTime GetLastTaskDateTime() {
return _repository.Table.Max(t => t.CreatedUtc) ?? new DateTime(1980, 1, 1);
}
/// <summary>
/// Removes existing tasks for the specified content item
/// </summary>
public void DeleteTasks(ContentItem contentItem) {
var tasks = _repository
.Fetch(x => x.ContentItemRecord.Id == contentItem.Id)
.ToArray();
foreach (var task in tasks) {
_repository.Delete(task);
}
_repository.Flush();
}
}
}

View File

@@ -1,4 +1,5 @@
@model Orchard.Indexing.ViewModels.IndexViewModel
@using Orchard.Indexing.Services;
@{ Layout.Title = T("Search Index Management").ToString(); }
@@ -6,7 +7,7 @@
<fieldset>
@if (Model.IndexEntry == null) {
<p>@T("There is currently no search index")</p>
} else if (Model.IndexEntry.LastUpdateUtc == null) {
} else if (Model.IndexEntry.LastUpdateUtc == DateTime.MinValue) {
<p>@T("The search index has not been built yet.")</p>
} else {
if (Model.IndexEntry.DocumentCount == 0) {
@@ -21,8 +22,17 @@
<p>@T("The search index contains the following fields: {0}.", string.Join(T(", ").Text, Model.IndexEntry.Fields))</p>
}
<p>@T("The search index was last updated {0}.", Display.DateTimeRelative(dateTimeUtc: Model.IndexEntry.LastUpdateUtc.Value))</p>
}
<p>@T("The search index was last updated {0}.", Display.DateTimeRelative(dateTimeUtc: Model.IndexEntry.LastUpdateUtc))</p>
switch(Model.IndexEntry.IndexingStatus) {
case IndexingStatus.Rebuilding:
@T("The indexing process is currently being rebuilt.");
break;
case IndexingStatus.Updating:
@T("The indexing process is currently being updated.");
break;
}
}
<p>@T("Update the search index now: ")<button type="submit" title="@T("Update the search index.")" class="primaryAction">@T("Update")</button></p>
@Html.AntiForgeryTokenOrchard()
</fieldset>

View File

@@ -1,5 +1,4 @@
using System;
using System.Collections.Generic;
using System.Collections.Generic;
namespace Orchard.Indexing {
public interface IIndexProvider : ISingletonDependency {
@@ -60,16 +59,6 @@ namespace Orchard.Indexing {
/// <returns>A search builder instance</returns>
ISearchBuilder CreateSearchBuilder(string indexName);
/// <summary>
/// Returns the date and time when the index was last processed, or null if the index doesn't exist
/// </summary>
DateTime? GetLastIndexUtc(string indexName);
/// <summary>
/// Sets the date and time when the index was last processed
/// </summary>
void SetLastIndexUtc(string indexName, DateTime lastIndexUtc);
/// <summary>
/// Returns every field available in the specified index
/// </summary>

View File

@@ -17,11 +17,5 @@ namespace Orchard.Tasks.Indexing {
/// Returns the Date Time of the last task created
/// </summary>
DateTime GetLastTaskDateTime();
/// <summary>
/// Deletes all indexing tasks assigned to a specific content item
/// </summary>
/// <param name="contentItem"></param>
void DeleteTasks(ContentItem contentItem);
}
}