Elasticsearch 笔记

用 ES 做实体召回

Posted by Pelhans on November 17, 2019

ES 简介

官方文档内容很全,推荐阅读 ES 官方文档

Elasticsearch 是一个分布式、可扩展、实时的搜索与数据分析引擎。基于 Lucene 作为其核心来实现所有索引和搜索的功能。原本 Lucene 只是一个库, 想要使用它必须用 Java 语言将其集成到你的应用中, 麻烦在于 Lucene 比较复杂, 使用者必须深入了解检索的相关知识来理解它是如何工作的。但 ES 通过简单的 RESTful API 来隐藏 Lucene 的复杂性, 从而让全文搜索变得简单。

ES 除了基于 Lucene 上开发的搜索引擎外,还提供了:

  • 分布式的实时文件存储,每个字段都被索引并且可被搜索
  • 实时分析的分布式搜索引擎
  • 可以扩展到上百台服务器,处理 PB 级结构化或非结构化数据

除此之外, ES 是高度可定制化的,你可以根据不同领域需求定制 ES 的高级特性。

ES 核心概念

  • 集群:一个集群就是由一个或多个节点组织在一起,他们共同持有你的整个数据,并一起提供索引和搜索功能。一个集群由一个唯一的名字标识,一个节点只能通过指定某个集群的名字,来加入这个集群。
  • 节点:一个节点是你集群中的一个服务器,作为集群的一部分,它存储你的数据,参与集群的索引和搜索功能。节点也是由一个名字来标识的。
  • 索引:一个索引就是一个拥有积分相似特征的文档的集合。一个索引由一个名字来标识(必须全部是小写字母的),并且当我们要对对应于这个索引中的文档进行索引、搜索、更新和删除的时候,都要使用到这个名字。对应到数据库中的概念的话,应该是 Database。
  • 类型:在一个索引中,你可以定义一种或多种类型。一个类型是你的索引的一个逻辑上的分类/分区。通常,会为具有一组共同字段的文档定义一个类型。对应数据库中的 Table。
  • 文档:一个文档是一个可被索引的基础信息单元,文档以 JSON 格式来标识,文档类似于关系型数据库中Record的概念。ES 会为一个文档添加如 _index、_type和_id 等用户数据以外的字段。
  • 分片:Elasticsearch提供了将索引划分成多份的能力,当你创建一个索引的时候,你可以指定你想要的分片的数量。每个分片本身也是一个功能完善并且独立的“索引”,这个“索引”可以被放置到集群中的任何节点上。
  • 复制:为了数据安全, ES 允许用户创建分片的一份或多份拷贝。同时复制还可以通过在所有复制上并行运行搜索,扩展搜索量/吞吐量。默认情况下,Elasticsearch中的每个索引被分片5个主分片和1个复制。

简单查询原理

倒排索引

倒排索引指的是,把文档拆分成一个个单词,每个单词指向包含该单词的文档ID。在查询时,根据关键字,找到包含该关键字的文档ID列表。再根据ID读取具体的数据。

比如现在库里有三个文档

  • doc0: “it is what it is”
  • doc1: “what is it”
  • doc2: “it is a banana”

那么我们就可以得到如下的反向文件索引

"a": {2}    
"banana": {2}    
"is": {0, 1, 2}    
"it": {0, 1, 2}    
"what": {0, 1}

当查询 what 这个词时,我们可以定位到 0 和 1 这两个文档。当多个查询且要求必须匹配的话,则会取对应文档的交集。

接下来的问题是,建立索引和查询的基本单位是什么呢?颗粒度太小,如字那么返回的结果会太多,没什么意义。对于中文来说,一般查询的基本单位是词。 ES 自带的中文分词不太够用,一般采用 ik 中文切词插件。在https://github.com/medcl/elasticsearch-analysis-ik上找到我们elasticSearch对应的版本,然后对应安装就行了。 ik 还可以自定义词典和切割模式,很好用。

查询评分

对于多术语查询,Lucene采用布尔模型(Boolean model)、词频/逆向文档频率(TF/IDF)、以及向量空间模型(Vector Space Model),然后将他们合并到单个包中来收集匹配文档和分数计算。 只要一个文档与查询匹配,Lucene 就会为查询计算分数,然后合并每个匹配术语的分数。

布尔检索

根据多个查询项之间的关系(and/ or/ not) 来查找候选文档。and 表示同时出现, Or 表示出现一个就可以,not 表示不包含。布尔检索优点是系统构建简单,缺点是查询构建比较复杂,同时没有考虑查询词间的重要性等因素。

实用评分函数

其中

  • 查询正则化: 试图将查询进行正则化来达到比较不同查询结果的目的,但其实比较不同查询结果的相关度分数并没有什么意义,同一查询结果间的相对分值更有用。
  • 协调因子: 文档里出现的查询术语越多,协调因子就会越大,结果也往往更好。
  • 求和是对文档中每个与查询匹配的 t 进行遍历。
  • tf(t):术语在文档中出现的频次,
  • :逆向文档频率的平方, 其中
  • t.getBoost():查询中使用的 boost,默认为1,用户也可以进行设置,使得某一个查询 term 比其他的更重要。但需要注意的是, 权重提升值会经过正则化和一些其他的内部优化过程,boost 为 2 的最终分数并不代表是 boost 为1 的2倍。但还是提高了的,也表明你想让该term 比 boost 比它低的 更重要。这一项很重要,因为大部分情况下简单查询配合 boost 就能获得不错的召回效果,但在实际应用中,无法通过简单的公式得出某个特定查询语句的 “正确” 权重提升值,只能通过不断尝试获得。需要记住的是 boost 只是影响相关度评分的其中一个因子;它还需要与其他因子相互竞争。
  • 字段长度正则值:t 所在的字段越短权值越高,如 term 出现在标题中就会比出现在内容中的相关度更高,

向量空间模型

每个词都用向量表示,查询时计算向量间的相似度,两个向量越相似则 cos 值越大。

更牛的评分机制

上面是常见的评分策略,但是对于实体来说,大部分都很短, 而且每个字都很重要, 此时 tf-idf 就不那么重要了, 此时可以用 constant_score 查询 来为任意一个匹配的文档制定分数,忽略掉 TF-IDF 信息。

更进一步,如果想改变当前的排序规则,ES 还提供了function_score 查询, 实体链接没这么复杂,暂时没用到就不记了。

利用 python 控制 ES

ES 不同版本间的差距还比较大,这里用的是 ES 7 和对应的 python 模块。

建立索引

es = Elasticsearch(es_server)
mappings = {
    "mappings": {
        "_doc": {
            "properties":{
                {
                "field":{"type": "text",
                         "index": True,
                         "store": True,
                         "analyzer": "ik_max_word",
                         "search_analyzer": "ik_max_word",
                        }
                }
            }
        }
    }
}

es.indices.create(index="index_name", body=mappings)

删除索引

es.indices.delete(index=index_name)

插入数据

data = {"field": "刘德华"}
es.index(index=index_name, body = data, doc_type="_doc")

查询

doc = {
    "query":{
        "bool": {
            "should": [
                {"match": {
                    "field": {"query": "刘德美",
                              "boost": 2}
                    }
                }
            ]
        }
    }
}

这里的 should 可以换做 must 和 must_not,should 等价于 OR, must 等价于 AND, must_not 等价于 NOT。