Elasticsearch搜索订制

本篇文章记录一些关于ES搜索原理方面的思考,不一定全部正确,但还是先写下来。

搜索流程

搜索分几个重要阶段:

  • 查询改写
    • 说明:比如用户输入KFC,你发起搜索的的时候,得搜索KFC或者肯德基;还有一些语义化、智能化的东西,比如该用户经常搜手机,那么当他查询”苹果”的时候,你应该更偏重返回”苹果手机”,而不是”水果”。
    • 其他:ES有拼写纠错接口,但是像这些同义词,智能识别之类的是没有的,需要业务自己去实现,最后将改写后的查询提交给ES。
  • 召回:
    • 说明:搜索词经过分词成多个term,然后通过倒排索引找到包含term的文档集合,这基本是一个求并集的过程,也就是假若文档包含任意term,那么它就会被筛选回来。
    • 其他:ES采用lucene实现了这一块功能,而ES本身通过sharding分片实现lucene的分布式;我认为,一次搜索很有可能召回一个shard内所有的文档,这可能是一个千万级别的集合。
  • 粗排:
    • 说明:在召回了大量文档后,一般来说会返回与搜索相关性高的文档,因此必须计算每个文档的相关性,最终排序才知道哪个文档相关性高,所以这个过程无法避免。简单的理解,召回的千万文档集合(假设这么大的话),需要遍历每个文档,计算其包含了哪些term,并根据这些term在文档中的出现次数等信息,计算出文档的相关性得分,而这个计算方法就是tf/idf算法。
    • 其他:对千万级召回文档遍历算分并按相关分排序,用C/C++实现大概需要1秒以上,这个粗排一般是一个很简单高效的计算公式,只是大概计算文档与搜索的一个相关性,所以好像网上并没有人提过这个召回量大的问题。
  • 精排:
    • 说明:假设召回的千万文档粗排完成,一般的搜索业务也就可以直接取top N返回了。但是随着业务深入,一定不可能只基于默认相关性排序,而是会根据业务需求订制相关性计算的算法。
    • 其他:ES支持function_score或者script_score,简单的说就是在粗排的结果集上再执行我们自定义的计算脚本,脚本的输入是文档内容以及粗排阶段基于tf/idf计算的相关性,我们可以自定义算法生成一个新的相关性得分,作为最终排序的依据。貌似ES也只是把粗排的集合直接交给精排阶段,所以这个阶段也可能是千万级(假设单个shard召回千万)

相关性打分

ES可以搜索一个字段、多个字段,每个字段可以搜索一个term、多个term,综合起来的打分关系大概是这样的:

  • 1,搜索一个字段,只传一个term:
    • 从term的倒排拉链里,找出所有文档。
    • 对于每个文档,算term的tf/idf分数。
  • 2,搜索一个字段,传多个term:
    • 找到每个term的倒排拉链,因为每个拉链内按文档ID有序,所以进行多路归并,最终得到一个”文档ID – 命中的term数组”的列表。
    • 遍历上述结果列表,对每个文档ID,根据其命中的term数组,计算文档分数。这个过程可以简单的理解为,命中几个term,就把这几个term的tf/idf进行加和。
  • 3,搜索多个字段,每个字段传1个term:
    • 对于每个字段,执行1)中的方法计算文档的分数。
    • 最终,对于每个文档来说,将各个字段的分数进行加和,然后求均值。
  • 4,搜索多个字段,每个字段传多个term:
    • 对于每个字段,执行2)中的方法计算文档的分数。
    • 最终,对于每个文档来说,将各个字段的分数进行加和,然后求均值。

上述几个场景,是默认ES在处理查询时的一些计算行为。

ES提供了很多查询参数,可以改变上述的算分默认行为,比如给某个字段调高权重,从而在算分时提升其影响力;或者,整个文档的分数直接取分最高的那个字段的分数。

个性化

搜索和推荐都可以实现个性化,其原理还是可以大概理解的。

假设我们收集了用户最近的浏览历史,获知他喜欢手机和游戏,那么我们就可以给用户关联这两个标签,并且根据喜欢的程度给予一定的权重,比如:手机5分,游戏15分。

同样的,我们可以给商品也打一些标签,比如:游戏”魔兽世界”关联”游戏”标签,并且”魔兽世界”本身有自己的商品属性,比如销量,浏览量等等。

当用户搜索一个关键词”世界”时,直接去ES中匹配商品的标题、分类等文本信息,这样就可以根据从倒排中召回一大波商品(其中包含了”魔兽世界”这个商品),ES默认会对召回结果进行粗排。

接下来,我们需要实现自定义的精排相关性计算函数:如果商品的标签包含”手机”或者”游戏”,那么就给这个商品进行额外的加分,从而让它往前排。

比如:粗排相关性得分是_score,它表示了文本匹配的一个相关性,但是为了实现个性化,在商品也包含”游戏”标签的情况下,我可以对_score进行一定比例的放大(乘法),这样就会在文本相关性接近的情况下把感兴趣的商品往前排了。

归一化

我们可以根据商品标签累加用户对商品的兴趣分,比如”魔兽世界”这个商品的兴趣有15分,而”炉石传说”因为既是游戏又可以在手机上玩(关联”游戏”、”手机”两个标签),所以是15+5=20分。

如果直接用_score * 15,_score * 20这样算最终得分,看起来没什么问题,但如果某个商品的兴趣分有300分,那么就有可能把有个很感兴趣但是文本关联性很低的商品提到首位,这也不是我们想要的。

所以,一般会对业务数据进行归一化,变成一个有限区间内的数字,一般是[0,1],常见归一算法在这里:数据标准化

下面就是一个归一化的算法例子,无论输入多大或者多小,输出总是在一个小区间里,同时仍旧能反馈出一个数字的大小程度:

_score是ES算的分,不放心也可以进行一次归一化。

默认_score * 兴趣分相当于权重1:1,如果我们更在意文本相关性,那么可以使用0.8*_score + 0.2*兴趣分,这样可以避免出现文本相关性很低但是兴趣很高的文章排到最前面,给用户造成困扰。

以上只是思路,并未经过实践验证。

相关阅读

OpenSearch

搜索引擎中输入检索词到返回十条结果,发生了哪些事情

阿里巴巴:电商场景下云搜索应用与实践

有赞搜索引擎实践(算法篇)

如果文章帮助您解决了工作难题,您可以帮我点击屏幕上的任意广告,或者赞助少量费用来支持我的持续创作,谢谢~