扩展 Cuyahoga 全文索引 (Lucene.NET)
在本文中,我们将扩展 Cuyahoga.Core.Search 命名空间中的类,以提供更通用的全文索引服务
引言
Cuyahoga 为模块开发人员提供了一个易于使用的包装器类,用于使用 Lucene.NET 进行全文索引。
尽管包装器类易于使用,但这些类本身并不通用。我的意思是
我们作为模块开发人员,必须将自定义内容(无论是静态还是动态)
转换为 SearchContent 对象,以便包装器类可以索引该内容。另一个类似的限制是,当我们对已索引的内容进行搜索时,我们只能获得 SearchResult 对象的集合。我还要提一下,我们作为模块开发人员,无法控制哪些字段将被存储/不存储,哪些字段将被用作关键字,或者在包装器对象构建全文索引时哪些字段不会被索引。
在本文中,我将尝试向您展示如何通过提供一个单独的扩展程序集来使 Cuyahoga.Core 的全文索引功能更加通用。
背景
我使用 Cuyahoga 框架创建了两个网站
- PragmaSQL Online:PragmaSQL T-SQL 编辑器的产品网站
- BenimOdam.com (土耳其语):一个室友广告和搜索网站。
在开发这些网站时,我需要对某些动态内容进行全文索引,即持久化在数据库中的内容。在探索了 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。