Elasticsearch - 查询、相关性及优化 Intro

基于词项和基与全⽂的搜索

基于Term的查询

  • Term 的重要性

    • Term 是表达语意的最⼩单位。搜索和利用统计语言模型进⾏⾃然语⾔处理都需要处理 Term
    • ES中 Term查询,不做分词.在倒排索引中查找准确的词项,并且使⽤相关度算分公式为每个包含该词项的⽂档进行相关度算分
  • Term Level Query: Term Query / Range Query / Exists Query / Prefix Query /Wildcard Query

  • 可以通过 Constant Score 将查询转换成⼀个 Filtering,避免算分,并利⽤缓存

POST /products/_search
{
  "explain": true,
  "query": {
    "constant_score": {
      "filter": {
        "term": {
          "desc.keyword": "iPhone"
        }
      }

    }
  }
}
{
 
  "hits" : {
    "total" : {
      "value" : 1,
      "relation" : "eq"
    },
    "max_score" : 1.0,
    "hits" : [
      {
        "_explanation" : {
          "value" : 1.0,
          "description" : "ConstantScore(desc.keyword:iPhone)",
          "details" : [ ]
        }
      }
    ]
  }
}

没有ti-idf计算,减少开销

基于Full-Text的查询

  • 基于全文的查找

    • Match Query / Match Phrase Query / Query String Query
  • 特点

    • 索引和搜索时都会进⾏分词

      • 查询 string,会先经过设置的tokenizer,然后生成一个query的term list
      • 例如查 “ apple orange ”,会查到包括 apple 或者 orange 的所有结果
      • 然后每个词项逐个进⾏index的查询,最终将结果进行合并。并为每个文档生成一个算分
  • 核心指标

    • 准确率
    • 召回率

结构化搜索

对结构化数据的搜索,如日期、数字、bool等,是根据查询内容的一种分类方式,实际使用的还是上面提到的term查询等

  • ⽂本也可以是结构化的

  • 博客可能被标记了标签,例如,分布式(distributed) 和 搜索(elasticsearch)

  • 结构化的文本可以做精确匹配或者部分匹配

    • Term 查询 / Prefix 前缀查询

    • 结构化结果只有“是”或“否”两个值

    • 根据场景需要,可以决定结构化搜索是否需要打分

      • 如果不需要算分,可以通过 Constant Score,将查询转为 Filtering

相关性和相关性得分

在ElasticSearch中,TF-IDF和BM25是两种常用的打分算法,它们可以帮助我们更好地理解和优化搜索结果

  • Relevance

    • 搜索的相关性算分,描述了了⼀个⽂档和查询语句匹配的程度。 ES 会对每个匹配查询条件的结 果进⾏算分 _score
    • 打分的本质是排序,需要把最符合⽤用户需求的⽂档排在前⾯面。 ES5 之前,默认的相关性算分 采⽤用 TF-IDF,现在采⽤用 BM 25
  • 词频 TF

    • Term Frequency 检索词在⼀篇doc中出现的频率

      • 检索词出现的次数除以文档的总词数
    • 度量一条查询和结果⽂档相关性的简单方法:简单将搜索中每⼀个 词的 TF 进⾏相加

      • TF(中国)+TF(的)+TF(男足)
    • Stop World

      • “的” 在⽂档中出现了很多次,但是对贡献相关度⼏乎没有用处,不应该考虑他们的 TF
  • 逆⽂档频率 IDF

    • DF 检索词在所有⽂档中出现的频率

    • Inverse Document Frequency :简单说 = log(全部⽂档数/检索词出现过的文档总数)

    • TF-IDF 本质是将TF 加权求和 - TF(中国)IDF(中国) + TF(的)IDF(的)+ TF(男足)*IDF(男足)

      TF-IDF 算法

    • score 打分函数

    • q : query

    • d :doc

    • t 分词后的词项

    • coord(q,d) 函数:协调因子,越多的查询项在一个文档中,说明些文档的匹配程序越高,比如说,查询"A B C",那么同时包含A/B/C3个词的文档 是3分,只包含A/B的文档是2分,coord可以在query中关掉

    • queryNorm(q) 函数,是用来让一个 doc 的分数处于一个合理的区间内,不要太离谱, 举个例子,一个 doc 分数是 10000,一个 doc 分数是 0.1,肯定不好,归一处理

    • ∑ 文档d的查询q中每个词t的权重之和

    • tf 计算函数: 词项在doc中出现的次数/文档的总词数

    • idf 计算函数: 1 +log(文档总数/(包含t的文档数+1))

    • boosting 提升,可编程接口,查询时可以指定

    • norm(t,d):1 / √numTerms , field length norm 计算,文档越短,相关性越高,可编程接口

      • 字段长度归一值( norm )是字段中词数平方根的倒数

      TF-IDF 算法是以 term为基础的,term就是最小的分词单元,这说明分词算法对基于统计的ranking无比重要,如果你对中文用单字切分,那么就会损失所有的语义相关性,这个时候 搜索只是当做一种高效的全文匹配方法

