从零开始搭建.NET Core版搜索引擎(九)–Facet维度查询

  • Post author:
  • Post category:其他


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

在这里插入图片描述


项目地址:

https://github.com/ludewig/Muyan.Search



版权声明:本文为lordwish原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。