资料参考:https://elasticsearch.cn/article/142
Google在用户刚开始输入的时候是自动补全的,而当输入到一定长度,如果因为单词拼写错误无法补全,就开始尝试提示相似的词。
那么类似的功能在Elasticsearch里如何实现呢?答案就在Suggesters API。Suggesters基本的运作原理是将输入的文本分解为token,然后在索引的字典里查找相似的term并返回。根据使用场景的不同,Elasticsearch里设计了4种类别的Suggester,分别是:

  • Term Suggester
  • Phrase Suggester
  • Completion Suggester
  • Context Suggester

    1.Term Suggester

    准备一个叫做blogs的索引,配置一个text字段:
    1. PUT /blogs/
    2. {
    3. "mappings": {
    4. "tech": {
    5. "properties": {
    6. "body": {
    7. "type": "text"
    8. }
    9. }
    10. }
    11. }
    12. }
    通过bulk api写入几条文档:
    1. POST _bulk/?refresh=true
    2. {"index":{"_index":"blogs","_type":"tech"}}
    3. {"body":"Lucene is cool"}
    4. {"index":{"_index":"blogs","_type":"tech"}}
    5. {"body":"Elasticsearch builds on top of lucene"}
    6. {"index":{"_index":"blogs","_type":"tech"}}
    7. {"body":"Elasticsearch rocks"}
    8. {"index":{"_index":"blogs","_type":"tech"}}
    9. {"body":"Elastic is the company behind ELK stack"}
    10. {"index":{"_index":"blogs","_type":"tech"}}
    11. {"body":"elk rocks"}
    12. {"index":{"_index":"blogs","_type":"tech"}}
    13. {"body":"elasticsearch is rock solid"}
    此时blogs索引里已经有一些文档了,可以进行下一步的探索。为帮助理解,我们先看看哪些term会存在于词典里。
    将输入的文本分析一下: ```yaml POST _analyze { “text”: [ “Lucene is cool”, “Elasticsearch builds on top of lucene”, “Elasticsearch rocks”, “Elastic is the company behind ELK stack”, “elk rocks”, “elasticsearch is rock solid” ] }
  1. 这些分出来的token都会成为词典里一个term,注意有些token会出现多次,因此在倒排索引里记录的词频会比较高,同时记录的还有这些token在原文档里的偏移量和相对位置信息。<br />执行一次suggester搜索看看效果:
  2. ```yaml
  3. POST /blogs/_search
  4. {
  5. "suggest": {
  6. "my-suggestion": {
  7. "text": "lucne rock",
  8. "term": {
  9. "suggest_mode": "missing",
  10. "field": "body"
  11. }
  12. }
  13. }
  14. }

suggest就是一种特殊类型的搜索,DSL内部的”text”指的是api调用方提供的文本,也就是通常用户界面上用户输入的内容。这里的lucne是错误的拼写,模拟用户输入错误。 “term”表示这是一个term suggester。 “field”指定suggester针对的字段,另外有一个可选的”suggest_mode”。 范例里的”missing”实际上就是缺省值,它是什么意思?有点挠头… 还是先看看返回结果吧:

  1. {
  2. "took" : 6,
  3. "timed_out" : false,
  4. "_shards" : {
  5. "total" : 1,
  6. "successful" : 1,
  7. "skipped" : 0,
  8. "failed" : 0
  9. },
  10. "hits" : {
  11. "total" : {
  12. "value" : 0,
  13. "relation" : "eq"
  14. },
  15. "max_score" : null,
  16. "hits" : [ ]
  17. },
  18. "suggest" : {
  19. "my-suggestion" : [
  20. {
  21. "text" : "lucne",
  22. "offset" : 0,
  23. "length" : 5,
  24. "options" : [
  25. {
  26. "text" : "lucene",
  27. "score" : 0.8,
  28. "freq" : 2
  29. }
  30. ]
  31. },
  32. {
  33. "text" : "rock",
  34. "offset" : 6,
  35. "length" : 4,
  36. "options" : [ ]
  37. }
  38. ]
  39. }
  40. }

在返回结果里”suggest” -> “my-suggestion”部分包含了一个数组,每个数组项对应从输入文本分解出来的token(存放在”text”这个key里)以及为该token提供的建议词项(存放在options数组里)。 示例里返回了”lucne”,”rock”这2个词的建议项(options),其中”rock”的options是空的,表示没有可以建议的选项,为什么? 上面提到了,我们为查询提供的suggest mode是”missing”,由于”rock”在索引的词典里已经存在了,够精准,就不建议啦。 只有词典里找不到词,才会为其提供相似的选项。
如果将”suggest_mode”换成”popular”会是什么效果?

  1. POST /blogs/_search
  2. {
  3. "suggest": {
  4. "my-suggestion": {
  5. "text": "lucne rock",
  6. "term": {
  7. "suggest_mode": "popular",
  8. "field": "body"
  9. }
  10. }
  11. }
  12. }

尝试一下,重新执行查询,返回结果里”rock”这个词的option不再是空的,而是建议为rocks。

  1. {
  2. "took" : 2,
  3. "timed_out" : false,
  4. "_shards" : {
  5. "total" : 1,
  6. "successful" : 1,
  7. "skipped" : 0,
  8. "failed" : 0
  9. },
  10. "hits" : {
  11. "total" : {
  12. "value" : 0,
  13. "relation" : "eq"
  14. },
  15. "max_score" : null,
  16. "hits" : [ ]
  17. },
  18. "suggest" : {
  19. "my-suggestion" : [
  20. {
  21. "text" : "lucne",
  22. "offset" : 0,
  23. "length" : 5,
  24. "options" : [
  25. {
  26. "text" : "lucene",
  27. "score" : 0.8,
  28. "freq" : 2
  29. }
  30. ]
  31. },
  32. {
  33. "text" : "rock",
  34. "offset" : 6,
  35. "length" : 4,
  36. "options" : [
  37. {
  38. "text" : "rocks",
  39. "score" : 0.75,
  40. "freq" : 2
  41. }
  42. ]
  43. }
  44. ]
  45. }
  46. }