BM25 算法

  • q_{i} 查询query切分后的term

  • IDF()函数 ,与👆的idf有所区别

    • docCount是分片中字段有值的文档总数(如果您使用的是searchtype=dfs_query_then_fetch,则为跨分片的总数),f(q{i})是包含第i个查询词的文档数量。我们可以在示例中看到,“shane”出现在所有4个文档中,所以对于词语“shane”,我们得到的IDF(“shane”)为:

      • IDF(“connelly”) 为:
    • 我们可以看到,包含这些较少见词语(“connelly”在我们的4篇文档语料库中比“shane”更少见)的查询具有更高的乘数,因此它们对最终得分的贡献更大。这在直觉上是合理的:词语“the”几乎会出现在每一篇英语文档中,因此当用户搜索“the elephant”时,“elephant”可能更重要——我们希望它对得分的贡献更大——比词语“the”(它几乎会出现在所有文档中)

  • fieldLen/avgFieldLen

    • 我们可以将其理解为文档相对于平均文档长度的长短。如果文档比平均长度长,分母变大(降低得分);如果文档比平均长度短,分母变小(增加得分)。请注意,Elasticsearch中字段长度的实现是基于词数(而不是其他如字符长度)。这正如原始BM25论文中所描述的,不过我们确实有一个特殊标志(discount_overlaps)来特别处理同义词。如果文档中的词语越多——至少那些与查询不匹配的词语——文档的得分就越低。这在直觉上也是合理的:如果一篇文档有300页长且只提到我一次,它与我的相关性可能不如一条短推文中提到我一次那么大
  • 变量 b, 如果 b 较大,文档长度相对于平均长度的影响就会被放大。为了更好地理解这一点,你可以想象如果将 b 设为 0,长度比率的影响将完全消除,文档的长度将不会对得分产生任何影响。在 Elasticsearch 中,默认情况下,b 的值是 0.75

  • 变量 k1

    • 有助于确定词频饱和特性。也就是说,它限制了单个查询词对给定文档得分的影响程度。它通过接近一个渐近线来实现这一点
    • 更高/更低的 k1 值意味着“BM25 的 tf()”曲线的斜率发生变化。这会影响“词语出现额外次数时增加的得分量”。k1 的一种解释是,对于平均长度的文档,它是给定词语获得最大得分一半的词频值。当 tf() ≤ k1 时,tf 对得分的影响曲线增长迅速,而当 tf() > k1 时,增长则越来越慢。
    • 通过 k1 我们控制的问题是“在文档中添加第二个‘shane’对得分的贡献应该比第一个多多少,或者第三个对比第二个多多少?”更高的 k1 值意味着该词语的每个出现对得分的贡献可以相对更多。k1 值为 0 意味着除了 IDF(qi) 之外的一切都会被抵消。Elasticsearch 中,k1 的默认值为 1.2
  • f(qi, D),是“第 i 个查询词在文档 D 中出现了多少次?”在所有这些文档中,f(“shane”, D) 都是 1,但 f(“connelly”, D) 则不同:在文档 3 和 4 中是 1,而在文档 1 和 2 中是 0。如果有第五个文档,其内容为“shane shane”,那么它的 f(“shane”, D) 就是 2。我们可以看到 f(qi, D) 出现在分子和分母中,并且还有一个特殊的“k1”因子,我们接下来会讨论。对于 f(qi, D) 的理解是,查询词在文档中出现的次数越多,其得分就越高。这在直觉上是合理的:包含我们名字次数更多的文档比只出现一次的文档更可能与我们相关

