前面几篇文章讲了索引创建、检索等一系列操作,说到底索引这个东西就是为了更快的查询信息,常见的模糊搜索可以实现,那么统计分析作为一种特殊的搜索当然也可以实现。Lucene.NET提供了Facet相关类用于维度统计分析。
Facet怎么理解?翻译过来是方面、平面、部分。可以理解为数据对象中的属性或维度。以用于描述自然人的数据对象为例,我们为某个地区的自然人建立数据模型对象,通过性别、年龄、民族、籍贯、学历、政治面貌、婚姻状况等属性(或者称为维度)来描述一个人的信息。每个属性或维度可以看作这个人的信息的不同方面。从统计分析的角度看,假设这个地区有1万人,我们可以分别从性别、年龄、民族、籍贯、学历、政治面貌、婚姻状况这几个维度进行统计,对这个1万人的集合进行描述。
把Facet按照维度来翻译或理解的另一个好处是,一个数据对象有几个Facet就可以认为它是一个几维数据。自然人用性别、年龄、民族、籍贯、学历、政治面貌、婚姻状况用7个属性描述,就可以认为是7维数据,每个自然人都可以用一个7维向量描述。我们甚至可以通过计算两个7维向量的余弦值也就是夹角来判断两个人的信息有多相近。
另外从Facet本身的特点来讲,Facet所表示的属性值的取值范围应该是有限可数的,而不应该是无限不可数。比如性别的取值范围是男、女,将其作为Facet是可行的,但是姓名就没有明确的取值范围,将其作为Facet就意义不大。
1.安装Lucene.NET.Facet
使用NuGet管理器安装Lucene,NET.Facet,版本与之前的Lucene.NET版本保持一致。
2.为实体创建Facet索引
为了更好的体现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索引存储。
3.使用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)的形式展示出来的效果是: