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

扩展 Cuyahoga 全文索引 (Lucene.NET)

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.30/5 (9投票s)

2008 年 3 月 7 日

CPOL

5分钟阅读

viewsIcon

46900

downloadIcon

335

在本文中,我们将扩展 Cuyahoga.Core.Search 命名空间中的类,以提供更通用的全文索引服务

引言

Cuyahoga 为模块开发人员提供了一个易于使用的包装器类,用于使用 Lucene.NET 进行全文索引。
尽管包装器类易于使用,但这些类本身并不通用。我的意思是
我们作为模块开发人员,必须将自定义内容(无论是静态还是动态)
转换为 SearchContent 对象,以便包装器类可以索引该内容。另一个类似的限制是,当我们对已索引的内容进行搜索时,我们只能获得 SearchResult 对象的集合。我还要提一下,我们作为模块开发人员,无法控制哪些字段将被存储/不存储,哪些字段将被用作关键字,或者在包装器对象构建全文索引时哪些字段不会被索引。

在本文中,我将尝试向您展示如何通过提供一个单独的扩展程序集来使 Cuyahoga.Core 的全文索引功能更加通用。

背景

我使用 Cuyahoga 框架创建了两个网站

在开发这些网站时,我需要对某些动态内容进行全文索引,即持久化在数据库中的内容。在探索了 Cuyahoga.Core.Search 命名空间中的包装器类之后,我注意到了当前搜索架构的局限性,并决定通过应用 .NET Generics 和 Reflection 使这些包装器类更加通用。

Bo.Cuyahoga.Extensions.Search 命名空间

为了使用 .NET Generics 和 Reflection 扩展 Cuyahoga.Core.Search 命名空间的功能,我将所有文件复制到我的项目中并开始重构。我在类名后面添加了 Ex 来区分扩展后的包装器类和 Cuyahoga.Core.Search 命名空间中的原始类。

IndexBuilder<T> 类

Cuyahoga.Core.Search.IndexBuilder 类的重构泛型版本。这个类
用作 Lucene.NET 索引构建功能的包装器。仔细查看 `BuildDocumentFromSearchContent` 和 `DeleteContent` 方法。这些方法已被修改,以便我们可以使用反射来检索内容对象的字段。

private Document BuildDocumentFromSearchContent(T searchContent)
{
    Document doc = new Document();
    IList fields = SearchGenericUtils.GetSearchContentFields(typeof(T), searchContent);
    for(int i = 0; i < fields.Count ; i++)
    {
        SearchContentFieldInfo fi = fields[i];
        switch (fi.FieldType)
        {
            case SearchContentFieldType.Text:
                doc.Add(Field.Text(fi.Name, fi.Value));
                break;
            case SearchContentFieldType.UnStored:
                doc.Add(Field.UnStored(fi.Name, fi.Value));
                break;
            case SearchContentFieldType.UnIndexed:
                doc.Add(Field.UnIndexed(fi.Name, fi.Value));
                break;
            case SearchContentFieldType.Keyword:
                doc.Add(Field.Keyword(fi.Name, fi.Value));
                break;
            default:
                break;
        }
    }
    return doc;
}


public void DeleteContent(T searchContent)
{
    if (this._rebuildIndex)
    {
        throw new InvalidOperationException("Cannot delete documents when rebuilding the index.");
    }
    else
    {
        this._indexWriter.Close();
        this._indexWriter = null;

        // Search content key field uniquely identifies a document in the index.
        SearchContentFieldInfo ki = SearchGenericUtils.GetSearchContentKeyFieldInfo(typeof(T), searchContent);
        if (String.IsNullOrEmpty(ki.Name))
            throw new Exception("SearchContentKey Field not specified on target class!");

        Term term = new Term(ki.Name, ki.Name);
        IndexReader rdr = IndexReader.Open(this._indexDirectory);
        rdr.Delete(term);
        rdr.Close();
    }
}

IndexQuery<T> 类

Cuyahoga.Core.Search.IndexQuery 类的重构泛型版本。这个类用作 Lucene.NET 索引搜索功能的包装器。`Find` 方法已被修改,以便我们可以
创建内容对象的实例并通过反射设置属性值。