Boosting

Boosting 是控制相关度的一种可编程手段,简单说,人为控制相关度的方法

使用场景 索引,字段 或查询子条件(下文的组合嵌套查询👇)

  • 参数 boost的含义

    • 当 boost > 1 时,打分的相关度相对性提升
    • 当 0 < boost < 1 时,打分的权重相对性降低
    • 当 boost < 0 时,贡献负分

组合Query

在 Elasticsearch 中,有Query 和 Filter 两种不同的 Context

  • Query Context:相关性算分
  • Filter Context:不需要算分( Yes or No),可以利用 Cache, 获得更好的性能 区别👆的 term/full-text/structure search,ES中的组合查询,拼装了上面的几种查询方式

多字符串多字段

假设要搜索⼀本电影,包含了了以下⼀些条件

  • 评论中包含了 Guitar,⽤用户打分⾼于 3 分,同时上映⽇期要在 1993 与 2000 年之间
  • 评论字段中要包含 Guitar / ⽤户评分大于 3 / 上映日期⽇期需要在给定的范围
  • 同时包含这三个逻辑

多字段复合查询: bool Query

相关性并不只是全⽂本检索的专利利。也适用于 yes | no 的子句,匹配的⼦句越多,相关性评分 越高。如果多条查询⼦句被合并为⼀条复合查询语句 ,⽐如 bool 查询,则每个查询⼦句计算 得出的评分会被合并到总的相关性评分中

  • must | should (query context) / must_not | filter ((filter context))

  • 支持嵌套,如should must_not

    • 同⼀层级下的竞争字段,具有相同的权重
    • 通过嵌套 bool 查询,可以改变对算分的影响
POST /products/_search
{
  "explain": true, 
  "query": {
    "bool": {
      "must": {
        "term": {
          "price": "30"
        }
      },
      "should": [
        {
          "bool": {
            "must_not": {
              "term": {
                "avaliable": "false"
              }
            }
          }
        }
      ],
      "minimum_should_match": 1
    }
  }
}

单字符串多字段Query

联想下google search

算分过程

PUT /blogs/_doc/1
{
    "title": "Quick brown rabbits",
    "body":  "Brown rabbits are commonly seen."
}

PUT /blogs/_doc/2
{
    "title": "Keeping pets healthy",
    "body":  "My quick brown fox eats rabbits on a regular basis."
}
POST /blogs/_search
{
    "query": {
        "bool": {
            "should": [
                { "match": { "title": "Brown fox" }},
                { "match": { "body":  "Brown fox" }}
            ]
        }
    }
}
POST blogs/_search
{
    "query": {
        "dis_max": {
            "queries": [
                { "match": { "title": "Quick pets" }},
                { "match": { "body":  "Quick pets" }}
            ]
        }
    }
}
POST blogs/_search
{
    "query": {
        "dis_max": {
            "queries": [
                { "match": { "title": "Quick pets" }},
                { "match": { "body":  "Quick pets" }}
            ],
            "tie_breaker": 0.2
        }
    }
}
  • 查询 should 语句中的两个查询
  • 加和两个查询的评分
  • 乘以匹配语句的总数
  • 除以所有语句的总数

