一.sug概述

对提供的搜索词(suggest text)返回相关的提示词

ES高亮搜索的高亮模式 es搜索提示_elastic

二.四种suggester

通用option字段及含义

option

含义

text

搜索词,因为一个search可以存在多个suggester,所以可以设置全局text,也可对每个suggester单独设置,单独设置时以单独为准

field

返回提示词的字段

analyzer

搜索词text分词器,默认和field的分词器相同

size

每个text返回size个提示词

sort

对于text的每个分出来的词(suggest text term),返回的提示词如何排序

  • score 先按相似度,相似度相同再按词频
  • frequency 先词频再相似度

suggest_mode

用来控制①返回什么提示词②对哪些搜索词的分词返回提示词

  • missing 针对②,只对索引中没有的 搜索词分词 返回提示词 【默认】
  • popular 针对①,只返回比搜索词分词在文档里出现频率高的提示词
  • always ①②,对所有搜索词分词返回所有匹配提示词

1.Term suggester

term sug提供提示词的依据是和搜索词的编辑距离,直观理解就是根据相似程度。ES会先对搜索词进行分词处理(分词器可以手动设定,默认和要搜索字段的分词器相同),然后对每一个分出来的词返回相似词。

term sug的option字段及含义

option

含义

lowercase_terms

把搜索词分词小写

max_edits

提示词与搜索词分词的最大编辑距离,取值范围[1,2],默认2

prefix_length

至少匹配的前缀长度(TODO:汉字长度咋算),默认1

min_word_length

返回提示词的最小长度,默认4

shard_size

每个shard返回到coordinate node 的提示词数,增大时提高返回精准度,具体参考

terms分组计数精确度问题中关于shard_size 和 size的说明。

max_inspections

乘shard_size的因子,加大时在各分片上监测更多的拼写矫正结果,性能换精度,默认5。估计也用不到

min_doc_freq

大于1的整数或百分比值,表示提示词最少在几篇,或百分之多少的文档中出现。默认为0,不生效。

max_term_freq

大于1的整数或百分比值,表示提示词最多出现在几篇或百分之多少的文档里。设置此值的背景是需要拼写检查的场景,对于某些频率特别高的词,打搜索词时出错概率低,不返回提示词。

string_distance

五种计算编辑距离的算法......

Term suggester示例

1、数据准备

curl -u $user:passoword -H "Content-Type:application/json" -XPUT "http://$ip:$port/blogs" -d '{"settings":{"index":{"number_of_shards":2,"number_of_replicas":1}}}'

curl -u $user:passoword -H "Content-Type:application/json" -XPUT "http://$ip:$port/blogs/_mapping/_doc?pretty" -d '{"properties":{"body":{"type":"text"}}}'

curl -u $user:passoword -H "Content-Type:application/json" -XPOST "http://$ip:$port/blogs/_doc/_bulk?pretty" --data-binary @test.data
{ "index" : { "_index" : "blogs"} }
{ "body":"Lucene is cool"}
{ "index" : { "_index" : "blogs"} }
{ "body": "Elasticsearch builds on top of lucene"}
{ "index" : { "_index" : "blogs"} }
{ "body": "Elasticsearch rocks"}
{ "index" : { "_index" : "blogs"} }
{ "body": "Elastic is the company behind ELK stack"}
{ "index" : { "_index" : "blogs"} }
{ "body": "elk rocks"}
{ "index" : { "_index" : "blogs"} }
{  "body": "elasticsearch is rock solid"}

suggest就是一种特殊类型的搜索,DSL内部的"text"指的是api调用方提供的文本,也就是通常用户界面上用户输入的内容。这里的lucne是错误的拼写,模拟用户输入错误。 "term"表示这是一个term suggester。 "field"指定suggester针对的字段,另外有一个可选的"suggest_mode"。 范例里的"missing"实际上就是缺省值

curl -u $user:passoword -H "Content-Type:application/json" -XPOST "http://$ip:$port/blogs/_doc/_search?pretty" -d '{
    "suggest":{
        "my-suggestion":{
            "text":"lucne rocks",
            "term":{
                "suggest_mode":"missing",
                "field":"body"
            }
        }
    }
}'

结果:
{
    "took":79,
    "timed_out":false,
    "_shards":{
        "total":2,
        "successful":2,
        "skipped":0,
        "failed":0
    },
    "hits":{
        "total":0,
        "max_score":0,
        "hits":[

        ]
    },
    "suggest":{
        "my-suggestion":[
            {
                "text":"lucne",
                "offset":0,
                "length":5,
                "options":[
                    {
                        "text":"lucene",
                        "score":0.8,
                        "freq":2
                    }
                ]
            },
            {
                "text":"rocks",
                "offset":6,
                "length":5,
                "options":[
                    {
                        "text":"rock",
                        "score":0.75,
                        "freq":1
                    }
                ]
            }
        ]
    }
}

