mirror of
https://github.com/OrchardCMS/Orchard.git
synced 2025-10-21 03:14:10 +08:00
Complete refactoring on Indexing module
Changed Parse() to use lucene syntax Moved everything to Orchard.Indexing New filters for search --HG-- branch : dev
This commit is contained in:
@@ -0,0 +1,10 @@
|
||||
using System;
|
||||
|
||||
namespace Orchard.Indexing.Services {
|
||||
public interface IIndexingService : IDependency {
|
||||
bool HasIndexToManage { get; }
|
||||
void RebuildIndex();
|
||||
void UpdateIndex();
|
||||
DateTime GetIndexUpdatedUtc();
|
||||
}
|
||||
}
|
@@ -0,0 +1,59 @@
|
||||
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;
|
||||
|
||||
public IndexingService(IOrchardServices services, IIndexManager indexManager, IEnumerable<IIndexNotifierHandler> indexNotifierHandlers, ICultureManager cultureManager) {
|
||||
Services = services;
|
||||
_indexManager = indexManager;
|
||||
_indexNotifierHandlers = indexNotifierHandlers;
|
||||
T = NullLocalizer.Instance;
|
||||
}
|
||||
|
||||
public IOrchardServices Services { get; set; }
|
||||
public Localizer T { get; set; }
|
||||
|
||||
public bool HasIndexToManage {
|
||||
get { return _indexManager.HasIndexProvider(); }
|
||||
}
|
||||
|
||||
void IIndexingService.RebuildIndex() {
|
||||
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);
|
||||
|
||||
searchProvider.CreateIndex(SearchIndexName); // or just reset the updated date and let the background process recreate the index
|
||||
|
||||
Services.Notifier.Information(T("The search index has been rebuilt."));
|
||||
}
|
||||
|
||||
void IIndexingService.UpdateIndex() {
|
||||
|
||||
foreach(var handler in _indexNotifierHandlers) {
|
||||
handler.UpdateIndex(SearchIndexName);
|
||||
}
|
||||
|
||||
Services.Notifier.Information(T("The search index has been updated."));
|
||||
}
|
||||
|
||||
DateTime IIndexingService.GetIndexUpdatedUtc() {
|
||||
return !HasIndexToManage
|
||||
? DateTime.MinValue
|
||||
: _indexManager.GetSearchIndexProvider().GetLastIndexUtc(SearchIndexName);
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,28 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Web;
|
||||
|
||||
namespace Orchard.Indexing.Services {
|
||||
public interface IIndexSynLock : ISingletonDependency {
|
||||
object GetSynLock(string indexName);
|
||||
}
|
||||
|
||||
public class IndexSynLock : IIndexSynLock {
|
||||
private readonly Dictionary<string, object> _synLocks;
|
||||
private readonly object _synLock = new object();
|
||||
|
||||
public IndexSynLock() {
|
||||
_synLocks =new Dictionary<string, object>();
|
||||
}
|
||||
|
||||
public object GetSynLock(string indexName) {
|
||||
lock(_synLock) {
|
||||
if(!_synLocks.ContainsKey(indexName)) {
|
||||
_synLocks[indexName] = new object();
|
||||
}
|
||||
return _synLocks[indexName];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,26 @@
|
||||
using JetBrains.Annotations;
|
||||
using Orchard.Logging;
|
||||
using Orchard.Tasks;
|
||||
|
||||
namespace Orchard.Indexing.Services {
|
||||
/// <summary>
|
||||
/// Regularly fires IIndexNotifierHandler events
|
||||
/// </summary>
|
||||
[UsedImplicitly]
|
||||
public class IndexingBackgroundTask : IBackgroundTask {
|
||||
private readonly IIndexNotifierHandler _indexNotifierHandler;
|
||||
private const string SearchIndexName = "Search";
|
||||
|
||||
public IndexingBackgroundTask(
|
||||
IIndexNotifierHandler indexNotifierHandler) {
|
||||
_indexNotifierHandler = indexNotifierHandler;
|
||||
Logger = NullLogger.Instance;
|
||||
}
|
||||
|
||||
public ILogger Logger { get; set; }
|
||||
|
||||
public void Sweep() {
|
||||
_indexNotifierHandler.UpdateIndex(SearchIndexName);
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,147 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using JetBrains.Annotations;
|
||||
using Orchard.ContentManagement;
|
||||
using Orchard.Data;
|
||||
using Orchard.Indexing.Models;
|
||||
using Orchard.Logging;
|
||||
using Orchard.Services;
|
||||
using Orchard.Tasks.Indexing;
|
||||
|
||||
namespace Orchard.Indexing.Services {
|
||||
/// <summary>
|
||||
/// Contains the logic which is regularly executed to retrieve index information from multiple content handlers.
|
||||
/// </summary>
|
||||
[UsedImplicitly]
|
||||
public class IndexingTaskExecutor : IIndexNotifierHandler {
|
||||
private readonly IClock _clock;
|
||||
private readonly IRepository<IndexingTaskRecord> _repository;
|
||||
private IIndexProvider _indexProvider;
|
||||
private readonly IIndexManager _indexManager;
|
||||
private readonly IIndexingTaskManager _indexingTaskManager;
|
||||
private readonly IContentManager _contentManager;
|
||||
private readonly IIndexSynLock _indexSynLock;
|
||||
private const string SearchIndexName = "Search";
|
||||
|
||||
public IndexingTaskExecutor(
|
||||
IClock clock,
|
||||
IRepository<IndexingTaskRecord> repository,
|
||||
IIndexManager indexManager,
|
||||
IIndexingTaskManager indexingTaskManager,
|
||||
IContentManager contentManager,
|
||||
IIndexSynLock indexSynLock) {
|
||||
_clock = clock;
|
||||
_repository = repository;
|
||||
_indexManager = indexManager;
|
||||
_indexingTaskManager = indexingTaskManager;
|
||||
_contentManager = contentManager;
|
||||
_indexSynLock = indexSynLock;
|
||||
Logger = NullLogger.Instance;
|
||||
}
|
||||
|
||||
public ILogger Logger { get; set; }
|
||||
|
||||
public void UpdateIndex(string indexName) {
|
||||
var synLock = _indexSynLock.GetSynLock(SearchIndexName);
|
||||
|
||||
if ( !System.Threading.Monitor.TryEnter(synLock) ) {
|
||||
Logger.Information("Index was requested but was already running");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
if (!_indexManager.HasIndexProvider()) {
|
||||
return;
|
||||
}
|
||||
|
||||
_indexProvider = _indexManager.GetSearchIndexProvider();
|
||||
var updateIndexDocuments = new List<IDocumentIndex>();
|
||||
DateTime lastIndexing;
|
||||
|
||||
// Do we need to rebuild the full index (first time module is used, or rebuild index requested) ?
|
||||
if (_indexProvider.IsEmpty(SearchIndexName)) {
|
||||
Logger.Information("Rebuild index started");
|
||||
|
||||
// mark current last task, as we should process older ones (in case of rebuild index only)
|
||||
lastIndexing = _indexingTaskManager.GetLastTaskDateTime();
|
||||
|
||||
// get every existing content item to index it
|
||||
foreach (var contentItem in _contentManager.Query(VersionOptions.Published).List()) {
|
||||
try {
|
||||
var documentIndex = _indexProvider.New(contentItem.Id);
|
||||
|
||||
_contentManager.Index(contentItem, documentIndex);
|
||||
if(documentIndex.IsDirty) {
|
||||
updateIndexDocuments.Add(documentIndex);
|
||||
}
|
||||
}
|
||||
catch (Exception ex) {
|
||||
Logger.Warning(ex, "Unable to index content item #{0} during rebuild", contentItem.Id);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
else {
|
||||
// retrieve last processed index time
|
||||
lastIndexing = _indexProvider.GetLastIndexUtc(SearchIndexName);
|
||||
}
|
||||
|
||||
_indexProvider.SetLastIndexUtc(SearchIndexName, _clock.UtcNow);
|
||||
|
||||
// retrieve not yet processed tasks
|
||||
var taskRecords = _repository.Fetch(x => x.CreatedUtc > lastIndexing)
|
||||
.ToArray();
|
||||
|
||||
// nothing to do ?
|
||||
if (taskRecords.Length + updateIndexDocuments.Count == 0)
|
||||
return;
|
||||
|
||||
Logger.Information("Processing {0} indexing tasks", taskRecords.Length);
|
||||
|
||||
if (!_indexProvider.Exists(SearchIndexName)) {
|
||||
_indexProvider.CreateIndex(SearchIndexName);
|
||||
}
|
||||
|
||||
// process Delete tasks
|
||||
try {
|
||||
_indexProvider.Delete(SearchIndexName, taskRecords.Where(t => t.Action == IndexingTaskRecord.Delete).Select(t => t.Id));
|
||||
}
|
||||
catch (Exception ex) {
|
||||
Logger.Warning(ex, "An error occured while removing a document from the index");
|
||||
}
|
||||
|
||||
// process Update tasks
|
||||
foreach (var taskRecord in taskRecords.Where(t => t.Action == IndexingTaskRecord.Update)) {
|
||||
var task = new IndexingTask(_contentManager, taskRecord);
|
||||
|
||||
try {
|
||||
var documentIndex = _indexProvider.New(task.ContentItem.Id);
|
||||
|
||||
_contentManager.Index(task.ContentItem, documentIndex);
|
||||
if ( documentIndex.IsDirty ) {
|
||||
updateIndexDocuments.Add(documentIndex);
|
||||
}
|
||||
|
||||
}
|
||||
catch (Exception ex) {
|
||||
Logger.Warning(ex, "Unable to process indexing task #{0}", taskRecord.Id);
|
||||
}
|
||||
}
|
||||
|
||||
if (updateIndexDocuments.Count > 0) {
|
||||
try {
|
||||
_indexProvider.Store(SearchIndexName, updateIndexDocuments);
|
||||
}
|
||||
catch (Exception ex) {
|
||||
Logger.Warning(ex, "An error occured while adding a document to the index");
|
||||
}
|
||||
}
|
||||
}
|
||||
finally {
|
||||
System.Threading.Monitor.Exit(synLock);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,73 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using JetBrains.Annotations;
|
||||
using Orchard.ContentManagement;
|
||||
using Orchard.Data;
|
||||
using Orchard.Indexing.Models;
|
||||
using Orchard.Logging;
|
||||
using Orchard.Tasks.Indexing;
|
||||
using Orchard.Services;
|
||||
|
||||
namespace Orchard.Indexing.Services {
|
||||
[UsedImplicitly]
|
||||
public class IndexingTaskManager : IIndexingTaskManager {
|
||||
private readonly IRepository<IndexingTaskRecord> _repository;
|
||||
private readonly IClock _clock;
|
||||
|
||||
public IndexingTaskManager(
|
||||
IContentManager contentManager,
|
||||
IRepository<IndexingTaskRecord> repository,
|
||||
IClock clock) {
|
||||
_clock = clock;
|
||||
_repository = repository;
|
||||
Logger = NullLogger.Instance;
|
||||
}
|
||||
|
||||
public ILogger Logger { get; set; }
|
||||
|
||||
private void CreateTask(ContentItem contentItem, int action) {
|
||||
if ( contentItem == null ) {
|
||||
throw new ArgumentNullException("contentItem");
|
||||
}
|
||||
|
||||
DeleteTasks(contentItem);
|
||||
|
||||
var taskRecord = new IndexingTaskRecord {
|
||||
CreatedUtc = _clock.UtcNow,
|
||||
ContentItemRecord = contentItem.Record,
|
||||
Action = action
|
||||
};
|
||||
|
||||
_repository.Create(taskRecord);
|
||||
|
||||
}
|
||||
|
||||
public void CreateUpdateIndexTask(ContentItem contentItem) {
|
||||
|
||||
CreateTask(contentItem, IndexingTaskRecord.Update);
|
||||
Logger.Information("Indexing task created for [{0}:{1}]", contentItem.ContentType, contentItem.Id);
|
||||
}
|
||||
|
||||
public void CreateDeleteIndexTask(ContentItem contentItem) {
|
||||
|
||||
CreateTask(contentItem, IndexingTaskRecord.Delete);
|
||||
Logger.Information("Deleting index task created for [{0}:{1}]", contentItem.ContentType, contentItem.Id);
|
||||
}
|
||||
|
||||
public DateTime GetLastTaskDateTime() {
|
||||
return _repository.Table.Max(t => t.CreatedUtc) ?? DateTime.MinValue;
|
||||
}
|
||||
|
||||
/// <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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,233 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Lucene.Net.Analysis;
|
||||
using Lucene.Net.Analysis.Standard;
|
||||
using Lucene.Net.Documents;
|
||||
using Lucene.Net.Index;
|
||||
using Lucene.Net.Store;
|
||||
using Orchard.Environment.Configuration;
|
||||
using Orchard.FileSystems.AppData;
|
||||
using Orchard.Indexing.Models;
|
||||
using Orchard.Logging;
|
||||
using System.Xml.Linq;
|
||||
using Directory = Lucene.Net.Store.Directory;
|
||||
using Version = Lucene.Net.Util.Version;
|
||||
|
||||
namespace Orchard.Indexing.Services {
|
||||
/// <summary>
|
||||
/// Represents the default implementation of an IIndexProvider, based on Lucene
|
||||
/// </summary>
|
||||
public class LuceneIndexProvider : IIndexProvider {
|
||||
private readonly IAppDataFolder _appDataFolder;
|
||||
private readonly ShellSettings _shellSettings;
|
||||
public static readonly Version LuceneVersion = Version.LUCENE_29;
|
||||
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 ILogger Logger { get; set; }
|
||||
|
||||
public LuceneIndexProvider(IAppDataFolder appDataFolder, ShellSettings shellSettings) {
|
||||
_appDataFolder = appDataFolder;
|
||||
_shellSettings = shellSettings;
|
||||
_analyzer = CreateAnalyzer();
|
||||
|
||||
// TODO: (sebros) Find a common way to get where tenant's specific files should go. "Sites/Tenant" is hard coded in multiple places
|
||||
_basePath = _appDataFolder.Combine("Sites", _shellSettings.Name, "Indexes");
|
||||
|
||||
Logger = NullLogger.Instance;
|
||||
|
||||
// Ensures the directory exists
|
||||
EnsureDirectoryExists();
|
||||
}
|
||||
|
||||
public static Analyzer CreateAnalyzer() {
|
||||
// StandardAnalyzer does lower-case and stop-word filtering. It also removes punctuation
|
||||
return new StandardAnalyzer(LuceneVersion);
|
||||
}
|
||||
|
||||
private void EnsureDirectoryExists() {
|
||||
var directory = new DirectoryInfo(_appDataFolder.MapPath(_basePath));
|
||||
if(!directory.Exists) {
|
||||
directory.Create();
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual Directory GetDirectory(string indexName) {
|
||||
var directoryInfo = new DirectoryInfo(_appDataFolder.MapPath(_appDataFolder.Combine(_basePath, indexName)));
|
||||
return FSDirectory.Open(directoryInfo);
|
||||
}
|
||||
|
||||
private static Document CreateDocument(LuceneDocumentIndex indexDocument) {
|
||||
var doc = new Document();
|
||||
|
||||
indexDocument.PrepareForIndexing();
|
||||
foreach(var field in indexDocument.Fields) {
|
||||
doc.Add(field);
|
||||
}
|
||||
return doc;
|
||||
}
|
||||
|
||||
public bool Exists(string indexName) {
|
||||
return new DirectoryInfo(_appDataFolder.MapPath(_appDataFolder.Combine(_basePath, indexName))).Exists;
|
||||
}
|
||||
|
||||
public bool IsEmpty(string indexName) {
|
||||
if ( !Exists(indexName) ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
var reader = IndexReader.Open(GetDirectory(indexName), true);
|
||||
|
||||
try {
|
||||
return reader.NumDocs() == 0;
|
||||
}
|
||||
finally {
|
||||
reader.Close();
|
||||
}
|
||||
}
|
||||
|
||||
public int NumDocs(string indexName) {
|
||||
if ( !Exists(indexName) ) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
var reader = IndexReader.Open(GetDirectory(indexName), true);
|
||||
|
||||
try {
|
||||
return reader.NumDocs();
|
||||
}
|
||||
finally {
|
||||
reader.Close();
|
||||
}
|
||||
}
|
||||
|
||||
public void CreateIndex(string indexName) {
|
||||
var writer = new IndexWriter(GetDirectory(indexName), _analyzer, true, IndexWriter.MaxFieldLength.UNLIMITED);
|
||||
writer.Close();
|
||||
|
||||
Logger.Information("Index [{0}] created", indexName);
|
||||
}
|
||||
|
||||
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) {
|
||||
Store(indexName, new [] { (LuceneDocumentIndex)indexDocument });
|
||||
}
|
||||
|
||||
public void Store(string indexName, IEnumerable<IDocumentIndex> indexDocuments) {
|
||||
Store(indexName, indexDocuments.Cast<LuceneDocumentIndex>());
|
||||
}
|
||||
|
||||
public void Store(string indexName, IEnumerable<LuceneDocumentIndex> indexDocuments) {
|
||||
if(indexDocuments.AsQueryable().Count() == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
var writer = new IndexWriter(GetDirectory(indexName), _analyzer, false, IndexWriter.MaxFieldLength.UNLIMITED);
|
||||
LuceneDocumentIndex current = null;
|
||||
|
||||
try {
|
||||
foreach ( var indexDocument in indexDocuments ) {
|
||||
current = indexDocument;
|
||||
var doc = CreateDocument(indexDocument);
|
||||
writer.AddDocument(doc);
|
||||
Logger.Debug("Document [{0}] indexed", indexDocument.Id);
|
||||
}
|
||||
}
|
||||
catch ( Exception ex ) {
|
||||
Logger.Error(ex, "An unexpected error occured while add the document [{0}] from the index [{1}].", current.Id, indexName);
|
||||
}
|
||||
finally {
|
||||
writer.Optimize();
|
||||
writer.Close();
|
||||
}
|
||||
}
|
||||
|
||||
public void Delete(string indexName, int documentId) {
|
||||
Delete(indexName, new[] { documentId });
|
||||
}
|
||||
|
||||
public void Delete(string indexName, IEnumerable<int> documentIds) {
|
||||
if ( documentIds.AsQueryable().Count() == 0 ) {
|
||||
return;
|
||||
}
|
||||
|
||||
var reader = IndexReader.Open(GetDirectory(indexName), false);
|
||||
|
||||
try {
|
||||
foreach (var id in documentIds) {
|
||||
try {
|
||||
var term = new Term("id", id.ToString());
|
||||
if (reader.DeleteDocuments(term) != 0) {
|
||||
Logger.Error("The document [{0}] could not be removed from the index [{1}]", id, indexName);
|
||||
}
|
||||
else {
|
||||
Logger.Debug("Document [{0}] removed from index", id);
|
||||
}
|
||||
}
|
||||
catch (Exception ex) {
|
||||
Logger.Error(ex, "An unexpected error occured while removing the document [{0}] from the index [{1}].", id, indexName);
|
||||
}
|
||||
}
|
||||
}
|
||||
finally {
|
||||
reader.Close();
|
||||
}
|
||||
}
|
||||
|
||||
public IDocumentIndex New(int documentId) {
|
||||
return new LuceneDocumentIndex(documentId);
|
||||
}
|
||||
|
||||
public ISearchBuilder CreateSearchBuilder(string indexName) {
|
||||
return new LuceneSearchBuilder(GetDirectory(indexName));
|
||||
}
|
||||
|
||||
private string GetSettingsFileName(string indexName) {
|
||||
return _appDataFolder.MapPath(_appDataFolder.Combine(_basePath, indexName + ".settings.xml"));
|
||||
}
|
||||
|
||||
public DateTime GetLastIndexUtc(string indexName) {
|
||||
var settingsFileName = GetSettingsFileName(indexName);
|
||||
|
||||
return File.Exists(settingsFileName)
|
||||
? DateTime.Parse(XDocument.Load(settingsFileName).Descendants(LastIndexUtc).First().Value)
|
||||
: DefaultMinDateTime;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@@ -0,0 +1,317 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using Lucene.Net.Analysis;
|
||||
using Lucene.Net.Analysis.Tokenattributes;
|
||||
using Lucene.Net.Index;
|
||||
using Lucene.Net.Search;
|
||||
using Lucene.Net.Store;
|
||||
using Orchard.Indexing.Models;
|
||||
using Orchard.Logging;
|
||||
using Lucene.Net.Documents;
|
||||
using Lucene.Net.QueryParsers;
|
||||
|
||||
namespace Orchard.Indexing.Services {
|
||||
public class LuceneSearchBuilder : ISearchBuilder {
|
||||
|
||||
private const int MaxResults = Int16.MaxValue;
|
||||
|
||||
private readonly Directory _directory;
|
||||
|
||||
private readonly List<BooleanClause> _clauses;
|
||||
private int _count;
|
||||
private int _skip;
|
||||
private readonly Dictionary<string, DateTime> _before;
|
||||
private readonly Dictionary<string, DateTime> _after;
|
||||
private string _sort;
|
||||
private bool _sortDescending;
|
||||
private string _parse;
|
||||
private readonly Analyzer _analyzer;
|
||||
private string[] _defaultFields;
|
||||
|
||||
// pending clause attributes
|
||||
private BooleanClause.Occur _occur;
|
||||
private bool _exactMatch;
|
||||
private float _boost;
|
||||
private Query _query;
|
||||
|
||||
public ILogger Logger { get; set; }
|
||||
|
||||
public LuceneSearchBuilder(Directory directory) {
|
||||
_directory = directory;
|
||||
Logger = NullLogger.Instance;
|
||||
|
||||
_count = MaxResults;
|
||||
_skip = 0;
|
||||
_before = new Dictionary<string, DateTime>();
|
||||
_after = new Dictionary<string, DateTime>();
|
||||
_clauses = new List<BooleanClause>();
|
||||
_sort = String.Empty;
|
||||
_sortDescending = true;
|
||||
_parse = String.Empty;
|
||||
_analyzer = LuceneIndexProvider.CreateAnalyzer();
|
||||
|
||||
InitPendingClause();
|
||||
}
|
||||
public ISearchBuilder Parse(string defaultField, string query) {
|
||||
return Parse(new string[] {defaultField}, query);
|
||||
}
|
||||
|
||||
public ISearchBuilder Parse(string[] defaultFields, string query) {
|
||||
if ( defaultFields.Length == 0 ) {
|
||||
throw new ArgumentException("Default field can't be empty");
|
||||
}
|
||||
|
||||
if ( String.IsNullOrWhiteSpace(query) ) {
|
||||
throw new ArgumentException("Query can't be empty");
|
||||
}
|
||||
|
||||
_defaultFields = defaultFields;
|
||||
_parse = query;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ISearchBuilder WithField(string field, int value) {
|
||||
CreatePendingClause();
|
||||
_query = NumericRangeQuery.NewIntRange(field, value, value, true, true);
|
||||
return this;
|
||||
}
|
||||
|
||||
public ISearchBuilder WithinRange(string field, int min, int max) {
|
||||
CreatePendingClause();
|
||||
_query = NumericRangeQuery.NewIntRange(field, min, max, true, true);
|
||||
return this;
|
||||
}
|
||||
|
||||
public ISearchBuilder WithField(string field, float value) {
|
||||
CreatePendingClause();
|
||||
_query = NumericRangeQuery.NewFloatRange(field, value, value, true, true);
|
||||
return this;
|
||||
}
|
||||
|
||||
public ISearchBuilder WithinRange(string field, float min, float max) {
|
||||
CreatePendingClause();
|
||||
_query = NumericRangeQuery.NewFloatRange(field, min, max, true, true);
|
||||
return this;
|
||||
}
|
||||
|
||||
public ISearchBuilder WithField(string field, bool value) {
|
||||
CreatePendingClause();
|
||||
_query = new TermQuery(new Term(field, value.ToString()));
|
||||
return this;
|
||||
}
|
||||
|
||||
public ISearchBuilder WithField(string field, DateTime value) {
|
||||
CreatePendingClause();
|
||||
_query = new TermQuery(new Term(field, DateTools.DateToString(value, DateTools.Resolution.SECOND)));
|
||||
return this;
|
||||
}
|
||||
|
||||
public ISearchBuilder WithinRange(string field, DateTime min, DateTime max) {
|
||||
CreatePendingClause();
|
||||
_query = new TermRangeQuery(field, DateTools.DateToString(min, DateTools.Resolution.SECOND), DateTools.DateToString(max, DateTools.Resolution.SECOND), true, true);
|
||||
return this;
|
||||
}
|
||||
|
||||
public ISearchBuilder WithinRange(string field, string min, string max) {
|
||||
CreatePendingClause();
|
||||
_query = new TermRangeQuery(field, QueryParser.Escape(min.ToLower()), QueryParser.Escape(min.ToLower()), true, true);
|
||||
return this;
|
||||
}
|
||||
|
||||
public ISearchBuilder WithField(string field, string value) {
|
||||
CreatePendingClause();
|
||||
|
||||
if ( !String.IsNullOrWhiteSpace(value) ) {
|
||||
_query = new TermQuery(new Term(field, QueryParser.Escape(value.ToLower())));
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public ISearchBuilder Mandatory() {
|
||||
_occur = BooleanClause.Occur.MUST;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ISearchBuilder Forbidden() {
|
||||
_occur = BooleanClause.Occur.MUST_NOT;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ISearchBuilder ExactMatch() {
|
||||
_exactMatch = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ISearchBuilder Weighted(float weight) {
|
||||
_boost = weight;
|
||||
return this;
|
||||
}
|
||||
|
||||
private void InitPendingClause() {
|
||||
_occur = BooleanClause.Occur.SHOULD;
|
||||
_exactMatch = false;
|
||||
_query = null;
|
||||
_boost = 0;
|
||||
}
|
||||
|
||||
private void CreatePendingClause() {
|
||||
if(_query == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (_boost != 0) {
|
||||
_query.SetBoost(_boost);
|
||||
}
|
||||
|
||||
if(!_exactMatch) {
|
||||
var termQuery = _query as TermQuery;
|
||||
if(termQuery != null) {
|
||||
_query = new PrefixQuery(termQuery.GetTerm());
|
||||
}
|
||||
}
|
||||
|
||||
_clauses.Add(new BooleanClause(_query, _occur));
|
||||
}
|
||||
|
||||
public ISearchBuilder SortBy(string name) {
|
||||
_sort = name;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ISearchBuilder Ascending() {
|
||||
_sortDescending = false;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ISearchBuilder Slice(int skip, int count) {
|
||||
if ( skip < 0 ) {
|
||||
throw new ArgumentException("Skip must be greater or equal to zero");
|
||||
}
|
||||
|
||||
if ( count <= 0 ) {
|
||||
throw new ArgumentException("Count must be greater than zero");
|
||||
}
|
||||
|
||||
_skip = skip;
|
||||
_count = count;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
private Query CreateQuery() {
|
||||
CreatePendingClause();
|
||||
|
||||
var query = new BooleanQuery();
|
||||
|
||||
if(!String.IsNullOrWhiteSpace(_parse)) {
|
||||
|
||||
foreach ( var defaultField in _defaultFields ) {
|
||||
var clause = new BooleanClause(new QueryParser(LuceneIndexProvider.LuceneVersion, defaultField, LuceneIndexProvider.CreateAnalyzer()).Parse(_parse), BooleanClause.Occur.SHOULD);
|
||||
query.Add(clause);
|
||||
}
|
||||
}
|
||||
|
||||
foreach( var clause in _clauses)
|
||||
query.Add(clause);
|
||||
|
||||
if ( query.Clauses().Count == 0 ) { // get all documents ?
|
||||
query.Add(new TermRangeQuery("id", "0", "9", true, true), BooleanClause.Occur.SHOULD);
|
||||
}
|
||||
|
||||
Logger.Debug("New search query: {0}", query.ToString());
|
||||
return query;
|
||||
}
|
||||
|
||||
public IEnumerable<ISearchHit> Search() {
|
||||
var query = CreateQuery();
|
||||
|
||||
IndexSearcher searcher;
|
||||
|
||||
try {
|
||||
searcher = new IndexSearcher(_directory, true);
|
||||
}
|
||||
catch {
|
||||
// index might not exist if it has been rebuilt
|
||||
Logger.Information("Attempt to read a none existing index");
|
||||
return Enumerable.Empty<ISearchHit>();
|
||||
}
|
||||
|
||||
try {
|
||||
var sort = String.IsNullOrEmpty(_sort)
|
||||
? Sort.RELEVANCE
|
||||
: new Sort(new SortField(_sort, CultureInfo.InvariantCulture, _sortDescending));
|
||||
var collector = TopFieldCollector.create(
|
||||
sort,
|
||||
_count + _skip,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
true);
|
||||
|
||||
Logger.Debug("Searching: {0}", query.ToString());
|
||||
searcher.Search(query, collector);
|
||||
|
||||
var results = collector.TopDocs().scoreDocs
|
||||
.Skip(_skip)
|
||||
.Select(scoreDoc => new LuceneSearchHit(searcher.Doc(scoreDoc.doc), scoreDoc.score))
|
||||
.ToList();
|
||||
|
||||
Logger.Debug("Search results: {0}", results.Count);
|
||||
|
||||
return results;
|
||||
}
|
||||
finally {
|
||||
searcher.Close();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public int Count() {
|
||||
var query = CreateQuery();
|
||||
IndexSearcher searcher;
|
||||
|
||||
try {
|
||||
searcher = new IndexSearcher(_directory, true);
|
||||
}
|
||||
catch {
|
||||
// index might not exist if it has been rebuilt
|
||||
Logger.Information("Attempt to read a none existing index");
|
||||
return 0;
|
||||
}
|
||||
|
||||
try {
|
||||
var hits = searcher.Search(query, Int16.MaxValue);
|
||||
Logger.Information("Search results: {0}", hits.scoreDocs.Length);
|
||||
var length = hits.scoreDocs.Length;
|
||||
return Math.Min(length - _skip, _count) ;
|
||||
}
|
||||
finally {
|
||||
searcher.Close();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public ISearchHit Get(int documentId) {
|
||||
var query = new TermQuery(new Term("id", documentId.ToString()));
|
||||
|
||||
var searcher = new IndexSearcher(_directory, true);
|
||||
try {
|
||||
var hits = searcher.Search(query, 1);
|
||||
Logger.Information("Search results: {0}", hits.scoreDocs.Length);
|
||||
if ( hits.scoreDocs.Length > 0 ) {
|
||||
return new LuceneSearchHit(searcher.Doc(hits.scoreDocs[0].doc), hits.scoreDocs[0].score);
|
||||
}
|
||||
else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
finally {
|
||||
searcher.Close();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user