Dis Max Query

  • 上例中, title 和 body 相互竞争

  • Disjunction Max Query

    • 将任何与任⼀查询匹配的文档作为结果返回。采⽤字段上最匹配的评分最终评分返回

    • 同时匹配 title 和 body 字段的⽂档⽐只与⼀个字段匹配的⽂档的相关度更高,怎么办?

      • 通过 Tie Breaker 参数调整
      • Tier Breaker 是⼀一个介于 0-1 之间的浮点数。 0 代表使⽤最佳匹配; 1 代表所有语句同等重要

Multi Match Query

三种场景供选择

  • 最佳字段 (Best Fields) - 类似于👆的dis_max & match 默认类型

  • 多数字段 (Most Fields)

    • 合并所有字段的评分:title & body
    • 所有字段包含相同的文本
    • ⽤广度匹配字段 title 包括尽可能多的文档——以提 升召回率——同时⼜使用字段 title.std 作为信号 将 相关度更高的文档置于结果顶部,操作见👇代码部分
    • 每个字段对于最终评分的贡献可以通过⾃定义值 boost 来控制。⽐如,使 title 字段更为重要, 这样同时也降低了其他信号字段的作用
  • 混合字段 (Cross Field)

    • 对于某些实体,例如⼈名,地址,图书信息。需要在多个字段中确定信息,单个字段只能作为整体 的⼀部分。希望在任何这些列出的字段中找到尽可能多的词
    • 类似可以使用copy_to 合并为1个列处理
    • 与 copy_to, 相比,其中⼀个优势就是它可以在搜索时为单个字段提升权重(^ boosting)
POST blogs/_search
{
  "query": {
    "multi_match": {
      "type": "best_fields",
      "query": "Quick pets",
      "fields": ["title","body"],
      "tie_breaker": 0.2,
      "minimum_should_match": "20%"
    }
  }
}

PUT /titles
{
  "mappings": {
    "properties": {
      "title": {
        "type": "text",
        "analyzer": "english",
        "fields": {
            "std":{
                  "type":     "text",
                  "analyzer": "standard"
              }
          }
      }
    }
  }
}

POST titles/_bulk
{ "index": { "_id": 1 }}
{ "title": "My dog barks" }
{ "index": { "_id": 2 }}
{ "title": "I see a lot of barking dogs on the road " }

GET /titles/_search
{
   "query": {
        "multi_match": {
            "query":  "barking dogs",
            "type":   "most_fields",
            "fields": [ "title", "title.std" ]
        }
    }
}

GET /titles/_search
{
   "query": {
        "multi_match": {
            "query":  "barking dogs",
            "type":   "most_fields",
            "fields": [ "title^10", "title.std" ]
        }
    }
}

多语⾔及中文分词与检索

自然语言处理

当处理理人类⾃然语言时,有些情况,尽管搜索和原⽂不完全匹配,但是希望搜到相关内容

  • 如英文的时态

  • 如中文拼音输入法的同音字

  • 如同义词

  • 可采取的优化

    • 归⼀化词元:清除变⾳符号,如 rôle 的时候也会匹配 role
    • 抽取词根:清除单复数和时态的差异
    • 包含同义词

混合语言的挑战

不同的索引使⽤不同的语言 / 同⼀个索引中,不同的字段使用不同的语言 / 一个 ⽂档的⼀个字段内混合不同的语⾔言

  • 英文中包含德文,德⽂算分⾼(稀有)
  • 不同的语言使用不同的索引或者不同的字段
  • 需要判断⽤户搜索时使用的语⾔,语言识别

分词的挑战

  • You’re 分成⼀一个还是多个?
  • 美国 会 通过法案/美 国会 通过对台售武法案

中⽂分词方法的演变 – 字典法