在返回结果里"suggest" -> "my-suggestion"部分包含了一个数组,每个数组项对应从输入文本分解出来的token(存放在"text"这个key里)以及为该token提供的建议词项(存放在options数组里)。
示例里返回了"lucne","rock"这2个词的建议项(options)

#suggest_mode换成popular再试试?

两个term的相似性是如何判断的? ES使用了一种叫做Levenstein edit distance的算法,其核心思想就是一个词改动多少个字符就可以和另外一个词一致。

2.Phrase suggester

Phrase suggester在Term suggester的基础上,会考量多个term之间的关系,比如是否同时出现在索引的原文里,相邻程度,以及词频等等。

curl -u $user:$password -H "Content-Type:application/json" -XPOST "http://$ip:$port/blogs/_doc/_search?pretty" -d '{
    "suggest":{
        "my-suggestion":{
            "text":"lucne and elasticsear rock",
            "phrase":{
                "field":"body",
                "highlight":{
                    "pre_tag":"<em>",
                    "post_tag":"</em>"
                }
            }
        }
    }
}'

# 结果
{
    "took":30,
    "timed_out":false,
    "_shards":{
        "total":2,
        "successful":2,
        "skipped":0,
        "failed":0
    },
    "hits":{
        "total":0,
        "max_score":0,
        "hits":[

        ]
    },
    "suggest":{
        "my-suggestion":[
            {
                "text":"lucne and elasticsear rock",
                "offset":0,
                "length":26,
                "options":[
                    {
                        "text":"lucene and elasticsearch rock",
                        "highlighted":"<em>lucene</em> and <em>elasticsearch</em> rock",
                        "score":0.012031202
                    },
                    {
                        "text":"lucne and elasticsearch rocks",
                        "highlighted":"lucne and <em>elasticsearch rocks</em>",
                        "score":0.009254823
                    },
                    {
                        "text":"lucne and elasticsearch rock",
                        "highlighted":"lucne and <em>elasticsearch</em> rock",
                        "score":0.008044719
                    },
                    {
                        "text":"lucene and elasticsear rock",
                        "highlighted":"<em>lucene</em> and elasticsear rock",
                        "score":0.007966585
                    },
                    {
                        "text":"lucne and elasticsear rocks",
                        "highlighted":"lucne and elasticsear <em>rocks</em>",
                        "score":0.0073081385
                    }
                ]
            }
        ]
    }
}

options直接返回一个phrase列表,由于加了highlight选项,被替换的term会被高亮。因为lucene和elasticsearch曾经在同一条原文里出现过,同时替换2个term的可信度更高,所以打分较高,排在第一位返回。Phrase suggester有相当多的参数用于控制匹配的模糊程度,需要根据实际应用情况去挑选和调试。

3.Completion suggester

主要针对的应用场景就是"Auto Completion"。此场景下用户每输入一个字符的时候,就需要即时发送一次查询请求到后端查找匹配项,在用户输入速度较高的情况下对后端响应速度要求比较苛刻。因此实现上它和前面两个Suggester采用了不同的数据结构,索引并非通过倒排来完成,而是将analyze过的数据编码成FST和索引一起存放。对于一个open状态的索引,FST会被ES整个装载到内存里的,进行前缀查找速度极快。但是FST只能用于前缀查找,这也是Completion Suggester的局限所在。

#需要重新建立索引数据
PUT /blogs_completion/
{
  "mappings": {
    "tech": {
      "properties": {
        "body": {
          "type": "completion"
        }
      }
    }
  }
}

POST _bulk/?refresh=true
{ "index" : { "_index" : "blogs_completion", "_type" : "tech" } }
{ "body": "Lucene is cool"}
{ "index" : { "_index" : "blogs_completion", "_type" : "tech" } }
{ "body": "Elasticsearch builds on top of lucene"}
{ "index" : { "_index" : "blogs_completion", "_type" : "tech" } }
{ "body": "Elasticsearch rocks"}
{ "index" : { "_index" : "blogs_completion", "_type" : "tech" } }
{ "body": "Elastic is the company behind ELK stack"}
{ "index" : { "_index" : "blogs_completion", "_type" : "tech" } }
{ "body": "the elk stack rocks"}
{ "index" : { "_index" : "blogs_completion", "_type" : "tech" } }
{ "body": "elasticsearch is rock solid"}


##查找
POST blogs_completion/_search?pretty
{ "size": 0,
  "suggest": {
    "blog-suggest": {
      "prefix": "elastic i",
      "completion": {
        "field": "body"
      }
    }
  }
}
#结果
{
  "took": 33,
  "timed_out": false,
  "_shards": {
    "total": 5,
    "successful": 5,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": 0,
    "max_score": 0,
    "hits": []
  },
  "suggest": {
    "blog-suggest": [
      {
        "text": "elastic i",
        "offset": 0,
        "length": 9,
        "options": [
          {
            "text": "Elastic is the company behind ELK stack",
            "_index": "blogs_completion",
            "_type": "tech",
            "_id": "AWh05W0vrgjL5DnrjuWz",
            "_score": 1,
            "_source": {
              "body": "Elastic is the company behind ELK stack"
            }
          }
        ]
      }
    ]
  }
}

值得注意的一点是Completion Suggester在索引原始数据的时候也要经过analyze阶段,取决于选用的analyzer不同,某些词可能会被转换,某些词可能被去除,这些会影响FST编码结果,也会影响查找匹配的效果。

比如我们删除上面的索引,重新设置索引的mapping,将analyzer更改为"english"

PUT /blogs_completion/
{
  "mappings": {
    "tech": {
      "properties": {
        "body": {
          "type": "completion",
          "analyzer": "english"
        }
      }
    }
  }
}

#bulk api索引同样的数据后,执行下面的查询:
POST blogs_completion/_search?pretty
{ "size": 0,
  "suggest": {
    "blog-suggest": {
      "prefix": "elastic i",
      "completion": {
        "field": "body"
      }
    }
  }
}

#结果
{
  "took": 7,
  "timed_out": false,
  "_shards": {
    "total": 5,
    "successful": 5,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": 0,
    "max_score": 0,
    "hits": []
  },
  "suggest": {
    "blog-suggest": [
      {
        "text": "elastic i",
        "offset": 0,
        "length": 9,
        "options": []
      }
    ]
  }
}
#居然没有匹配结果了,多么费解!原来我们用的english analyzer会剥离掉stop word,而is就是其中一个,被剥离掉了!

#用analyze api测试一下:
POST _analyze?analyzer=english
{
  "text": "elasticsearch is rock solid"
}

#结果
{
  "tokens": [
    {
      "token": "elasticsearch",
      "start_offset": 0,
      "end_offset": 13,
      "type": "<ALPHANUM>",
      "position": 0
    },
    {
      "token": "rock",
      "start_offset": 17,
      "end_offset": 21,
      "type": "<ALPHANUM>",
      "position": 2
    },
    {
      "token": "solid",
      "start_offset": 22,
      "end_offset": 27,
      "type": "<ALPHANUM>",
      "position": 3
    }
  ]
}

FST只编码了这3个token,并且默认的还会记录他们在文档中的位置和分隔符。 用户输入"elastic i"进行查找的时候,输入被分解成"elastic"和"i",FST没有编码这个“i” , 匹配失败。

好吧,如果你现在还足够清醒的话,试一下搜索"elastic is",会发现又有结果,why?  因为这次输入的text经过english analyzer的时候is也被剥离了,只需在FST里查询"elastic"这个前缀,自然就可以匹配到了。

其他能影响completion suggester结果的,还有诸如"preserve_separators","preserve_position_increments"等等mapping参数来控制匹配的模糊程度。以及搜索时可以选用Fuzzy Queries,使得上面例子里的"elastic i"在使用english analyzer的情况下依然可以匹配到结果。

因此用好Completion Sugester并不是一件容易的事,实际应用开发过程中,需要根据数据特性和业务需要,灵活搭配analyzer和mapping参数,反复调试才可能获得理想的补全效果。

回到篇首Google搜索框的补全/纠错功能,如果用ES怎么实现呢?我能想到的一个的实现方式:

  1. 在用户刚开始输入的过程中,使用Completion Suggester进行关键词前缀匹配,刚开始匹配项会比较多,随着用户输入字符增多,匹配项越来越少。如果用户输入比较精准,可能Completion Suggester的结果已经够好,用户已经可以看到理想的备选项了。 
  2. 如果Completion Suggester已经到了零匹配,那么可以猜测是否用户有输入错误,这时候可以尝试一下Phrase Suggester。
  3. 如果Phrase Suggester没有找到任何option,开始尝试term Suggester。

精准程度上(Precision)看: Completion >  Phrase > term, 而召回率上(Recall)则反之。从性能上看,Completion Suggester是最快的,如果能满足业务需求,只用Completion Suggester做前缀匹配是最理想的。 Phrase和Term由于是做倒排索引的搜索,相比较而言性能应该要低不少,应尽量控制suggester用到的索引的数据量,最理想的状况是经过一定时间预热后,索引可以全量map到内存。

Suggesters | Elasticsearch Guide [8.3] | Elastic

4.Context Suggester

https://www.elastic.co/guide/en/elasticsearch/reference/5.2/suggester-context.html#