public SearchResultCollection<T> Find(string queryText, Hashtable keywordFilter, int pageIndex, int pageSize)
{
    long startTicks = DateTime.Now.Ticks;
    //We get query fileds with reflection
    string[] qryFields = SearchGenericUtils.GetSearchContentQueryFields(typeof(T));
    if (qryFields.Length == 0)
        throw new Exception("No query field specified on target class!");

    Query query = MultiFieldQueryParser.Parse(queryText, qryFields, new StandardAnalyzer());
    IndexSearcher searcher = new IndexSearcher(this._indexDirectory);
    Hits hits;
    if (keywordFilter != null && keywordFilter.Count > 0)
    {
        QueryFilter qf = BuildQueryFilterFromKeywordFilter(keywordFilter);
        hits = searcher.Search(query, qf);
    }
    else
    {
        hits = searcher.Search(query);
    }
    int start = pageIndex * pageSize;
    int end = (pageIndex + 1) * pageSize;
    if (hits.Length() <= end)
    {
        end = hits.Length();
    }
    SearchResultCollection<T> results = new SearchResultCollection<T>();
    results.TotalCount = hits.Length();
    results.PageIndex = pageIndex;

    //We get filds that will be populated as a result of the search via reflection
    string[] resultFields = SearchGenericUtils.GetSearchContentResultFields(typeof(T));
    for (int i = start; i < end; i++)
    {
      // We create instance of the target type with Activator and set field(property) values with reflection
        T instance = Activator.CreateInstance<T>();
        for (int j = 0; j < resultFields.Length; j++)
        {
            SearchGenericUtils.SetSearchResultField(instance, resultFields[j], hits.Doc(i).Get(resultFields[j]));
        }

        //If target type implements ISearchResultStat we set Boost and Score properties of the instance
        if (instance is ISearchResultStat)
        {
            SearchGenericUtils.SetSearchResultField(instance, "Boost", hits.Doc(i).GetBoost());
            SearchGenericUtils.SetSearchResultField(instance, "Score", hits.Score(i));
        }
        results.Add(instance);
    }

    searcher.Close();
    results.ExecutionTime = DateTime.Now.Ticks - startTicks;
    return results;
}

SearchContentFieldAttribute 类

.NET 属性是声明式编程的绝佳实现。我们使用 SearchContentFieldAttribute 来标识我们将如何对我们的内容执行全文索引。我们全文索引中的字段直接映射到我们自定义内容类型的属性。例如,如果我们想索引一个 Person 类,我们将指定该类的哪些属性将被索引,它们如何被索引,以及哪些属性将被设置为 SearchContentFieldAttribute 的全文索引搜索结果。

SearchContentFieldAttribute 用于声明

  • 字段类型:需要此声明,因为 Lucene.NET 有不同的字段声明。有关详细信息,请参阅 Lucene.Net.Documents 命名空间中的 `Field` 类。
    Lucene.Net.Documents 命名空间
  • 键字段:键字段用于唯一标识我们的内容。最多允许声明一个键字段。如果您在内容类上将两个属性
    声明为键字段,则只使用第一个。
  • 结果字段:IsResultField=true 的内容类属性将在索引搜索后自动设置,其他字段将不被设置。
  • 查询字段:IsQueryField=true 的内容类属性将在索引搜索中使用。索引搜索将仅在这些
    属性
namespace Bo.Cuyahoga.Extensions.Search
{
    [AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
    public class SearchContentFieldAttribute : Attribute
    {
        private SearchContentFieldType _fieldType;
        /// 
        /// Default is Text
        /// 
        public SearchContentFieldType FieldType
        {
            get { return _fieldType; }
            set { _fieldType = value; }
        }

        private bool _isResultField = true;
        /// 
        /// Default is true
        /// 
        public bool IsResultField
        {
            get { return _isResultField; }
            set { _isResultField = value; }
        }

        private bool _isQueryField = false;
        /// 
        /// Default is false
        /// 
        public bool IsQueryField
        {
            get { return _isQueryField; }
            set { _isQueryField = value; }
        }

        private bool _isKeyField = false;
        /// 
        /// Default is false
        /// 
        public bool IsKeyField
        {
            get { return _isKeyField; }
            set { _isKeyField = value; }
        }

        public SearchContentFieldAttribute(SearchContentFieldType fieldType)
        {
            _fieldType = fieldType;
        }

    }

    public enum SearchContentFieldType
    {
        Text,
        UnStored,
        UnIndexed,
        Keyword
    }
}

SearchResultCollection<T> 类

这是 Cuyahoga.Core.Search 命名空间中 SearchResultCollection 类的泛型版本。关于此类没有什么特别需要说明的。

ISearchResultStat 接口

我们使用此接口来指定我们的自定义内容类型将保存查询统计信息,如 Boost 和 Score。如果我们的自定义内容类型实现了此接口,则在索引查询后将设置 Boost 和 Score 属性的值。

public interface ISearchResultStat
{
    float Boost{get;set;}
    float Score{ get;set;}
}

ReflectionHelper 单例

为提高性能而设计的反射帮助类

/// 
    /// Singleton reflection helper
    /// 
    public sealed class ReflectionHelper
    {
        #region Static Fields And Properties

