65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.85/5 (10投票s)

2011 年 4 月 29 日

CPOL

4分钟阅读

viewsIcon

51016

downloadIcon

1441

提供一个通用的包装器,用于 Lucene.net 的基本搜索功能

引言

Lucene.net 是流行的 Java 文本搜索引擎 Lucene 的移植版。它允许我们在不同类型的数据源上存储和索引数据,并提供对存储文本的高性能查询。

本文的目的不是深入探讨 Lucene.Net 的架构或行为(有很多资源,您可以在文章末尾找到一些),而是概述该库最常见的用法,并展示一种简化我们必须执行的操作的方法。

背景

在运行代码之前,让我们总结一下使用 Lucene 索引/搜索数据的流程。

索引

  1. 获取要索引的数据(来自数据库、XML 等)
  2. 打开 IndexDirectory,创建一个 IndexWriter 并初始化其相关对象。
  3. 为要存储的每个值创建一个 Field,一旦拥有构成“一行数据”的所有字段,就将它们插入 Document ,最后将文档添加到索引中。
  4. 在所有文档都写入后,我们可以 Optimize 索引并关闭它。

搜索

  1. 打开 IndexDirectory,创建一个 IndexReader 并初始化其相关对象。
  2. 对索引执行 Query
  3. 迭代返回的 Document 结果集,并获取每行每个字段的值。

LuceneWrap 范围

LuceneWrap 只是对原始 Lucene.net DLL 的一个简单包装。它不打算包含 Lucene 的所有功能(尽管您可以随意扩展或重构它),而是旨在简化/减少与索引交互所需的调用。

LuceneWrap 基础

LuceneWrap 主要做两件事

  1. 使开发人员不必正确创建/处理 Lucene.net 对象,如目录、搜索对象、索引等。
  2. 通过使用 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);
    }
}  

LucenewrapSearch_small.JPG

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);
    }
}	

searchres.JPG

关注点

Lucene 是一个优秀的全文搜索框架,在 .NET 世界中的主要替代品是 SQL Server 提供的 全文搜索。以下是两者之间的主要区别

SQL Server FTS

  1. 您无需向解决方案添加任何内容,它已包含在 SQL Server 中
  2. 索引管理更加简单
  3. 它依赖于数据库

Lucene.net

  1. 它是免费开源的,提供了比 SQL Server 更多的可能性。
  2. 它不与任何产品绑定,您可以通过向 Web 服务器添加更多索引来轻松地水平扩展。
  3. 您必须以编程方式处理索引的每个阶段,从创建到更新。

结论

传闻 Lucene.net 在大量数据上的性能优于 SQL Server,我还没有进行任何比较,而且这个论点也不适合本文。

但是,如果您正在考虑为您的网站添加全文搜索,我的建议是:如果需要索引的数据已经存在于 SQL Server 中,并且您想要一个快速简单的实现,那就选择 SQL Server FTS。

否则,如果您想要一个快速且更复杂的解决方案,因为您知道您将不得不处理数百万条记录,或者因为 SQL Server 中缺少某些功能,或者仅仅是因为您没有 SQL Server……那么,在这种情况下,请选择 Lucene.net 并使用 LuceneWrap。 :)

参考文献

源代码在文章顶部。

© . All rights reserved.