LuceneWrap:一个精简的 Lucene.net 包装器






4.85/5 (10投票s)
提供一个通用的包装器,用于 Lucene.net 的基本搜索功能
引言
Lucene.net 是流行的 Java 文本搜索引擎 Lucene 的移植版。它允许我们在不同类型的数据源上存储和索引数据,并提供对存储文本的高性能查询。
本文的目的不是深入探讨 Lucene.Net 的架构或行为(有很多资源,您可以在文章末尾找到一些),而是概述该库最常见的用法,并展示一种简化我们必须执行的操作的方法。
背景
在运行代码之前,让我们总结一下使用 Lucene 索引/搜索数据的流程。
索引
- 获取要索引的数据(来自数据库、XML 等)
- 打开
IndexDirectory
,创建一个IndexWriter
并初始化其相关对象。 - 为要存储的每个值创建一个 Field,一旦拥有构成“一行数据”的所有字段,就将它们插入
Document
,最后将文档添加到索引中。 - 在所有文档都写入后,我们可以
Optimize
索引并关闭它。
搜索
- 打开
IndexDirectory
,创建一个IndexReader
并初始化其相关对象。 - 对索引执行
Query
。 - 迭代返回的
Document
结果集,并获取每行每个字段的值。
LuceneWrap 范围
LuceneWrap 只是对原始 Lucene.net DLL 的一个简单包装。它不打算包含 Lucene 的所有功能(尽管您可以随意扩展或重构它),而是旨在简化/减少与索引交互所需的调用。
LuceneWrap 基础
LuceneWrap 主要做两件事
- 使开发人员不必正确创建/处理 Lucene.net 对象,如目录、搜索对象、索引等。
- 通过使用
LuceneWrapAttribute
装饰类的成员,我们避免了处理需要存储的数据与Lucene.Net.Documents.Field
之间的映射。搜索也是如此,我们将能够调用Search<T>
并从索引中检索强类型列表。
代码
代码非常简单,包装器的关键功能在于通用的 insert
/update
/search
方法,并通过自定义属性来实现。
[System.AttributeUsage(System.AttributeTargets.Property)]
public class LuceneWrapAttribute : System.Attribute
{
public string Name { get; set; }
public string Value { get; set; }
public bool IsStored { get; set; }
public bool IsSearchable { get; set; }
public LuceneWrapAttribute(){}
}
然后,我们选择要存储的类,并用一个负责将其标记为索引的自定义属性来装饰需要索引的成员。假设我们想存储使用 Entity Framework 执行的查询结果,在这种情况下,我们只需用 LuceneWrapAttribute
装饰实体 POCO 对象的成员。在我的示例中,我使用的是一个简单的类,代表一个只有三个字段的 Feed。
public class FeedResult
{
[LuceneWrap(IsSearchable = false, Name = "Id", IsStored = true)]
public string Id { get; set; }
[LuceneWrap(IsSearchable = true, Name = "Title", IsStored = true)]
public string Title { get; set; }
[LuceneWrap(IsSearchable = true, Name = "Summary", IsStored = true)]
public string Summary { get; set; }
}
在类被正确装饰后,我就可以创建一个索引并执行搜索。以下是几个测试的片段。
LuceneManager<FeedResult> _luceneManager = new LuceneManager<FeedResult>();
LuceneSearcher _luceneSearcher = new LuceneSearcher();
[Test]
public void WriteIndex_Test()
{
//We retrieve a list of feeds from a website and get a list of FeedResult
List<FeedResult> feeds = FeedManager.GetFeeds();
foreach (var feed in feeds)
{
_luceneManager.AddItemToIndex(feed);
}
_luceneManager.FinalizeWriter(true);
}
[Test]
public void SearchInIndex_Test()
{
//we retrieve a list of FeedResult by searching on the
//Summary field any occurrence of "presentations"
var result = _luceneSearcher.Search<FeedResult>("Summary", "presentations");
foreach (var feedResult in result)
{
Console.WriteLine(feedResult.Id);
Console.WriteLine(feedResult.Title);
Console.WriteLine(feedResult.Summary);
Console.WriteLine(Environment.NewLine);
}
}
LuceneManager
将负责 insert
s 和 update
。请注意,为了 update
一个字段,我们必须先 delete
它,然后再次 insert
它。
public class LuceneManager<T> : ILuceneManager<T>
{
private readonly string _INDEX_FILEPATH =
ConfigurationManager.AppSettings.Get("LuceneIndexFilePath");
private Analyzer _analyzer = null;
private IndexWriter _indexWriter = null;
private IndexReader _indexReader = null;
private Directory _luceneIndexDirectory = null;
public LuceneManager()
{
Create();
}
public LuceneManager(string indexFilePath): this()
{
_INDEX_FILEPATH = indexFilePath;
}
public void Create()
{
_analyzer = new StandardAnalyzer(Version.LUCENE_29);
_luceneIndexDirectory = FSDirectory.Open(new DirectoryInfo(_INDEX_FILEPATH));
_indexWriter = new IndexWriter
(_luceneIndexDirectory, _analyzer, IndexWriter.MaxFieldLength.UNLIMITED);
_indexReader = IndexReader.Open(_luceneIndexDirectory, false);
_indexReader.Close();
}
#region Insert index
#region Public methods
public void AddItemToIndex(T obj)
{
AddObjectToIndex(obj);
}
public void AddItemsToIndex(List<T> objects)
{
foreach (var obj in objects)
{
AddObjectToIndex(obj);
}
}
#endregion
#region Private methods
public void AddObjectToIndex(T obj)
{
Document document = new Document();
var newFields = LuceneReflection.GetLuceneFields(obj, false);
foreach (var newField in newFields)
{
document.Add(newField);
}
_indexWriter.AddDocument(document);
}
#endregion
#endregion
#region UpdateIndex
#region Public methods
public void ModifyItemFromIndex(T oldObj, T newObj)
{
DeleteObjectFromIndex(oldObj);
InsertUpdateFieldFromIndex(newObj);
}
public void ModifyItemFromIndex(List<T> oldObj, List<T> newObj)
{
foreach (var field in oldObj)
{
DeleteObjectFromIndex(field);
}
foreach (var field in newObj)
{
InsertUpdateFieldFromIndex(field);
}
}
#endregion
#region Private methods
public void DeleteObjectFromIndex(T oldObj)
{
var oldFields = LuceneReflection.GetLuceneFields(oldObj, false);
foreach (var oldField in oldFields)
{
_indexWriter.DeleteDocuments(new Term
(oldField.Name(), oldField.StringValue()));
}
}
public void InsertUpdateFieldFromIndex(T newfield)
{
AddObjectToIndex(newfield);
}
#endregion
#endregion
public void FinalizeWriter(bool optimize)
{
if (optimize)
_indexWriter.Optimize();
_indexWriter.Commit();
_indexWriter.Close();
_luceneIndexDirectory.Close();
}
}
LuceneSearcher
负责重试指定类型的对象列表。例如,如果索引包含 Employees
,我们可以使用 LuceneSearcher.Search<Employee>
进行搜索。在我们的示例中,我们使用 FeedResult
作为我们索引的类型,所以我们将使用 LuceneSearcher.Search<FeedResult>
进行搜索。
public class LuceneSearcher : ILuceneSearcher
{
private readonly string _INDEX_FILEPATH =
ConfigurationManager.AppSettings.Get("LuceneIndexFilePath");
private Directory _luceneIndexDirectory = null;
private IndexSearcher _indexSearcher = null;
private QueryParser _queryParser = null;
private StandardAnalyzer _analyzer = null;
public LuceneSearcher()
{
Create();
}
public LuceneSearcher(string indexFilePath): this()
{
_INDEX_FILEPATH = indexFilePath;
}
public void Create()
{
_luceneIndexDirectory = FSDirectory.Open(new DirectoryInfo(_INDEX_FILEPATH));
_analyzer = new StandardAnalyzer();
_indexSearcher = new IndexSearcher(_luceneIndexDirectory);
}
public List<T> Search<T>(string property, string textsearch) where T : new()
{
_queryParser = new QueryParser(property, _analyzer);
var result = GetResults<T>(textsearch);
return result;
}
public List<T> Search<T>(string textSearch) where T: new()
{
return GetResults<T>(textSearch);
}
public List<T> GetResults<T>(string textSearch) where T: new()
{
List<T> results = new List<T>();
Query query = _queryParser.Parse(textSearch);
//Do the search
Hits hits = _indexSearcher.Search(query);
int resultsCount = hits.Length();
for (int i = 0; i < resultsCount; i++)
{
Document doc = hits.Doc(i);
var obj = LuceneReflection.GetObjFromDocument<T>(doc);
results.Add(obj);
}
return results;
}
}
这是负责读写反射的类。
public class LuceneReflection
{
public static List<Field> GetLuceneFields<T>(T obj, bool isSearch)
{
List<Field> fields = new List<Field>();
Field field = null;
// get all properties of the object type
PropertyInfo[] propertyInfos = obj.GetType().GetProperties();
foreach (var propertyInfo in propertyInfos)
{
//If property is not null add it as field and it is not a search
if (obj.GetType().GetProperty(propertyInfo.Name).GetValue
(obj, null) != null && !isSearch)
{
field = GetLuceneFieldsForInsertUpdate
(obj, propertyInfo, false);
}
else
{
field = GetLuceneFieldsForInsertUpdate
(obj, propertyInfo, true);
}
fields.Add(field);
}
return fields;
}
private static Field GetLuceneFieldsForInsertUpdate<T>
(T obj, PropertyInfo propertyInfo, bool isSearch)
{
Field field = null;
object[] dbFieldAtts = propertyInfo.GetCustomAttributes
(typeof(LuceneWrapAttribute), isSearch);
if (dbFieldAtts.Length > 0 && propertyInfo.PropertyType == typeof(System.String))
{
var luceneWrapAttribute = ((LuceneWrapAttribute)dbFieldAtts[0]);
field = GetLuceneField(obj, luceneWrapAttribute, propertyInfo, isSearch);
}
else if (propertyInfo.PropertyType != typeof(System.String))
{
throw new InvalidCastException(string.Format("{0}
must be a string in order to get indexed", propertyInfo.Name));
}
return field;
}
private static Field GetLuceneField<T>(T obj, LuceneWrapAttribute luceneWrapAttribute,
PropertyInfo propertyInfo, bool isSearch)
{
Field.Store store = luceneWrapAttribute.IsStored ?
Field.Store.YES : Field.Store.NO;
Lucene.Net.Documents.Field.Index index = luceneWrapAttribute.IsSearchable ?
Field.Index.ANALYZED : Field.Index.NOT_ANALYZED;
//if it is not a search assign the object value to the field
string propertyValue = isSearch ? string.Empty :
obj.GetType().GetProperty(propertyInfo.Name).GetValue(obj, null).ToString();
Field field = new Field(propertyInfo.Name, propertyValue, store, index);
return field;
}
public static T GetObjFromDocument<T>(Document document) where T : new()
{
T obj = new T();
var fields = GetLuceneFields(obj, true);
foreach (var field in fields)
{
//setting values to properties of the object via reflection
obj.GetType().GetProperty(field.Name()).SetValue
(obj, document.Get(field.Name()), null);
}
return (T)obj;
}
}
一旦我们构建了索引,我们就可以搜索或修改一个字段,然后搜索新数据
[Test]
public void UpdateIndex_Test()
{
var feeds = FeedManager.GetFeeds();
var oldFeed = feeds.First();
FeedResult newField = new FeedResult(){Id = oldFeed.Id,
Summary = "CIAO CIAO",Title = oldFeed.Title};
_luceneManager.ModifyItemFromIndex(oldFeed,newField);
_luceneManager.FinalizeWriter(true);
}
[Test]
public void SearchModifiedEntry_Test()
{
//we retrieve a list of FeedResult by searching on the
//Summary field any occurrence of "presentations"
var result = _luceneSearcher.Search<FeedResult>("Summary", "CIAO CIAO");
foreach (var feedResult in result)
{
Console.WriteLine(feedResult.Id);
Console.WriteLine(feedResult.Title);
Console.WriteLine(feedResult.Summary);
Console.WriteLine(Environment.NewLine);
}
}
关注点
Lucene 是一个优秀的全文搜索框架,在 .NET 世界中的主要替代品是 SQL Server 提供的 全文搜索。以下是两者之间的主要区别
SQL Server FTS
- 您无需向解决方案添加任何内容,它已包含在 SQL Server 中
- 索引管理更加简单
- 它依赖于数据库
Lucene.net
- 它是免费开源的,提供了比 SQL Server 更多的可能性。
- 它不与任何产品绑定,您可以通过向 Web 服务器添加更多索引来轻松地水平扩展。
- 您必须以编程方式处理索引的每个阶段,从创建到更新。
结论
传闻 Lucene.net 在大量数据上的性能优于 SQL Server,我还没有进行任何比较,而且这个论点也不适合本文。
但是,如果您正在考虑为您的网站添加全文搜索,我的建议是:如果需要索引的数据已经存在于 SQL Server 中,并且您想要一个快速简单的实现,那就选择 SQL Server FTS。
否则,如果您想要一个快速且更复杂的解决方案,因为您知道您将不得不处理数百万条记录,或者因为 SQL Server 中缺少某些功能,或者仅仅是因为您没有 SQL Server……那么,在这种情况下,请选择 Lucene.net 并使用 LuceneWrap
。 :)
参考文献
- Lucene.Net: http://incubator.apache.org/lucene.net/
- SQL Server 2008 FTS: http://msdn.microsoft.com/en-us/library/ms142571.aspx
- 还有一些围绕 Lucene 构建的项目,以下是主要的几个
- LinqToLucene: http://linqtolucene.codeplex.com/
- SimpleLucene: http://simplelucene.codeplex.com/
源代码在文章顶部。