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 将负责 inserts 和 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/
源代码在文章顶部。