        static ReflectionHelper _instance = null;
        static readonly object _padlock = new object();
        public static ReflectionHelper Instance
        {
            get
            {
                lock (_padlock)
                {
                    if (_instance == null)
                    {
                        _instance = new ReflectionHelper();
                    }
                    return _instance;
                }
            }
        }
        
        #endregion Static Fields And Properties

        #region Instance Fields And Properties
        
        private Dictionary> _cache;
        
        #endregion Instance Fields And Properties

        #region CTOR

        private ReflectionHelper()
        {
            _cache = new Dictionary>();
        }

        #endregion //CTOR

        #region Reflection Helper Methods

        public SearchContentFieldInfo[] GetKeyFields(Type t)
        {
            if (!_cache.ContainsKey(t))
                AddTypeToCache(t);


            List keyFields =  _cache[t].FindAll(
                delegate(SearchContentFieldInfo fi)
                {
                    return fi.IsKeyField == true;
                });
            return keyFields.ToArray();
        }

        public SearchContentFieldInfo[] GetKeyFields(Type t, object instance)
        {
            SearchContentFieldInfo[] fields = GetKeyFields(t);
            if (instance == null)
                return fields;

            GenericGetter getMethod;
            for (int i = 0; i < fields.Length; i++)
            {
                getMethod = CreateGetMethod(fields[i].PropertyInfo);
                object val = getMethod(instance);
                fields[i].Value = val == null ? String.Empty : val.ToString();
            }
            return fields;
        }

        public IList GetFields(Type t, object instance)
        {
            if (!_cache.ContainsKey(t))
                AddTypeToCache(t);

            Listresult = new List(_cache[t].ToArray());

            if (instance == null)
                return result;
            
            GenericGetter getMethod;
            for (int i = 0; i < result.Count; i++)
            {
                SearchContentFieldInfo fi = result[i];
                getMethod = CreateGetMethod(fi.PropertyInfo);
                object val = getMethod(instance);
                fi.Value = val == null ? String.Empty : val.ToString();
            }
            return result;
        }

        public string[] GetQueryFields(Type t)
        {
            if (!_cache.ContainsKey(t))
                AddTypeToCache(t);

            List < SearchContentFieldInfo > qryFields = _cache[t].FindAll(
                delegate(SearchContentFieldInfo fi)
                {
                    return fi.IsQueryField == true;
                });

            
            List result = qryFields.ConvertAll(
                delegate(SearchContentFieldInfo fi) 
                {
                    return fi.Name;
                });

            return result.ToArray();
        }

        public string[] GetResultFields(Type t)
        {
            if (!_cache.ContainsKey(t))
                AddTypeToCache(t);

            List qryFields = _cache[t].FindAll(
                delegate(SearchContentFieldInfo fi)
                {
                    return fi.IsResultField == true;
                });


            List result = qryFields.ConvertAll(
                delegate(SearchContentFieldInfo fi)
                {
                    return fi.Name;
                });

            return result.ToArray();
        }

        public void SetSearchResultField(string fieldName, object instance, object value)
        {
            Type t = instance.GetType();
            if (!_cache.ContainsKey(t))
                AddTypeToCache(t);
            
            SearchContentFieldInfo field = _cache[t].Find(
            delegate(SearchContentFieldInfo fi)
            {
                return fi.Name == fieldName;
            });

            if (field.Name != fieldName)
                throw new Exception(String.Format("Field with name \"{0}\" not found on type \"{1}\"!", fieldName, t));

            GenericSetter setter = CreateSetMethod(field.PropertyInfo);
            setter(instance, Convert.ChangeType(value,field.PropertyInfo.PropertyType));
        }

        #endregion //Reflection Helper Methods