根据词典

  • ⼀个句⼦子从左到右扫描⼀遍。遇到有的词就标示出来。找到复合词,就找最⻓的
  • 不认识的字串就分割成单字词(这里有问题,单字match召回过多准确率下降)

优化

  • 最⼩词数的分词理论
  • ⼀句话应该分成数量最少的词串
  • 遇到⼆义性的分割,⽆能为⼒(发展中国家)

中⽂分词方法的演变 – 基于统计法的机器学习算法

解决二义性问题,加入上下文

  • 这类⽬前常⽤的是算法是HMM、 CRF、 SVM、 深度学习等算法。⽐如 Hanlp 分词⼯具是基于CRF 算法。以CRF为例,基本思路是对汉字进行标注训练,不仅考虑了词语出现的频率,还考虑上下文, 具备较好的学习能力,因此其对歧义词和未登录词的识别都具有良好的效果
  • 随着深度学习的兴起,也出现了了基于神经网络的分词器,有⼈尝试使⽤双向LSTM+CRF实现分词器,其本质上是序列标注,据报道其分词器字符准确率可⾼达97.5%

中⽂分词器现状

字典法+机器学习的结合

  • HanLP – ⾯向⽣产环境的⾃然语⾔处理⼯具包
  • IK - 支持词典热更新
  • Pinyin

索引时,尽量切分的短,查询的时候,尽量⽤长的词

Search Templates

POST _scripts/tmdb
{
  "script": {
    "lang": "mustache",
    "source": {
      "_source": [
        "title","overview"
      ],
      "size": 20,
      "query": {
        "multi_match": {
          "query": "{{q}}",
          "fields": ["title","overview"]
        }
      }
    }
  }
}

POST tmdb/_search/template
{
    "id":"tmdb",
    "params": {
        "q": "basketball with cartoon aliens"
    }
}

Function Score Query 优化算分

ES 默认会使用query 与doc的相关度算分排序

如果业务要求更灵活的排序方法,就需要用到Function Score Query

  • 在查询结束后,对每⼀个匹配的文档进行⼀系列列的重新算分,根据新⽣成的分数进⾏排序

先看一个demo

POST /blogs/_search
{
  "query": {
    "function_score": {
      "query": {
        "multi_match": {
          "query":    "popularity",
          "fields": [ "title", "content" ]
        }
      },
      "field_value_factor": {
        "field": "votes",
        "modifier": "log1p" ,
        "factor": 0.1
      },
      "boost_mode": "sum",
      "max_boost": 3
    }
  }
}
  • new score = old_score log(1+votesfactor)

  • boost mode

    • Multiply : score*log
    • Sum: score+log
    • Min/Max: Max|Min(score,log)
    • Replace : log
  • max_boost 将score控制在最大值内

  • random_score

    • 一致性随机函数
    • 使⽤场景:⽹站的⼴告需要提⾼展现率
    • 让每个⽤户能看到不同的随机排名,但是也希望同一个用户访问时,结果的相对顺序,保持一 致 (Consistently Random)

搜索建议与自动补全

Term & Phrase Suggester

  • suggestion mode

    • Missing 如果索引中已存在就不提供建议
    • Popular 推荐高频词
    • Always 无论是否存在都提供建议
  • 默认按照score排序 也可以按照frequency排序

  • 默认首字母不一致就不会匹配推荐,prefix_length=0 则继续推荐

  • Phrase Suggester 增加了拼错的terms数和confidence限制返回结果

  • 变相增加recall和准确率

POST /articles/_search
{
  "suggest": {
    "my-suggestion": {
      "text": "lucne and elasticsear rock hello world ",
      "phrase": {
        "field": "body",
        "max_errors":2,
        "confidence":0,
        "direct_generator":[{
          "field":"body",
          "suggest_mode":"always"
        }],
        "highlight": {
          "pre_tag": "<em>",
          "post_tag": "</em>"
        }
      }
    }
  }
}

