160  
查询码:00000401
基于Lucene.Net构建.NET Core版搜索引擎(九)--Facet维度统计
作者: 潘帅 于 2021年10月08日 发布在分类 / 人防组 / 人防后端 下,并于 2021年10月08日 编辑
Lucene.NET 搜索引擎

前面几篇文章讲了索引创建、检索等一系列操作,说到底索引这个东西就是为了更快的查询信息,常见的模糊搜索可以实现,那么统计分析作为一种特殊的搜索当然也可以实现。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)的形式展示出来的效果是:




 推荐知识

 历史版本

修改日期 修改人 备注
2021-10-08 13:04:23[当前版本] 潘帅 1.0

 附件

附件类型

PNGPNG

知识分享平台 -V 4.8.7 -wcp