        #region Cache Operations
        private void AddTypeToCache(Type t)
        {
            if (_cache.ContainsKey(t))
                return;

            List fields = new List();

            PropertyInfo[] props = t.GetProperties(BindingFlags.Public | BindingFlags.Instance);
            for (int i = 0; i < props.Length; i++)
            {
                PropertyInfo pi = props[i];
                SearchContentFieldAttribute[] atts = (SearchContentFieldAttribute[])pi.GetCustomAttributes(typeof(SearchContentFieldAttribute), true);
                if (atts.Length > 0)
                {
                    SearchContentFieldInfo fi = new SearchContentFieldInfo();
                    fi.Name = pi.Name;
                    fi.FieldType = atts[0].FieldType;
                    fi.IsKeyField = atts[0].IsKeyField;
                    fi.IsResultField = atts[0].IsResultField;
                    fi.IsQueryField = atts[0].IsResultField;
                    fi.PropertyInfo = pi;
                    fields.Add(fi);
                }
            }
            if (fields.Count > 0)
                _cache.Add(t, fields);
        }
        #endregion //Cache Operations

        #region Emit Getter/Setter

        /* Source for CreateSetMethod and CreateGetMethod taken from
         * http://jachman.wordpress.com/2006/08/22/2000-faster-using-dynamic-method-calls/
         */
        private GenericSetter CreateSetMethod(PropertyInfo propertyInfo)
        {
            /*
            * If there’s no setter return null
            */
            MethodInfo setMethod = propertyInfo.GetSetMethod();
            if (setMethod == null)
                return null;

            /*
            * Create the dynamic method
            */
            Type[] arguments = new Type[2];
            arguments[0] = arguments[1] = typeof(object);

            DynamicMethod setter = new DynamicMethod(
                String.Concat("_Set", propertyInfo.Name, "_"),
                typeof(void), arguments, propertyInfo.DeclaringType);
            ILGenerator generator = setter.GetILGenerator();
            generator.Emit(OpCodes.Ldarg_0);
            generator.Emit(OpCodes.Castclass, propertyInfo.DeclaringType);
            generator.Emit(OpCodes.Ldarg_1);

            if (propertyInfo.PropertyType.IsClass)
                generator.Emit(OpCodes.Castclass, propertyInfo.PropertyType);
            else
                generator.Emit(OpCodes.Unbox_Any, propertyInfo.PropertyType);

            generator.EmitCall(OpCodes.Callvirt, setMethod, null);
            generator.Emit(OpCodes.Ret);

            /*
            * Create the delegate and return it
            */
            return (GenericSetter)setter.CreateDelegate(typeof(GenericSetter));
        }

        ///
        /// Creates a dynamic getter for the property
        ///
        private static GenericGetter CreateGetMethod(PropertyInfo propertyInfo)
        {
            /*
            * If there’s no getter return null
            */
            MethodInfo getMethod = propertyInfo.GetGetMethod();
            if (getMethod == null)
                return null;

            /*
            * Create the dynamic method
            */
            Type[] arguments = new Type[1];
            arguments[0] = typeof(object);

            DynamicMethod getter = new DynamicMethod(
                String.Concat("_Get", propertyInfo.Name, "_"),
                typeof(object), arguments, propertyInfo.DeclaringType);
            ILGenerator generator = getter.GetILGenerator();
            generator.DeclareLocal(typeof(object));
            generator.Emit(OpCodes.Ldarg_0);
            generator.Emit(OpCodes.Castclass, propertyInfo.DeclaringType);
            generator.EmitCall(OpCodes.Callvirt, getMethod, null);

            if (!propertyInfo.PropertyType.IsClass)
                generator.Emit(OpCodes.Box, propertyInfo.PropertyType);

            generator.Emit(OpCodes.Ret);

            /*
            * Create the delegate and return it
            */
            return (GenericGetter)getter.CreateDelegate(typeof(GenericGetter));
        }
        #endregion //Emit Getter/Setter
    }

    public delegate void GenericSetter(object target, object value);
    public delegate object GenericGetter(object target);

SearchGenericUtils 静态类

public class SearchContentFieldInfo
    {
        public string Name;
        public string Value;
        public SearchContentFieldType FieldType;
        public bool IsResultField;
        public bool IsQueryField;
        public bool IsKeyField;
        public PropertyInfo PropertyInfo;
    }

    internal static class SearchGenericUtils
    {
        internal static SearchContentFieldInfo GetSearchContentKeyFieldInfo(Type t, object instance)
        {
            SearchContentFieldInfo[] keyFields =  ReflectionHelper.Instance.GetKeyFields(t,instance);
            
            if (keyFields.Length == 0)
                throw new Exception(String.Format("No key filed defined for type {0}!", t));
            
            if(keyFields.Length > 1)
                throw new Exception(String.Format("Only one key filed allowed for type {0}!", t));

            return keyFields[0];
        }