回想一下,rock和rocks在索引词典里都是有的。 不难看出即使用户输入的token在索引的词典里已经有了,但是因为存在一个词频更高的相似项,这个相似项可能是更合适的,就被挑选到options里了。 最后还有一个”always” mode,其含义是不管token是否存在于索引词典里都要给出相似项。
有人可能会问,两个term的相似性是如何判断的? ES使用了一种叫做Levenstein edit distance的算法,其核心思想就是一个词改动多少个字符就可以和另外一个词一致。 Term suggester还有其他很多可选参数来控制这个相似性的模糊程度,这里就不一一赘述了。
Term suggester正如其名,只基于analyze过的单个term去提供建议,并不会考虑多个term之间的关系。API调用方只需为每个token挑选options里的词,组合在一起返回给用户前端即可。 那么有无更直接办法,API直接给出和用户输入文本相似的内容? 答案是有,这就要求助Phrase Suggester了。

2.Phrase Suggester

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

  1. POST /blogs/_search
  2. {
  3. "suggest": {
  4. "my-suggestion": {
  5. "text": "lucne and elasticsear rock",
  6. "phrase": {
  7. "field": "body",
  8. "highlight": {
  9. "pre_tag": "<em>",
  10. "post_tag": "</em>"
  11. }
  12. }
  13. }
  14. }
  15. }

返回结果:

  1. {
  2. "took" : 5,
  3. "timed_out" : false,
  4. "_shards" : {
  5. "total" : 1,
  6. "successful" : 1,
  7. "skipped" : 0,
  8. "failed" : 0
  9. },
  10. "hits" : {
  11. "total" : {
  12. "value" : 0,
  13. "relation" : "eq"
  14. },
  15. "max_score" : null,
  16. "hits" : [ ]
  17. },
  18. "suggest" : {
  19. "my-suggestion" : [
  20. {
  21. "text" : "lucne and elasticsear rock",
  22. "offset" : 0,
  23. "length" : 26,
  24. "options" : [
  25. {
  26. "text" : "lucene and elasticsearch rock",
  27. "highlighted" : "<em>lucene</em> and <em>elasticsearch</em> rock",
  28. "score" : 0.004993905
  29. },
  30. {
  31. "text" : "lucne and elasticsearch rock",
  32. "highlighted" : "lucne and <em>elasticsearch</em> rock",
  33. "score" : 0.0033391973
  34. },
  35. {
  36. "text" : "lucene and elasticsear rock",
  37. "highlighted" : "<em>lucene</em> and elasticsear rock",
  38. "score" : 0.0029183894
  39. }
  40. ]
  41. }
  42. ]
  43. }
  44. }

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

3.Completion Suggester

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

  1. PUT /blogs_completion
  2. {
  3. "mappings": {
  4. "properties": {
  5. "body": {
  6. "type": "completion"
  7. }
  8. }
  9. }
  10. }

用bulk API索引点数据:

  1. POST _bulk/?refresh=true
  2. {"index":{"_index":"blogs_completion"}}
  3. {"body":"Lucene is cool"}
  4. {"index":{"_index":"blogs_completion","_type":"_doc"}}
  5. {"body":"Elasticsearch builds on top of lucene"}
  6. {"index":{"_index":"blogs_completion","_type":"_doc"}}
  7. {"body":"Elasticsearch rocks"}
  8. {"index":{"_index":"blogs_completion","_type":"_doc"}}
  9. {"body":"Elastic is the company behind ELK stack"}
  10. {"index":{"_index":"blogs_completion","_type":"_doc"}}
  11. {"body":"the elk stack rocks"}
  12. {"index":{"_index":"blogs_completion","_type":"_doc"}}
  13. {"body":"elasticsearch is rock solid"}

查找:

  1. POST blogs_completion/_search?pretty
  2. {
  3. "size": 0,
  4. "suggest": {
  5. "blog-suggest": {
  6. "prefix": "elastic i",
  7. "completion": {
  8. "field": "body"
  9. }
  10. }
  11. }
  12. }

结果:

  1. {
  2. "took" : 17,
  3. "timed_out" : false,
  4. "_shards" : {
  5. "total" : 1,
  6. "successful" : 1,
  7. "skipped" : 0,
  8. "failed" : 0
  9. },
  10. "hits" : {
  11. "total" : {
  12. "value" : 0,
  13. "relation" : "eq"
  14. },
  15. "max_score" : null,
  16. "hits" : [ ]
  17. },
  18. "suggest" : {
  19. "blog-suggest" : [
  20. {
  21. "text" : "elastic i",
  22. "offset" : 0,
  23. "length" : 9,
  24. "options" : [
  25. {
  26. "text" : "Elastic is the company behind ELK stack",
  27. "_index" : "blogs_completion",
  28. "_type" : "_doc",
  29. "_id" : "zKU-yHwBKi_n6G7PZCbM",
  30. "_score" : 1.0,
  31. "_source" : {
  32. "body" : "Elastic is the company behind ELK stack"
  33. }
  34. }
  35. ]
  36. }
  37. ]
  38. }
  39. }