再论elasticsearch自动补全在实战中的优化

实践加深理解,业务多变性带来实现复杂性,关于搜索中的自动补全,我又做了一些补充,也可以看看以前的文章 《elasticsearch自动补全优化》

排序

对于”测试1”,”测试2” 两个 input
词来说,不管这两次插入到索引的顺序是什么,suggets词是”测试”,则优先匹配到”测试1”,也就是按照后面的数字排序。
对于”测试好”,”测试好吗”两个纯粹的中文词来说,suggets词是”测试”,会优先匹配到”测试好”,也就是说词短的优先,这是合理的。
对于”测试111”,”测试2” 两个词来说,suggets词是”测试”,则优先匹配到”测试111”,也就是和短语长短没有关系,优先以数字顺序匹配。
对于”测试a”,”测试bcd” 两个词来说,suggets词是”测试”,则优先匹配到”测试bcd”,也就是和短语长短没有关系,优先以字母顺序匹配。
对于’[“测试我”,”测试你”,”测试它”]’ 和 ‘[“测试我”]’来说,suggets词是”测试”,匹配到顺序是前一个,suggets数量越多,则匹配的优先级较高。

整体来说,suggets 对优先级的控制是较弱的,它理论上是为了匹配,忽略了算分,只要匹配到了则一视同仁(_score都是1.0),只是在 排序
的时候依据数字大小、字母ASCII码、input索引到ES的顺序。
但可以人为控制排序,接下去会说weight。

preserve_separators 参数

默认是true,如果设置为false,如果你的suggets是ABC,则会匹配到AB CD,就是会忽略原始词中的空格,理解起来非常拗口,线上使用一般建议设置为false。

Preserves the separators, defaults to true. If disabled, you could find a field starting with Foo Fighters, if you suggest for foof.

skip_duplicates

这个要谨慎使用,就是去除匹配到的重复suggets词。
比如’[“测试我”,”测试你”]’和’[“测试我”]’,如果suggets词是”测试我”,则只会匹配到一条,而suggets词是”测试”,则会匹配到两条。
因为我们的索引词是重运营的,同一种性质的数据会包含多个索引词,如果skip_duplicates设置为true,很容易过滤掉包含同样索引词但其实性质不一样的数据,所以需要谨慎使用。可以在索引的时候尽量由应用保证只有单个索引词的数据尽量不要重复,这很容易做到。
另外如果设置为true,也会影响性能。

fuzzy

这个这次终于测底搞明白了,completion 也可以具备一定的纠错功能,什么意思呢,比如我们有个产品叫“西红试”,但可能很多人会输入“西红柿”,通过fuzzy配置一定程度上可以让两者匹配到,看代码:

POST test/_doc/x1
{
  "suggest": [
    {
      "input": [
        "西红柿活动",
        "abcde"
      ]
    }
  ]
}
POST test/_doc/x2
{
  "suggest": [
    {
      "input": [
        "西红试活动",
        "ac"
      ]
    }
  ]
}

POST test/_doc/x3
{
  "suggest": [
    {
      "input": [
        "西红饼",
        "abcde"
      ],
    }
  ]
}

然后:

POST test/_search?pretty
{
  "suggest": {
    "xwj-suggest": {
      "text": "西红饼",
      "completion": {
        "field": "suggest",
        "size": 30,
        "fuzzy": {
          "fuzziness": "AUTO",
          "min_length": 3,
          "prefix_length": 2,
          "unicode_aware": true
        }
      }
    }
  }
}

在我开始测试的时候,总是不成功,但如果你输入的是suggests是西红a就可以,而如果是“西红饼”则不行,原因就在于unicode_aware参数,我们是中文语系,一定要使用 Unicode code points
的长度,而不是字节数,切记!

fuzzy基于 Levenshtein Edit Distance
算法,fuzziness参数很重要,如果设置的较大,可能会将一些无用的词也搜索出来,所以官方建议设置为AUTO,这样更智能。
而min_length和prefix_length是避免出现更多无用结果,也是为了控制精度的,比如说你输入的suggests至少长度是3才进行fuzzy,而prefix前缀匹配数达到prefix_length才进行fuzzy。

我在线上目前没有使用fuzzy,原因有几点:第一项目快上线了,没有十足把握就不更改了。第二从这个例子看出,居然x3这个ID的排序是最低的,它可是 精确匹配
啊!第三还是诟病 completion suggester
的排序规则,但可以通过weight参数缓解。

另外在后台配置input词也很有技巧,比如说就应该设置“西红柿”这个词,让其和“西红试”对应的活动匹配上,就认为是同义词,从而让 策略去优化技术实现

weight

按照 completion suggester
这个技术来说,只要匹配到了,排序完全可以依赖于这个input这个词的历史搜索量进行排序,但在实际应用的过程中,就有一些问题。
比如我们是重运营的:

  • 一个活动会配置多个input词。
  • kol用户的昵称会作为input导入到ES中,即使匹配到两个kol用户,运营规则是只出现一个,那怎么排序?
    当然应用程序可以根据粉丝量进行排序,但这样的规则太多了。
    比如话题是不是要根据发文量排序?
  • 历史搜索词也会作为input词导入到ES,它们可以根据历史搜索量配置weight,但其他input词(比如kol昵称)的weight应该设置为多少?
    维度不一样。

总而言之,weight怎么配置,怎么通过 completion suggester
技术一次性解决?我目前的想法就是区分运营input词和历史搜索input词,历史搜索input词的weight有个封顶(至少保证他们之间排序是比较不错的),运营input词根据不同数据的性质配置weight。
就是说input相同性质的weight是可依赖的,而不同性质的input对应的weight只能人为控制了。

但理论上只要 completion suggester
能匹配出来,排序可以让应用程序去做(比考虑性能消耗),这就要说到size参数了。
目前weight控制还没有上线。

size

size参数是控制返回的条目数,由于业务的复杂性,且没有weight的控制,size必须足够大,才能取出所有的匹配input词,从而让应用程序更好的控制。
现在线上设置的是30,感觉有点少。设置30和50对ES估计影响不大,但网络传输的size、内存size、cpu处理消耗就比较大了,尤其input包含了很多运营属性的值。

建模

为了方便,我将 completion suggester
的input词和 search
的match词是放在同一个索引中的,这样既能搜索又能 completion suggester
。但这样存储空间就大了。

另外除了input词,还有很多的运营属性数据,比如文章数、商品描述等等,这些由于业务的复杂性,很难预先设置 mapping
,只能 动态mapping
,全部存储在other这个字段中,而动态mapping有很多弊端,比如其实other里面的数据无需搜索或者term(但以后可能会),浪费空间(会影响性能)。
这个也可能要二期去优化了。

standard还是simple分析器

《elasticsearch自动补全优化》
中也提到,默认 completion suggester
使用的分析器是simple,是不支持数字completion的,而standard则支持,但具体区别在哪儿没有深究,但切记不要使用中文分词器,没用且可能有干扰。