        internal static SearchContentFieldInfo GetSearchContentKeyFieldInfo(Type t)
        {
            return GetSearchContentKeyFieldInfo(t, null);
        }

        internal static IList GetSearchContentFields(Type t, object instance)
        {
            if (instance == null)
                throw new Exception("Instance parameter is null!");

            return ReflectionHelper.Instance.GetFields(t, instance);
        }

        internal static string[] GetSearchContentQueryFields(Type t)
        {
            return ReflectionHelper.Instance.GetQueryFields(t);
        }

        internal static string[] GetSearchContentResultFields(Type t)
        {
            return ReflectionHelper.Instance.GetResultFields(t);
        }

        internal static void SetSearchResultField(object instance , string fieldName, object value)
        {

            if (instance == null)
                throw new Exception("Object instance parameter is null!");
            if (String.IsNullOrEmpty(fieldName))
                throw new Exception("Field name is empty!");

            ReflectionHelper.Instance.SetSearchResultField(fieldName, instance, value);    
        }


    }

使用示例代码

我们的示例控制台应用程序简单地创建了三个 PersonContent 对象,并对这些对象进行了全文索引。然后,我们对已索引的内容执行查询。

在示例应用程序中,PersonContent 类将用作我们的自定义内容类型。该类如下所示:

public class PersonContent
{
    private string _id;

    [SearchContentField(SearchContentFieldType.Keyword,IsKeyField=true)]
    public string Id
    {
        get { return _id; }
        set { _id = value; }
    }

    private string _keyword;
    [SearchContentField(SearchContentFieldType.Keyword)]
    public string Keyword
    {
        get { return _keyword; }
        set { _keyword = value; }
    }

    private string _fullName;
    [SearchContentField(SearchContentFieldType.Text,IsQueryField=true)]
    public string FullName
    {
        get { return _fullName; }
        set { _fullName = value; }
    }

    private string _notes;
    [SearchContentField(SearchContentFieldType.UnStored,IsResultField=false,IsQueryField=true)]
    public string Notes
    {
        get { return _notes; }
        set { _notes = value; }
    }

    private int _age;
    [SearchContentField(SearchContentFieldType.UnIndexed)]
    public int Age
    {
        get { return _age; }
        set { _age = value; }
    }

    public override string ToString()
    {
        StringBuilder sb = new StringBuilder();
        sb.AppendLine("Id = " + _id);
        sb.AppendLine("FullName = " + _fullName);
        sb.AppendLine("Age = " + _age.ToString());
        
        return sb.ToString();
    }
}

注意:有关 UnIndexed、UnStored、Text 和 Keyword 含义的详细信息,请阅读
Lucene.NET 文档。`SearchContentFieldType` 是一个实用枚举,它使我们能够调用 Lucene.Net.Documents 命名空间中 `Field` 的相应方法。

将扩展搜索类合并到 Cuyahoga Core 的技巧

您无需将这些扩展直接合并到 Cuyahoga.Core 即可使用它们。
但是,如果您希望 Cuyahoga.Core 以更通用的方式处理全文索引,您只需用 Bo.Cuyahoga.Extensions.Search 命名空间中的对应类替换 IndexQuery、IndexBuilder、SearchCollection、ISearchable、IndexEventHandler 和 IndexEventArgs 类即可。显然,您还需要重构使用旧版本类的其他部分。您必须考虑的最后一个问题是,您将不得不向 SearchContent 和 SearchResult 类添加属性。

依赖项

要编译此代码,您需要
  • Cuyahoga.Core 项目
  • Lucene.Net.ddl
  • log4net.dll

历史

  • 2008 年 3 月 27 日
    • 已解决 DeleteContent 方法中的错误
    • 如果在 ReflectionHelper 类的 GetKeyFields、GetFields 和 SetSearchResultField 方法中存在 null 的 getter 或 setter 方法,则会引发异常
  • 2008 年 3 月 7 日:发布第一个版本
  • 2008 年 3 月 10 日
    • 添加了 ReflectionHelper 类以提高性能。
    • 修改了 SearchContentFieldInfo 方法以调用相应的 ReflectionHelper 方法。
    • SearchContentFieldInfo 不再是 struct。

© . All rights reserved.