随着在线书店的运营,我们想要改善用户的搜索体验。具体的需求是当用户在搜索框输入部分内容后,出现对应书本的推荐选项,让用户可以快速选择。其实自动补全或纠错的功能现代搜索引擎都有,下面是在谷歌浏览器上输入部分关键词 “lin” 后,它给我补全的内容选项:

google自动补全.png

想要实现上述的需求,我们可以使用 ES 提供的 Suggesters API。那 Suggesters 是如何做到的呢? 简单来说,Suggesters 会将输入的文本分解为 token(token 就是根据规则切分文本后一个个的词),然后在索引里查找相似的 Term。根据使用场景的不同,ES 提供了以下 4 种 Suggester:

  • Term Suggester:基于单词的纠错补全。

  • Phrase Suggester:基于短语的纠错补全。

  • Completion Suggester:自动补全单词,输入词语的前半部分,自动补全单词。

  • Context Suggester:基于上下文的补全提示,可以实现上下文感知推荐。

今天我们将沿用之前的模型和数据来学习这几个 Suggester API,并且改善在线书店用户的搜索体验!

一、Term Suggester

Term Suggester 提供了基于单词的纠错、补全功能,其工作原理是基于编辑距离(edit distance)来运作的,编辑距离的核心思想是一个词需要改变多少个字符就可以和另一个词一致。所以如果一个词转化为原词所需要改动的字符数越少,它越有可能是最佳匹配。例如,linvx 和 linux,为了把 linvx 转变为 linux 需要改变 一个字符 ‘v’,所以其编辑距离为 1。

Term Suggester 工作的时候,会先将输入的文本切分为一个个单词(我们称这个为 token),然后根据每个单词提供建议,所以其不会考虑输入文本间各个单词的关系。

Term Suggester API 提供了很多的参数,我们先看一下示例,然后再来看参数列表。Term Suggester 示例如下:

  1. # Term Suggester,"architture" 是错误的拼写,正确的是 "architecture"
  2. POST books/_search
  3. {
  4. "query": {
  5. "match": {
  6. "name": "kernel architture"
  7. }
  8. },
  9. "suggest": {
  10. "my_suggest": {
  11. "text": "kernel architture",
  12. "term": {
  13. "suggest_mode": "missing",
  14. "field": "name"
  15. }
  16. }
  17. }
  18. }

如上示例,用户搜索了 “kernel architture”,其中 “architture” 是错误的拼写。Suggester API 需要在 “suggest” 块中指定使用的参数。”my_suggest” 是这次建议的名字,是我们自定义的。”term” 指的是使用 Term Suggester API,如果是 Phrase Suggester API 则用的是 “phrase”。

Term Suggester API 有很多参数,比较常用的有以下几个:

  • text:指定了需要产生建议的文本,一般是用户的输入内容,例子中是:”kernel architture”。

  • field:指定从文档的哪个字段中获取建议,上例中,我们从书名(name)字段中获取建议。

  • suggest_mode:设置建议的模式。其值有以下几个选项:

    • missing:如果索引中存在就不进行建议,默认的选项。上例中使用的是此选项,所以可以看到返回的结果中 “kernel” 这个词是没有建议的。
    • popular:推荐出现频率更高的词。
    • always:不管是否存在,都进行建议。
  • analyzer:指定分词器来对输入文本进行分词,默认与 field 指定的字段设置的分词器一致。

  • size:为每个单词提供的最大建议数量。

  • sort:建议结果排序的方式,有以下两个选项:

    • score:先按相似性得分排序,然后按文档频率排序,最后按词项本身(字母顺序的等)排序。
    • frequency:先按文档频率排序,然后按相似性得分排序,最后按词项本身排序。

下面是上述示例的返回结果:

  1. # 返回的结果
  2. {
  3. "hits" : {
  4. "hits" : [
  5. {
  6. "_id" : "1",
  7. "_source" : {
  8. "book_id" : "4ee82462",
  9. "name" : "Dive into the Linux kernel architecture"
  10. }
  11. }
  12. ]
  13. },
  14. "suggest" : {
  15. "my_suggest" : [
  16. {
  17. "text" : "kernel",
  18. "offset" : 0,
  19. "length" : 6,
  20. "options" : [ ]
  21. },
  22. {
  23. "text" : "architture",
  24. "offset" : 7,
  25. "length" : 10,
  26. "options" : [
  27. {
  28. "text" : "architecture",
  29. "score" : 0.8,
  30. "freq" : 1
  31. }
  32. ]
  33. }
  34. ]
  35. }
  36. }

从返回结果中可以看出,对于每个词语的建议结果,放在了 “options” 数组中。如果一个词语有多个建议,那么将按照 sort 参数指定的方式进行排序。示例中,由于 “kernel” 这个词是有存在的,并且 suggest_mode 为 “missing”,所以不进行建议,其 option 是空的。

二、Phrase Suggester

Term Suggester 产生的建议是基于每个单词的,如果想要针对整个短语或者一句话做建议,Term Suggester 就有点无能为力了。那有什么更直接的办法解决这个问题呢?可以使用 Phrase Suggester API 获取与用户输入文本相似的内容。

Phrase Suggester 在 Term Suggester 的基础上增加了一些额外的逻辑,因为是短语形式的建议,所以会考量多个 term 间的关系,比如相邻的程度、词频等。下面的 Phrase Suggester 的实例:

  1. # Phrase Suggester 使用示例
  2. POST books/_search
  3. {
  4. "suggest": {
  5. "my_suggest": {
  6. "text": "Brief Hestory Of Tome",
  7. "phrase": {
  8. "field": "name",
  9. "highlight": {
  10. "pre_tag": "<em>",
  11. "post_tag": "</em>"
  12. }
  13. }
  14. }
  15. }
  16. }
  17. # 结果
  18. {
  19. ......
  20. "suggest" : {
  21. "my_suggest" : [
  22. {
  23. ......
  24. "options" : [
  25. {
  26. "text" : "brief history of time",
  27. "highlighted" : "brief <em>history</em> of <em>time</em>",
  28. "score" : 0.030559132
  29. },
  30. {
  31. "text" : "brief history of tome",
  32. "highlighted" : "brief <em>history</em> of tome",
  33. "score" : 0.025060574
  34. },
  35. {
  36. "text" : "brief hestory of time",
  37. "highlighted" : "brief hestory of <em>time</em>",
  38. "score" : 0.0236486
  39. }
  40. ]
  41. }
  42. ]
  43. }
  44. }

如上示例,”phrase” 指定使用 Phrase Suggester API。从返回结果可以看出,”options” 返回了一个短语列表,并且因为 “history” 和 “time” 在一个文档里出现过,其可信度相对于其他来说更高,所以得分更高。因为我们使用了 “highlight” 选项,所以返回结果中被替换的词语会高亮显示。

Phrase Suggester 可用的参数也是比较多的,下面介绍几个用得比较多的参数选项:

  • max_error:指定最多可以拼写错误的词语的个数。
  • confidence:其作用是用来控制返回结果条数的。如果用户输入的数据(短语)得分为 N,那么返回结果的得分需要大于 N * confidence。confidence 默认值为 1.0。
  • highlight:高亮被修改后的词语。

三、Completion Suggester

Completion Suggester 提供了自动补全的功能,其应用场景是用户每输入一个字符就需要返回匹配的结果给用户。在并发量大、用户输入速度快的时候,对服务的吞吐量来说是个不小的挑战。所以 Completion Suggester 不能像上面的 Suggester API 那样简单通过倒排索引来实现,必须通过某些更高效的数据结构和算法才能满足需求。

Completion Suggester 在实现的时候会将 analyze(将文本分词,并且去除没用的词语,例如 is、at这样的词语) 后的数据进行编码,构建为 FST 并且和索引存放在一起。FST(finite-state transducer)是一种高效的前缀查询索引。由于 FST 天生为前缀查询而生,所以其非常适合实现自动补全的功能。ES 会将整个 FST 加载到内存中,所以在使用 FST 进行前缀查询的时候效率是非常高效的。

在使用 Completion Suggester 前需要定义 Mapping,对应的字段需要使用 “completion” type。下面我们将构建一个新的 books_completion 索引,其 Mapping 和测试数据如下:

bash

  1. # 先删除原来的索引和数据
  2. DELETE books_completion
  3. # 新增 "name_completion" 字段做 Completion Suggester 测试, 其类型为 "completion"
  4. PUT books_completion
  5. {
  6. "mappings": {
  7. "properties": {
  8. "book_id": {
  9. "type": "keyword"
  10. },
  11. "name": {
  12. "type": "text",
  13. "analyzer": "standard"
  14. },
  15. "name_completion": {
  16. "type": "completion"
  17. },
  18. "author": {
  19. "type": "keyword"
  20. },
  21. "intro": {
  22. "type": "text"
  23. },
  24. "price": {
  25. "type": "double"
  26. },
  27. "date": {
  28. "type": "date"
  29. }
  30. }
  31. },
  32. "settings": {
  33. "number_of_shards": 3,
  34. "number_of_replicas": 1
  35. }
  36. }
  37. PUT books_completion/_doc/1
  38. {
  39. "book_id": "4ee82462",
  40. "name": "Dive into the Linux kernel architecture",
  41. "name_completion": "Dive into the Linux kernel architecture",
  42. "author": "Wolfgang Mauerer",
  43. "intro": "The content is comprehensive and in-depth, appreciate the infinite scenery of the Linux kernel.",
  44. "price": 19.9,
  45. "date": "2010-06-01"
  46. }
  47. PUT books_completion/_doc/2
  48. {
  49. "book_id": "4ee82463",
  50. "name": "A Brief History Of Time",
  51. "name_completion": "A Brief History Of Time",
  52. "author": "Stephen Hawking",
  53. "intro": "A fascinating story that explores the secrets at the heart of time and space.",
  54. "price": 9.9,
  55. "date": "1988-01-01"
  56. }
  57. PUT books_completion/_doc/3
  58. {
  59. "book_id": "4ee82464",
  60. "name": "Beginning Linux Programming 4th Edition",
  61. "name_completion": "Beginning Linux Programming 4th Edition",
  62. "author": "Neil Matthew、Richard Stones",
  63. "intro": "Describes the Linux system and other UNIX-style operating system on the program development",
  64. "price": 12.9,
  65. "date": "2010-06-01"
  66. }

如上示例,新增 “name_completion” 字段做 Completion Suggester 测试, 其类型为 “completion”。在测试数据准备好后,可以执行下面 Completion Suggester 的使用示例:

  1. # Completion Suggester
  2. POST books_completion/_search
  3. {
  4. "suggest": {
  5. "my_suggest": {
  6. "prefix": "a brief hist",
  7. "completion": {
  8. "field": "name_completion"
  9. }
  10. }
  11. }
  12. }
  13. # 结果
  14. {
  15. ......
  16. "suggest" : {
  17. "my_suggest" : [
  18. {
  19. "text" : "a brief hist",
  20. "offset" : 0,
  21. "length" : 12,
  22. "options" : [
  23. {
  24. "text" : "A Brief History Of Time",
  25. "_id" : "2",
  26. "_source" : {
  27. "book_id" : "4ee82463",
  28. "name_completion" : "A Brief History Of Time",
  29. ......
  30. }
  31. }
  32. ]
  33. }
  34. ]
  35. }
  36. }

如上示例,在 “my_suggest” 中,”prefix” 指定了需要匹配的前缀数据,”completion” 中的 “field” 指定了需要匹配文档的哪个字段。 返回结果中 “options” 包含了整个文档的数据。

需要注意的是,Completion Suggester 在索引数据的时候经过了 analyze 阶段,所以使用不同的 analyzer(分词器) 会造成构建 FST 的数据不同,例如某些词(is、at等停用词)被去除、某些词被转换(大小写等)。由于构建的数据不同,可能会影响查询匹配的结果。

四、Context Suggester

Context Suggester 是 Completion Suggester 的扩展,可以实现上下文感知推荐。例如当我们在编程类型的书籍中查询 “linu” 的时候,可以返回 linux 编程相关的书籍,但在人物自传类型的书籍中,将会返回 linus 的自传。 要实现这个功能,可以在文档中加入分类信息,帮助我们做精准推荐。

ES 支持两种类型的上下文:

  • Category:任意字符串的分类。
  • Geo:地理位置信息。

下面我们看看如何基于任意字符串的分类来做上下文推荐。同样,在使用 Context Suggester 前,首先要创建 Mapping,然后在数据中加入相关的 Context 信息。下面是使用 Context Suggester 时的 Mapping:

  1. #删除原来的索引
  2. DELETE books_context
  3. # 创建用于测试 Context Suggester 的索引
  4. PUT books_context
  5. {
  6. "mappings": {
  7. "properties": {
  8. "book_id": {
  9. "type": "keyword"
  10. },
  11. "name": {
  12. "type": "text",
  13. "analyzer": "standard"
  14. },
  15. "name_completion": {
  16. "type": "completion",
  17. "contexts": [
  18. {
  19. "name": "book_type",
  20. "type": "category"
  21. }
  22. ]
  23. },
  24. "author": {
  25. "type": "keyword"
  26. },
  27. "intro": {
  28. "type": "text"
  29. },
  30. "price": {
  31. "type": "double"
  32. },
  33. "date": {
  34. "type": "date"
  35. }
  36. }
  37. },
  38. "settings": {
  39. "number_of_shards": 3,
  40. "number_of_replicas": 1
  41. }
  42. }
  43. # 导入测试数据
  44. PUT books_context/_doc/4
  45. {
  46. "book_id": "4ee82465",
  47. "name": "Linux Programming",
  48. "name_completion": {
  49. "input": ["Linux Programming"],
  50. "contexts": {
  51. "book_type": "program"
  52. }
  53. },
  54. "author": "Richard Stones",
  55. "intro": "Happy to Linux Programming",
  56. "price": 10.9,
  57. "date": "2022-06-01"
  58. }
  59. PUT books_context/_doc/5
  60. {
  61. "book_id": "4ee82466",
  62. "name": "Linus Autobiography",
  63. "name_completion": {
  64. "input": ["Linus Autobiography"],
  65. "contexts": {
  66. "book_type": "autobiography"
  67. }
  68. },
  69. "author": "Linus",
  70. "intro": "Linus Autobiography",
  71. "price": 14.9,
  72. "date": "2012-06-01"
  73. }

如上所示的 Mapping,其中 “name_completion” 的类型还是为 “completion”,在 “contexts” 中有两个字段,其中 “type” 为上下文的类型,就是上面提到的 Category 和 Geo,本例子使用了 Category。而 “name” 则为 上下文的名称(即哪个分类),本例子为 “book_type”。

导入的数据中,”name_completion” 中的 “input” 字段用于内容匹配。”book_type” 的值有多个,”program” 是编程类的,”autobiography” 是自传类的。

导入数据成功后,下面来看看 Context Suggester 的使用例子:

  1. POST books_context/_search
  2. {
  3. "suggest": {
  4. "my_suggest": {
  5. "prefix": "linu",
  6. "completion": {
  7. "field": "name_completion",
  8. "contexts": {
  9. "book_type": "program"
  10. }
  11. }
  12. }
  13. }
  14. }

如上示例,还是使用 “prefix” 字段来指定需要匹配的前缀数据,其将与 “input” 字段的数据进行匹配。而 “contexts” 中指定了 “book_type” 为 “program”。所以查询的意思是:在书本类别为 “program” 的数据里,推荐以 “linu” 开头的书本。

五、总结

今天为你介绍了 ES 提供的 4 种 Suggester API。Suggester API 可以对用户的输入提供相关的推荐选项,利用这个功能可以完善用户的搜索体验。下面来简单总结一下这 4 种 Suggester:

  1. Term Suggester 提供基于单词的纠错、补全功能。 其工作原理是基于编辑距离(edit distance)来运作的,编辑距离的核心思想是一个词需要改变多少个字符就可以和另一个词一致。Term Suggester 是根据每个单词提供建议,所以其不会考虑输入文本间各个单词的关系

  2. Phrase Suggester 是基于短语的纠错补全的,不像Term Suggester 只能提供基于单词的纠错。所以其会考量多个 term 间的关系,比如相邻的程度、词频等。

  3. Completion Suggester 提供自动补全单词的功能,输入词语的前半部分,自动补全整个单词。由于 Completion Suggester 需要比较高的性能,所以底层使用了 FST 来实现。需要注意的是,Completion Suggester 在写入数据时会将数据进行分词等操作,而不同的分词器分词后的结果不尽相同,这会导致构建的 FST 也是不同的,这样将会造成某些查询无法匹配到结果的现象。

  4. Context Suggester 提供基于上下文的补全功能。当我们需要根据不同类别进行相关上下文补全的时候,Context Suggester 就派上用场了。Context Suggester 可以定义两种上下文类型:Category 和 Geo,其中我们今天详细讲解了 Category 类型的实例。

好了今天的内容到此为止,今天介绍的 Suggester 是比较常用的功能了,而这个几种 Suggester 提供的参数远不止我们今天提到的这部分,更多的使用例子可以参考官方文档