前面几篇文章讲了索引创建、检索等一系列操作,说到底索引这个东西就是为了更快的查询信息,常见的模糊搜索可以实现,那么统计分析作为一种特殊的搜索当然也可以实现。Lucene.NET提供了Facet相关类用于维度统计分析。
Facet怎么理解?翻译过来是方面、平面、部分。可以理解为数据对象中的属性或维度。以用于描述自然人的数据对象为例,我们为某个地区的自然人建立数据模型对象,通过性别、年龄、民族、籍贯、学历、政治面貌、婚姻状况等属性(或者称为维度)来描述一个人的信息。每个属性或维度可以看作这个人的信息的不同方面。从统计分析的角度看,假设这个地区有1万人,我们可以分别从性别、年龄、民族、籍贯、学历、政治面貌、婚姻状况这几个维度进行统计,对这个1万人的集合进行描述。
把Facet按照维度来翻译或理解的另一个好处是,一个数据对象有几个Facet就可以认为它是一个几维数据。自然人用性别、年龄、民族、籍贯、学历、政治面貌、婚姻状况用7个属性描述,就可以认为是7维数据,每个自然人都可以用一个7维向量描述。我们甚至可以通过计算两个7维向量的余弦值也就是夹角来判断两个人的信息有多相近。
另外从Facet本身的特点来讲,Facet所表示的属性值的取值范围应该是有限可数的,而不应该是无限不可数。比如性别的取值范围是男、女,将其作为Facet是可行的,但是姓名就没有明确的取值范围,将其作为Facet就意义不大。
使用NuGet管理器安装Lucene,NET.Facet,版本与之前的Lucene.NET版本保持一致。
为了更好的体现Lucene.NET.Facet的特性,搞点真实数据用于演示。从天眼查网站抓取了部分企业的公开信息。
根据这些信息创建实体结构Company:
/// <summary> /// 企业实体类 /// </summary> public class Company : AuditedEntity<string> { /// <summary> /// 企业名称 /// </summary> [Index(FieldName = "CompanyName", FieldType = FieldDataType.Text,IsStore = Field.Store.YES)] public virtual string CompanyName { get; set; } /// <summary> /// 法人代表 /// </summary> [Index(FieldName = "Representative", FieldType = FieldDataType.String, IsStore = Field.Store.YES)] public virtual string Representative { get; set; } /// <summary> /// 注册资本 /// </summary> [Index(FieldName = "CapitalAmount", FieldType = FieldDataType.Int32, IsStore = Field.Store.YES)] public virtual int CapitalAmount { get; set; } /// <summary> /// 成立日期 /// </summary> [Index(FieldName = "EnrollDate", FieldType = FieldDataType.DateTime, IsStore = Field.Store.YES)] public virtual DateTime EnrollDate { get; set; } /// <summary> /// 联系电话 /// </summary> public virtual string PhoneNum { get; set; } /// <summary> /// 所属行业 /// </summary> [Index(FieldName = "Industry", FieldType = FieldDataType.Facet, IsStore = Field.Store.YES)] public virtual string Industry { get; set; } /// <summary> /// 存续状态 /// </summary> [Index(FieldName = "Status", FieldType = FieldDataType.Facet, IsStore = Field.Store.YES)] public virtual string Status { get; set; } }其中枚举FieldDataType增加了新的取值项Facet,用于标识需要作为维度索引进行存储的字段。
维度索引对应的是FacetField,其创建方式与常见的StringField、TextField不太一样,存储位置也不同。
using (DirectoryTaxonomyWriter taxonomyWriter = new DirectoryTaxonomyWriter(TaxoDirectory)) { FacetsConfig facetsConfig = new FacetsConfig(); doc.Add(new FacetField("名称",“值”)); doc = facetsConfig.Build(taxonomyWriter, doc); } writer.AddDocument(doc);区别在于需要使用DirectoryTaxonomyWriter实例,与常规的IndexWriter独立,存储路径也是单独的。但索引的写入仍是由IndexWriter实现的。
完整代码如下:
public virtual void CreateIndexByEntity(IEntity<string> entity,bool isFiltered=true,bool isCreate = true) { var config = new IndexWriterConfig(LuceneVersion.LUCENE_48, Analyzer); if (isCreate) { config.OpenMode = OpenMode.CREATE; } else { config.OpenMode = OpenMode.CREATE_OR_APPEND; } using (IndexWriter writer = new IndexWriter(Directory, config)) { Document doc = new Document(); //创建文档 var type = entity.GetType(); //为实体所在的类名和Id创建Field,目的是对实体进行标识,便于以后检索 //文档Document是域Field的集合,本身并无标识。通过添加Id的域对文档进行标识。 //检索结束后可以从匹配的文档中反向找出对应的数据库实体,与业务建立关联 doc.Add(new StringField(CoreConstant.EntityType, type.AssemblyQualifiedName, Field.Store.YES));//添加表名/类名的域 doc.Add(new StringField(CoreConstant.EntityId, entity.Id, Field.Store.YES));//添加记录Id/标识的域 var properties = type.GetProperties(); //遍历实体的成员集合 foreach (var propertyInfo in properties) { var propertyValue = propertyInfo.GetValue(entity); if (propertyValue == null) { continue; } string fieldName = propertyInfo.Name;//成员字段名称 if (isFiltered) { var attributes = propertyInfo.GetCustomAttributes<IndexAttribute>();//获取自定义属性集合 foreach (var attribute in attributes) { string name = string.IsNullOrEmpty(attribute.FieldName) ? fieldName : attribute.FieldName; switch (attribute.FieldType) { case FieldDataType.DateTime: doc.Add(new StringField(fieldName, ((DateTime)propertyValue).ToString("yyyy-MM-dd HH:mm:ss"), attribute.IsStore)); break; case FieldDataType.DateYear: doc.Add(new StringField(fieldName, propertyValue.ToString(), attribute.IsStore)); break; case FieldDataType.Int32: doc.Add(new Int32Field(fieldName, (Int32)propertyValue, attribute.IsStore)); break; case FieldDataType.Int64: doc.Add(new Int64Field(fieldName, (Int64)propertyValue, attribute.IsStore)); break; case FieldDataType.Double: doc.Add(new DoubleField(fieldName, (double)propertyValue, attribute.IsStore)); break; case FieldDataType.Html: doc.Add(new TextField(fieldName, propertyValue.ToString().ClearHtml(), attribute.IsStore)); break; case FieldDataType.Json: doc.Add(new TextField(fieldName, propertyValue.ToString().ClearJson(), attribute.IsStore)); break; case FieldDataType.Xml: doc.Add(new TextField(fieldName, propertyValue.ToString().ClearXml(), attribute.IsStore)); break; case FieldDataType.Csv: doc.Add(new TextField(fieldName, propertyValue.ToString().ClearCsv(), attribute.IsStore)); break; case FieldDataType.Dic: Dictionary<string, string> dic = propertyValue.ToString() .ToObject<Dictionary<string, string>>(); foreach (var kv in dic) { doc.Add(new TextField(kv.Key, kv.Value, attribute.IsStore)); } break; case FieldDataType.Text: doc.Add(new TextField(fieldName, propertyValue.ToString(), attribute.IsStore)); break; case FieldDataType.Facet: doc.Add(new FacetField(fieldName, propertyValue.ToString())); break; default: doc.Add(new StringField(fieldName, propertyValue.ToString(), attribute.IsStore)); break; } } } else { switch (propertyValue) { case DateTime time: doc.Add(new StringField(fieldName, time.ToString("yyyy-MM-dd HH:mm:ss"), Field.Store.YES)); break; case int num: doc.Add(new Int32Field(fieldName, num, Field.Store.YES)); break; case long num: doc.Add(new Int64Field(fieldName, num, Field.Store.YES)); break; case double num: doc.Add(new DoubleField(fieldName, num, Field.Store.YES)); break; default: doc.Add(new TextField(fieldName, propertyValue.ToString(), Field.Store.YES)); break; } } } using (DirectoryTaxonomyWriter taxonomyWriter = new DirectoryTaxonomyWriter(TaxoDirectory)) { FacetsConfig facetsConfig = new FacetsConfig(); doc = facetsConfig.Build(taxonomyWriter, doc); } if (writer.Config.OpenMode == OpenMode.CREATE) { writer.AddDocument(doc); } else { writer.UpdateDocument(new Term(CoreConstant.EntityId, entity.Id), doc); } //刷新索引 writer.Flush(true, true); writer.Commit(); } }
把从网上拿到的公开数据批量导入并创建索引,将企业的所属行业和存续状态作为Facet索引存储。
定义Facet检索方法:
public FacetSearchResult FacetSearch(FacetSearchOption option) { FacetSearchResult result = new FacetSearchResult(); using (DirectoryReader reader=DirectoryReader.Open(Directory)) { DirectoryTaxonomyReader taxonomyReader = new DirectoryTaxonomyReader(TaxoDirectory); IndexSearcher searcher = new IndexSearcher(reader); FacetsCollector facetsCollector = new FacetsCollector(); FacetsCollector.Search(searcher, new MatchAllDocsQuery(), 10, facetsCollector); Facets facets = new FastTaxonomyFacetCounts(taxonomyReader, new FacetsConfig(), facetsCollector); foreach (var field in option.Fields) { FacetResult facetResult = facets.GetTopChildren(option.MaxHits, field); result.Items.Add(facetResult); } } return result; }其中输入参数为:
public class FacetSearchOption { public virtual List<string> Fields { get; set; } public virtual int MaxHits { get; set; } public FacetSearchOption() { Fields = new List<string>(); MaxHits = 10; } public FacetSearchOption(List<string> fields,int maxHits=10) { Fields = fields; MaxHits = maxHits; } }输出参数为:
public class FacetSearchResult { public virtual IList<FacetResult> Items { get; set; } public FacetSearchResult() { Items = new List<FacetResult>(); } }
以统计所有企业的所属行业分布为例:
public List<CountSearchResultItem> Industry() { List<CountSearchResultItem> items = new List<CountSearchResultItem>(); FacetSearchOption option = new FacetSearchOption(); option.Fields.Add("Industry"); FacetSearchResult result = _searchManager.FacetSearch(option); var facet = result.Items[0]; foreach (var lv in facet.LabelValues) { CountSearchResultItem item = new CountSearchResultItem(); item.Name = lv.Label; item.Value = lv.Value.To<int>(); items.Add(item); } return items; }查询结果为:
{ "status": { "code": 200, "message": "操作成功" }, "result": [ { "name": "商务服务业", "value": 22 }, { "name": "批发业", "value": 18 }, { "name": "科技推广和应用服务业", "value": 9 }, { "name": "软件和信息技术服务业", "value": 9 }, { "name": "零售业", "value": 9 }, { "name": "研究和试验发展", "value": 8 }, { "name": "建筑装饰、装修和其他建筑业", "value": 4 }, { "name": "娱乐业", "value": 3 }, { "name": "文化艺术业", "value": 2 }, { "name": "道路运输业", "value": 2 } ] }把上述结果用可视化图表(Echarts)的形式展示出来的效果是: