Здравствуйте, уважаемые читатели. Рад вам сообщить, что сегодня мы будем снова погружаться в
мир Lucene.NET. Зацепила меня эта тема, так что постараюсь засесть как можно плотнее с ней, тем
более что работы там просто немеряно. И если вас интересует, как реализовать
высокоскоростной поиск в Lucene.NET, надеюсь, что эта статья поможет вам
понять, как же все-таки работает поиск в Lucene.NET. Так что тема данной статьи как работает
поиск в Lucene.Net. Эта тема
достаточно мало раскрыта в интернете, поэтому мы с вами будем самостоятельно ее
раскрывать. Для начала создадим новый Console Application проект и назовем
его “LuceneSearchSample”, как показано на рисунке ниже.
Затем установим
пакет Lucene.NET через NuGet Packages Manager.
Начнем, пожалуй, с
самого начала, – это написание интерфейса, который будет предоставлять нам
нужные функции для поиска по Lucene документам.
public interface IExtendingSearchService
{
string DataFolder { get; }
FSDirectory LuceneDirectory { get; }
List<Document> WildcardSearch(string field, string query);
List<Document> TermSearch(string field, string query);
List<Document> PhaseSearch(string field, string query);
List<Document> PrefixSearch(string field, string query);
List<Document> FuzzySearch(string field, string query);
List<Document> BooleanSearch(List<Query> queries);
List<Document> Search(List<string> fields, string query);
}
А теперь приступим
к реализации класса ExtendingSearchService и объясним его функцию.
public class ExtendingSearchService : IExtendingSearchService
{
private const string LuceneIndexFolder = "LuceneIndex";
private FSDirectory _luceneDirectory;
private string _dataFolder;
public ExtendingSearchService()
{
InitializeLucene();
}
public string DataFolder
{
get { return _dataFolder; }
}
public FSDirectory LuceneDirectory
{
get { return _luceneDirectory; }
}
private void InitializeLucene()
{
_dataFolder = Path.Combine(Environment.CurrentDirectory, LuceneIndexFolder);
var di = new DirectoryInfo(_dataFolder);
if (!di.Exists)
{
di.Create();
}
_luceneDirectory = FSDirectory.Open(di.FullName);
}
public List<Document> WildcardSearch(string field, string query)
{
return ExecuteQuery((searcher, analizer) =>
{
var wildcardQuery = new WildcardQuery(new Term(field, query));
return GetDocuments(searcher, wildcardQuery);
});
}
public List<Document> TermSearch(string field, string query)
{
return ExecuteQuery((searcher, analizer) =>
{
var termQuery = new TermQuery(new Term(field, query));
return GetDocuments(searcher, termQuery);
});
}
public List<Document> PhaseSearch(string field, string query)
{
return ExecuteQuery((searcher, analizer) =>
{
var phaseQuery = new PhraseQuery();
phaseQuery.Add(new Term(field, query));
return GetDocuments(searcher, phaseQuery);
});
}
public List<Document> PrefixSearch(string field, string query)
{
return ExecuteQuery((searcher, analizer) =>
{
var prefixQuery = new PrefixQuery(new Term(field, query));
return GetDocuments(searcher, prefixQuery);
});
}
public List<Document> FuzzySearch(string field, string query)
{
return ExecuteQuery((searcher, analizer) =>
{
var fuzzyQuery = new FuzzyQuery(new Term(field, query));
return GetDocuments(searcher, fuzzyQuery);
});
}
public List<Document> BooleanSearch(List<Query> queries)
{
return ExecuteQuery((ind, analyzer) =>
{
var booleanQuery = new BooleanQuery();
foreach (var query in queries)
{
booleanQuery.Add(query, Occur.MUST);
}
return GetDocuments(ind, booleanQuery);
});
}
public List<Document> Search(List<string> fields, string searchQuery)
{
return ExecuteQuery((ind, analyzer) =>
{
QueryParser parser = fields.Count == 1 ?
new QueryParser(Lucene.Net.Util.Version.LUCENE_30, fields.First(), analyzer) :
new MultiFieldQueryParser(Lucene.Net.Util.Version.LUCENE_30, fields.ToArray(), analyzer);
var query = parser.Parse(searchQuery);
return GetDocuments(ind, query);
});
}
private static List<Document> GetDocuments(IndexSearcher ind, Query query)
{
var hits = ind.Search(query, 1000).ScoreDocs;
var documents = new List<Document>();
if (hits != null)
{
documents.AddRange(hits.Select(hit => ind.Doc(hit.Doc)));
}
return documents;
}
private List<Document> ExecuteQuery(Func<IndexSearcher, Analyzer, List<Document>> action)
{
using (var searcher = new IndexSearcher(LuceneDirectory))
{
var analyzer = new StandardAnalyzer(Lucene.Net.Util.Version.LUCENE_30);
var res = action(searcher, analyzer);
analyzer.Close();
searcher.Dispose();
return res;
}
}
}
Здесь много
методов, но по сути, в них ничего страшного нет. Ниже представлена таблица, в
которой расписаны разные варианты поиска.
Ниже приведена
таблица всех рассмотренных вариантов, которые мы рассмотрим далее в статье. У
нас написан также универсальный метод Search, который позволяет с помощью
класса QueryParser получить нужный
тип запроса для поиска. Первым делом напишем базовый абстрактный класс ADocument, в котором
реализуем логику по добавлению нового документа в Lucene.NET. Ниже приведена логика этого класса.
public abstract class ADocument
{
private long _id;
public long Id
{
set
{
_id = value;
AddParameterToDocument("Id", _id, Field.Store.YES, Field.Index.NOT_ANALYZED);
}
get { return _id; }
}
private readonly Document _document;
public Document Document { get { return _document; } }
protected ADocument()
{
_document = new Document();
}
protected void AddParameterToDocument(string name, dynamic value, Field.Store store, Field.Index index)
{
_document.Add(new Field(name, value.ToString(), store, index));
}
protected void AddParameterToDocumentStoreParameter(string name, dynamic value)
{
AddParameterToDocument(name, value, Field.Store.YES, Field.Index.ANALYZED);
}
protected void AddParameterToDocumentNoStoreParameter(string name, dynamic value)
{
AddParameterToDocument(name, value, Field.Store.NO, Field.Index.ANALYZED);
}
}
Теперь давайте
реализуем тестовый класс CityDocument, который будет хранить всю нашу необходимую
структуру.
public class CityDocument : ADocument
{
private string _location;
public string Location
{
get { return _location; }
set
{
_location = value;
AddParameterToDocument("Location", _location, Field.Store.YES, Field.Index.NOT_ANALYZED);
}
}
}
Теперь нам нужно
добавить класс, который будет записывать наши филды в конкретный документ в
индексе. Назовем этот класс BaseWriter, в котором реализуем эту логику. У этого
класса наружу смотрят только три метода: AddUpdateItemsToIndex для добавления новых значений в индекс, метод DeleteItemsFromIndex для удаления старых значений, ну и метод Optimize для того чтобы запустить оптимизацию
индекса. Ниже приведена реализация всего этого.
public class BaseWriter
{
private readonly IExtendingSearchService _luceneService;
public BaseWriter(IExtendingSearchService luceneService)
{
_luceneService = luceneService;
}
protected void AddUpdateItemsToIndex(IEnumerable<ADocument> docs)
{
var standardAnalyzer = new StandardAnalyzer(Lucene.Net.Util.Version.LUCENE_30);
using (var writer = new IndexWriter(_luceneService.LuceneDirectory, standardAnalyzer, IndexWriter.MaxFieldLength.UNLIMITED))
{
foreach (var doc in docs)
{
AddItemToIndex(doc, writer);
}
standardAnalyzer.Close();
writer.Dispose();
}
}
private void AddItemToIndex(ADocument doc, IndexWriter writer)
{
var query = new TermQuery(new Term("Id", doc.Id.ToString()));
writer.DeleteDocuments(query);
writer.AddDocument(doc.Document);
}
private void DeleteItemFromIndex(ADocument doc, IndexWriter writer)
{
var query = new TermQuery(new Term("Id", doc.Id.ToString()));
writer.DeleteDocuments(query);
}
protected void DeleteItemsFromIndex(IEnumerable<ADocument> docs)
{
var standardAnalyzer = new StandardAnalyzer(Lucene.Net.Util.Version.LUCENE_30);
using (var writer = new IndexWriter(_luceneService.LuceneDirectory, standardAnalyzer, IndexWriter.MaxFieldLength.UNLIMITED))
{
foreach (var doc in docs)
{
DeleteItemFromIndex(doc, writer);
}
standardAnalyzer.Close();
writer.Dispose();
}
}
protected void Optimize()
{
var standardAnalyzer = new StandardAnalyzer(Lucene.Net.Util.Version.LUCENE_30);
using (var writer = new IndexWriter(_luceneService.LuceneDirectory, standardAnalyzer, IndexWriter.MaxFieldLength.UNLIMITED))
{
standardAnalyzer.Close();
writer.Optimize();
writer.Dispose();
}
}
}
Следующим делом мы
сделаем враппер для нашего класса CityDocument, который будет записывать список документов
в наш индекс. Назовем этот класс CityWriter.
public class CityWriter : BaseWriter
{
public CityWriter(IExtendingSearchService luceneService) : base(luceneService)
{
}
public void AddUpdateCityToIndex(List<CityDocument> cities)
{
AddUpdateItemsToIndex(cities);
}
}
В этом классе
только один метод, в котором мы добавляем список наших документов в список.
Теперь перейдем в
класс Program, который у нас был
создан с самого начала, так как у нас консольное приложение, и реализуем
следующую логику.
class Program
{
static void Main(string[] args)
{
TestSearchingInLucene();
Console.ReadLine();
}
private static void TestSearchingInLucene()
{
IExtendingSearchService luceneService = new ExtendingSearchService();
var writer = new CityWriter(luceneService);
var cities = GenerateCityDocuments();
writer.AddUpdateCityToIndex(cities);
IExtendingSearchService extendingSearchService = new ExtendingSearchService();
#region WildcardQuery Search
Console.WriteLine("WildcardQuery Search");
var docs = extendingSearchService.WildcardSearch("Location", "chicago he*");
foreach (var doc in docs)
{
Console.WriteLine("Id = {0}, Location = {1}", doc.GetField("Id"), doc.GetField("Location"));
}
#endregion
#region FuzzyQuery Search
Console.WriteLine("FuzzyQuery Search");
docs = extendingSearchService.FuzzySearch("Location", "montre~");
foreach (var doc in docs)
{
Console.WriteLine("Id = {0}, Location = {1}", doc.GetField("Id"), doc.GetField("Location"));
}
#endregion
#region PhaseQuery Search
Console.WriteLine("PhaseQuery Search");
docs = extendingSearchService.PhaseSearch("Location", "new-york");
foreach (var doc in docs)
{
Console.WriteLine("Id = {0}, Location = {1}", doc.GetField("Id"), doc.GetField("Location"));
}
#endregion
#region PrefixQuery Search
Console.WriteLine("PrefixQuery Search");
docs = extendingSearchService.PrefixSearch("Location", "chica");
foreach (var doc in docs)
{
Console.WriteLine("Id = {0}, Location = {1}", doc.GetField("Id"), doc.GetField("Location"));
}
#endregion
#region TermQuery Search
Console.WriteLine("TermQuery Search");
docs = extendingSearchService.TermSearch("Id", "5");
foreach (var doc in docs)
{
Console.WriteLine("Id = {0}, Location = {1}", doc.GetField("Id"), doc.GetField("Location"));
}
#endregion
#region BooleanQuery Search
Console.WriteLine("BooleanQuery Search");
var prefixQuery = new PrefixQuery(new Term("Location", "chica"));
var wildcardQuery = new TermQuery(new Term("Id", "3"));
docs = extendingSearchService.BooleanSearch(new List<Query>(new Query[] { prefixQuery, wildcardQuery }));
foreach (var doc in docs)
{
Console.WriteLine("Id = {0}, Location = {1}", doc.GetField("Id"), doc.GetField("Location"));
}
#endregion
#region MultiFieldQuery Search
Console.WriteLine("MultiFieldQuery Search");
docs = extendingSearchService.Search(new List<string>(new[] { "Location", "Id" }), "6");
foreach (var doc in docs)
{
Console.WriteLine("Id = {0}, Location = {1}", doc.GetField("Id"), doc.GetField("Location"));
}
#endregion
Console.ReadLine();
}
private static List<CityDocument> GenerateCityDocuments()
{
var cities = new List<CityDocument>();
cities.Add(new CityDocument
{
Id = 1,
Location = "chicago heights"
});
cities.Add(new CityDocument
{
Id = 2,
Location = "new-york"
});
cities.Add(new CityDocument
{
Id = 3,
Location = "chicago low"
});
cities.Add(new CityDocument
{
Id = 4,
Location = "montreal"
});
cities.Add(new CityDocument
{
Id = 5,
Location = "paris"
});
cities.Add(new CityDocument
{
Id = 6,
Location = "this is my test"
});
return cities;
}
}
Для того чтобы
посмотреть, как был построен наш индекс и как работает наш поиск, нам
понадобится тулза Luke, которая позволяет просматривать индексы Lucene. Дальше запустим
наш проект и посмотрим, что же у нас получилось.
Далее откроем нашу тулзу Luke и посмотрим, как у нас построен индекс. (P.S. Ваша папка LuceneIndex, в которой
физически хранится индекс Lucene, лежит в папке Debug.)
После того как мы
укажем путь к директории, где у нас хранится физически файлы, которые строят Lucene индекс, нужно
нажать кнопку OK и посмотреть на
структуру, которая у нас вышла.
Справа видно
заполнение наших филдов, слева – структуру самого индекса. Кстати, в самом Luke вы можете открыть
вкладку Search и даже писать самим простые запросы для
поиска по документам.
Единственное, что,
наверное, будет непривычно для тех, кто работает с Lucene.Net, – это то, что аналайзеры для Lucene.NET немного
отличаются. Но если вы посмотрите на пример выше, то увидите, что запросы в Luke писать несложно.
Итоги
Сегодня мы рассмотрели,
как работает поиск в Lucene.NET с разными типами запросов, а также способы проверки поиска с помощью утилиты Luke. Надеюсь, что статья получилась не очень
скучной, и вы сможете использовать полученные знания с этой статьи на реальных
примерах. Надеюсь, что мы с вами еще не раз вернемся к Lucene и рассмотрим еще
что-то интересное, что может нам пригодится для реализации высокоскоростного
поиска в своем проекте.
No comments:
Post a Comment