Complete & Context Suggester

Completion Suggester

  • 提供了“⾃动完成” (Auto Complete) 的功能, ⽤户每输⼊⼀个字符,就需要即时发送一个查询请求到后段查找匹配项

  • 为了提高效率,ES没有直接使用倒排index,而是FST,内存查找,提高速度

  • FST介绍

    • 图示
    • FST是将term拆成单个字母存在节点上,每个节点存一个字母,根节点不存,从根节点出发到特定节点所经过的字母就可以组成目标term,其经过路径的权重和就是该term在Term Dictionary中对应的位置
    • 空间占用小。通过对词典中单词前缀和后缀的重复利用,压缩了存储空间,300万条数字使用FST存储 文件大小2k左右
    • 查询速度快 O(len(str))的查询时间复杂度
    • 限制:只能进行prefix search
  • demo

PUT articles
{
  "mappings": {
    "properties": {
      "title_completion":{
        "type": "completion"
      }
    }
  }
}

POST articles/_search?pretty
{
  "size": 0,
  "suggest": {
    "article-suggester": {
      "prefix": "elk ",
      "completion": {
        "field": "title_completion"
      }
    }
  }
}

Context Suggester

可以在搜索中加⼊更多的上下文信息,例如,输入 “star”

  • starbucks 咖啡
  • star wars 电影

Context类型

  • Category
  • Geo

需要定制mapping,将category加入到doc中

直接看Demo

PUT comments/_mapping
{
  "properties": {
    "comment_autocomplete":{
      "type": "completion",
      "contexts":[{
        "type":"category",
        "name":"comment_category"
      }]
    }
  }
}

POST comments/_doc
{
  "comment":"I love the star war movies",
  "comment_autocomplete":{
    "input":["star wars"],
    "contexts":{
      "comment_category":"movies"
    }
  }
}

POST comments/_doc
{
  "comment":"Where can I find a Starbucks",
  "comment_autocomplete":{
    "input":["starbucks"],
    "contexts":{
      "comment_category":"coffee"
    }
  }
}

POST comments/_search
{
  "suggest": {
    "MY_SUGGESTION": {
      "prefix": "sta",
      "completion":{
        "field":"comment_autocomplete",
        "contexts":{
          "comment_category":"coffee"
        }
      }
    }
  }
}

准确率和召回率

准确率

  • completion > phrase > term

召回率

  • Term > phrase >completion

性能

  • completion > phrase > term

Cross Cluster Search

当单机群遇到瓶颈时,需要多集群来共同工作,ES支持跨集群搜索

  • 允许任何节点扮演 federated 节点,以轻量的⽅方式,将搜索请求进行代理
  • 不需要以client node的形式加入其他集群
PUT _cluster/settings
{
  "persistent": {
    "cluster": {
      "remote": {
        "cluster0": {
          "seeds": [
            "127.0.0.1:9300"
          ],
          "transport.ping_schedule": "30s"
        },
        "cluster1": {
          "seeds": [
            "127.0.0.1:9301"
          ],
          "transport.compress": true,
          "skip_unavailable": true
        },
        "cluster2": {
          "seeds": [
            "127.0.0.1:9302"
          ]
        }
      }
    }
  }
}
curl -XPOST "http://localhost:9200/users/_doc" -H 'Content-Type: application/json' -d'
{"name":"user1","age":10}'

curl -XPOST "http://localhost:9201/users/_doc" -H 'Content-Type: application/json' -d'
{"name":"user2","age":20}'

curl -XPOST "http://localhost:9202/users/_doc" -H 'Content-Type: application/json' -d'
{"name":"user3","age":30}'

GET /users,cluster1:users,cluster2:users/_search
{
  "query": {
    "range": {
      "age": {
        "gte": 20,
        "lte": 40
      }
    }
  }
}

参考

Similarity module

Practical BM25官方文档

深入理解Elasticsearch PDF