本篇文章记录一些关于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],常见归一算法在这里:数据标准化。
下面就是一个归一化的算法例子,无论输入多大或者多小,输出总是在一个小区间里,同时仍旧能反馈出一个数字的大小程度:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
>>> import math >>> math.atan(0.5) * 2 / math.pi 0.29516723530086653 >>> math.atan(123123123) * 2 / math.pi 0.99999999482940538 >>> math.atan(123123123) * 2 / math.pi 0.99999999482940538 >>> math.atan(3) * 2 / math.pi 0.79516723530086653 >>> math.atan(1) * 2 / math.pi 0.5 >>> math.atan(0.5) * 2 / math.pi 0.29516723530086653 |
_score是ES算的分,不放心也可以进行一次归一化。
默认_score * 兴趣分相当于权重1:1,如果我们更在意文本相关性,那么可以使用0.8*_score + 0.2*兴趣分,这样可以避免出现文本相关性很低但是兴趣很高的文章排到最前面,给用户造成困扰。
以上只是思路,并未经过实践验证。
相关阅读
如果文章帮助您解决了工作难题,您可以帮我点击屏幕上的任意广告,或者赞助少量费用来支持我的持续创作,谢谢~
