9.工具使用:Elasticsearch从入门到放弃(1)-Elasticsearch概念篇
10.工具使用:Elasticsearch从入门到放弃(2)-相关性算法
11.工具使用:Elasticsearch从入门到放弃(3)-权重及打分 (qq.com)
文档摘自《Elasticsearch搜索引擎构建入门与实战》
参考:
https://blog.csdn.net/qq_29860591/article/details/109574595 https://blog.csdn.net/pbrlovejava/article/details/122290078 https://www.cnblogs.com/huan1993/p/15416099.html elasticsearch地理位置查询 https://www.freesion.com/article/5162262542/#41__364 FUNCTION_SCORE详解 复制代码
1.查询时设置权重
在默认情况下,这些查询的权重都为1,也就是查询之间都是平等的。有时我们希望某些查询的权重高一些,也就是在其他条件相同的情况下,匹配该查询的文档得分更高。此时应该怎么做呢?本节将介绍的boosting查询和boost设置可以满足上述查询需求。
1.1 查询时boost参数的设置
在ES中可以通过查询的boost值对某个查询设定其权重。在默认情况下,所有查询的boost值为1。但是当设置某个查询的boost为2时,不代表匹配该查询的文档评分是原来的2倍,而是代表匹配该查询的文档得分相对于其他文档得分被提升了。例如,可以为查询A设定boost值为3,为查询B设定boost值为6,则在进行相关性计算时,查询B的权重将比查询A相对更高一些。
boost值的设置只限定在term查询和类match查询中,其他类型的查询不能使用boost设置。boost值没有特别约束,因为它代表的是一个相对值。当该值在0~1时表示对权重起负向作用,当该值大于1时表示对权重起正向作用。
下面对索引进行查询,假设“金都”或者“文雅”是两个酒店的品牌,用户想查询标题中包含“金都”或者“文雅”的酒店文档:
在默认情况下,各个子查询的boost值为1,也就是说上述的两个match查询是平等的。文档的分值等于两个match相关性分数之和。执行上述DSL后结果如下:
通过上述结果可以看到,“金都”品牌的酒店文档的打分相对低一些,如果想对“金都”品牌的酒店进行推广,也就是提升标题中包含“金都”这些文档的排序分值,则可以设定“金都”的match查询的boost值更高一些,例如下面的DSL:
执行上述DSL后,ES的返回结果如下:
如上所示,设定的boost值提升了标题中包含“金都”的文档的得分。现在我们来思考一下上述match查询的打分细节,通过6.1节可以知道,在默认情况下,文档的boost为BM25中的k1+1,因为在默认情况下k1=1.2,所以boost=k1+1=1.2+1=2.2。当在match查询中设置boost为2时,匹配该查询文档的最终boost=(k1+1)×2=(1.2+1)×2=4.4。可以使用explain进行验证:
下面将上述DSL执行后的部分结果进行折叠,仅显示第一个文档的打分细节:
通过上面的结果可以看到,title字段使用标准分析器,设置“金都”这个match查询的boost值为2后,在查询时“金都”被切分成“金”“都”,这两个切分的字在BM25查询中的最终boost值都为4.4。因此设置match查询的boost参数可以直接影响BM25的评分机制,从而影响整体结果的相关度。更近一步说,设置boost参数为某个值后并不是将查询命中的文档分数乘以该值,而是将BM25中的boost参数乘以该数值。
1.2 在Java客户端中使用boost参数
在Java客户端中使用boost参数时,只需要在QueryBuilder实例中调用boost()方法即可,以下Java代码和上面的DSL在搜索结果上是等效的:
1.3 boosting查询
虽然使用boost值可以对查询的权重进行调整,但是仅限于term查询和类match查询。 有时需要调整更多类型的查询,如搜索酒店时,需要将房价低于200的酒店权重降低,此时可能需要用到range查询,但是range查询不能使用boost参数,这时可以使用ES的boosting查询进行封装。
ES的boosting查询分为两部分,一部分是positive查询,代表正向查询,另一部分是negative查询,代表负向查询。 可以通过negative_boost参数设置负向查询的权重系数,该值的范围为0~1。最终的文档得分为:正向匹配值+负向匹配值×negative_boost。
先来看看使用原始查询时的搜索排序状态,以下DSL为搜索“金都”查询:
可以看到,上面的搜索结果只是按照标题相关度进行了打分,其中,标价为200元的“金都欣欣酒店”排在第二位,如果希望它排在最后该怎么做呢?下面的DSL将对房价低于200元的酒店进行降权处理:
通过上面的结果可知,对房价低于200元的酒店进行降权处理后,目标酒店已经排在了最后面。
如果在以上结果基础上要求降低满房酒店的权重该怎么做呢?我们可以将在negative中的查询进行扩展:
在以上查询中,使用布尔查询将“房价低于200元”和“满房状态”的酒店封装到了一个布尔查询中然后放入negative查询中,执行上述DSL后,搜索结果如下:
如上所示,通过使用negative进行调权,“房价低于200元”和“满房状态”的酒店都排在了后面。
在Java客户端中使用boosting查询
在Java客户端中可以使用QueryBuilders.boostingQuery()方法构建BoostingQueryBuilder的实例对象,该方法有两个参数,即positiveQuery和negativeQuery,分别代表正向查询和负向查询。构建完BoostingQueryBuilder对象之后,可以使用其negativeBoost()方法设置负向查询的权重系数。以下Java代码和上面的DSL是等效的:
1.4 Function Score查询简介
前面几节介绍了ES的相关性排序,其中可以定制相关性算法及对其配套的参数进行设置。但是用户没有办法更多地参与到文档打分逻辑中,尤其是当前的业务需求不仅依靠相关性进行排序,而是由多种因素共同作用的,排序的规则比较复杂。例如,一个酒店搜索引擎,不仅需要考虑查询词和酒店名称的匹配程度,而且还需要评估酒店的好评率、地理位置和设施服务等诸多因素。ES提供了Function Score查询模式,用户可以借助该查询模式使用多种因素来影响打分逻辑。
1.4.1 简单函数
我们在第4章中简要介绍了在Function Score查询中可以使用random_score随机函数对文档进行打分,在Function Score查询中还提供了其他打分函数,如表6.1所示。
在这些函数中,比较灵活的是script_score函数,它支持用户使用脚本自定义评分函数。在函数体中,既可以使用原有的score值进行计算,也可以使用文档的某个字段值的一些运算结果来影响评分的值。下面使用script_score函数将原有评分和好评数相乘的结果作为最后的评分是,示例如下:
需要注意的是,script_score子句中的结果必须大于或者等于0,不能为负数,否则,ES将会报错。以下是script_score取值为原有评分和差评数的乘积再乘以-1的查询示例:
另外,还可以使用params为script_score传递参数。在下面的示例中,将原有评分和评论进行相乘,然后乘以p,并将得到的结果作为最后的评分。其中,p的值是通过params中的传递参数得到的。
random_score产生0~1的随机小数,但是不包括1。在默认情况下,该随机函数使用的随机种子为文档_id,可以通过seed参数指定随机数种子。例如,使用随机函数的简单形式对文档打分:
如果在Function Score的functions子句中有多个函数,则可以使用score_mode参数定义各个函数值之间的计算关系,当前支持的计算关系如表6.2所示。
1.4.2 衰减函数
在对文档进行打分时,希望在某个值域附近进行衰减打分。例如当搜索酒店时,酒店距离当前位置越近越好。假定距离当前位置1km范围内的酒店都可以接受,如果使用过滤器将超过1km的酒店排除掉,这种做法未免有些“生硬”。假设一个酒店距离当前位置刚好是1.1km,其好评度也不错,那么是可以考虑一下该酒店的。所以我们希望酒店最好距离当前位置在1km范围内,如果超过1km,酒店的评分应该随着距离的增大有一个明显的下降趋势。为了解决这类问题,可以在Function Score查询中使用衰减函数。
ES提供了3个衰减函数,分别为gauss、linear和exp,这3个函数的区别主要是衰减曲线形状不同,但是它们的用法和参数设置都是一样的。如图6.4所示为以年龄
衰减函数可以用于数值型、日期型和地理位置型数据,需要用户设置一个中心值,如果实际值偏离中心值,无论大于中心值还是小于中心值,文档的分数都将降低。由图6.4可知,
gauss图像有点类似于钟摆,起初,其衰减值随着年龄的增大缓慢增大,然后到达某个区域后急速增大,直到到达某个阈值后又急速减小,最后又缓慢减小;
linear的函数曲线是一条直线,其衰减值随着年龄的增大而线性增大,直到到达某个阈值后随着年龄的增大而线性减小;
exp是一种指数衰减,它的衰减速率比gauss要激烈一些。
- 使用衰减函数时,可以设定如下参数:
- origin:用于设定计算距离的原点,该参数的值必须和字段类型相对应。
- offset:用于设定距离原点多远范围内的数据将享有和原点一样的衰减值,其默认值为0。
- scale:衰减曲线的一个锚点,即定义到该点的值,其衰减的值为某个值(即为decay的值)。这个锚点横坐标值的定义为原点+offset+scale,纵坐标为decay参数的值。
- decay:和scale配套使用,用于设定锚点的纵坐标,即衰减值,其默认值为0.5。
假设当前位置是天安门,经纬度坐标为[116.4039,39.915143],需求为搜索附近5km内的酒店,其中最佳距离是1km,超过1km的酒店打分需要按照距离进行衰减,其中3km的时候酒店衰减得分为0.4,则搜索的DSL如下:
1.4.3 Script Score
现在让我们进入灵活度最高的排序打分世界!ES提供的Script Score查询可以以编写脚本的方式对文档进行灵活打分,以实现自定义干预结果排名的目的。Script Score默认的脚本语言为Painless,在Painless中可以访问文档字段,也可以使用ES内置的函数,甚至可以通过给脚本传递参数这种方式联通内部和外部数据。
1.4.3.1 Painless简介
ElasticStack在升级到5.0版本之后,带来了一个新的脚本语言,painless。这里说“新的“是相对与已经存在groove而言的。还记得Groove脚本的漏洞吧,Groove脚本开启之后,如果被人误用可能带来各种漏洞,为什么呢,主要是这些外部的脚本引擎太过于强大,什么都能做,用不好或者设置不当就会引起安全风险,基于安全和性能方面,所以elastic.co开发了一个新的脚本引擎,名字就叫Painless,顾名思义,简单安全,无痛使用,和Groove的沙盒机制不一样,Painless使用白名单来限制函数与字段的访问,针对es的场景来进行优化,只做es数据的操作,更加轻量级,速度要快好几倍,并且支持Java静态类型,语法保持Groove类似,还支持Java的lambda表达式。
painless可以用在所有可以使用script的场景下,并具有以下特性
- 高性能。painless在es的运行速度是其他语言的数倍
- 安全。使用白名单来限制函数与字段的访问,避免了可能的安全隐患
- 可选类型。你可以在脚本当中使用强类型的编程方式或者动态类型的编程方式。
- 语法。扩展了java的基本语法以兼容groove风格的脚本语言特性,使得plainless易读易写
- 有针对的优化。这门语言是为elasticsearch专门定制的。
1.4.3.2 数据类型
1.变量
变量在使用之前必须先进行声明,其声明方式和Java保持一致。如果变量在声明时没有指定值,则使用变量类型对应的默认值。
2.数据类型
Painless支持的原始类型有:byte、short、char、int、long、float、double和boolean。可以按照Java的方式声明它们,例如下面的代码:
int i=0; boolean a=true; double s; 复制代码
在Painless中也可以使用引用类型,可以使用new关键字对引用类型进行初始化,例如下面的代码:
//声明List类型时有两种方式,方式一为直接定义空的List,此时有隐式声明和显式声明两种 List l=new ArrayList();//显式 List l2=[];//隐式 l.add(1); //声明Map类型时有两种方式,方式一为直接定义空的Map,此时有隐式声明和显式声明两种。 Map map=[:]//隐式 Map map2=new HashMap(); map =["k1":"v1","k2":"v2"]; //在Painless中使用Set类型时,可直接使用new关键字进行Set的定义,例如: Set set=new HashSet(); set.add(1); set.add(2); for (def e in set){ .... } String a="abcd"; int xx=new int[3]; a[0]=1; a[1]=2; a[2]=3; int xx2=new int[3]{1,2,3}; 复制代码
painless还支持使用def对动态类型的变量进行声明,它的作用是在声明时不指定类型而是在运行时判断数据类型,例如下面的代码:
def x=5; def y="adc"; 复制代码
1.4.3.3 条件和循环
在Painless中条件语句的使用和大多数语言是一样的,其支持使用if语句对条件进行判断,但是不支持else if或者switch语句,if语句中的条件值为boolean型。如果引用文档中的字段,编写代码时需要注意判断字段为空的情况,在后面的内容中将会介绍如何在Painless脚本中使用文档字段值。以下代码为if条件判断:
def a=10; if(a>0){ ..... } 复制代码
Painless支持for循环、while循环和do…while循环,循环内的条件和if相同,下面的代码演示了for循环的一种使用方法:
def result=10; for(def a=0;a<10;a++){ result=a+1; } 复制代码
1.4.3.4 在Script Score中使用Painless
在Script Score查询中可以使用Painless脚本进行打分脚本的开发,脚本代码主体放在参数source的值中,注意,Script Score查询中的脚本代码必须有返回值并且类型为数值型,如果没有返回值,则Script Score查询默认返回0。
下面定义酒店索引的结构如下:
为方便演示,向酒店索引中新增如下数据:
以下代码演示了使用脚本代码进行打分的基本方法:
1.4.3.5 使用文档数据
1.使用普通字段
如果字段属于基本数据类型,则可以通过params._source.$field_name获取字段的值。例如,在酒店索引中,price字段为double类型,weight字段为integer类型,以下DSL演示了这两个字段的使用:
也可以使用doc['field′]来引用字段,使用doc[′field']来引用字段,使用doc['field′]来引用字段,使用doc[′field'].value引用字段的值。 例如,下面的DSL和上面的DSL的效果是相同的:
2.使用数组字段
当字段类型为数组时,可以直接使用for循环遍历数组中的元素,请看以下DSL:
在访问object类型字段中的值时,除了使用“.”操作符引用该object类型的字段外,对其他字段的访问与访问索引的普通字段类似。例如,酒店评论中的好评数据,可以使用params._source.comment_info['favourable_comment']来引用,以下DSL将评论数作为酒店的分值返回:
4.使用文档评分
在使用match匹配搜索时,ES会对文档进行BM25算法打分,尽管BM25很好地完成了评分/相关性,但有时需要根据业务需求在原有评分的基础上对相关性进行干预。可以用_score直接获取BM25算法的打分数值,请看以下示例代码:
1.4.3.6 向脚本传参
Painless不提供任何网络访问的功能,假设有一部分文档数据存储在Redis中,应该如何传递数据呢?答案就是向Painless传参。假设我们已经通过Java客户端连接Redis获取到了某个特定搜索应该设定的权重值,那么在索引中搜索时,可以通过params关键字定义参数名称并设置其值,在代码中通过params['$para']这种形式进行引用, 以下示例演示了使用Params关键字传递参数的方法:
1.4.3.7 在Script Score中使用函数
在学习Function Score查询时,我们知道在其中可以使用一些ES内置的预定义函数进行打分干预。同样,在Script Score中也可以使用这些函数,本节将介绍一些实际应用中的常用函数。
1.使用saturation函数
saturation,顾名思义,它是计算饱和度的函数,其实相当于计算占比,即saturation(value,k)=value/(k+value),请看如下示例:
return saturation(params._source.weight,10); 复制代码
以上代码中,文档的分值为酒店的好评率。
2.使用sigmoid函数
sigmoid函数在处理数值型数据时将其值的变换值映射到0~1
在使用Function Score时,sigmoid函数可以对某个字段的数据进行相应的处理,以下代码直接返回调用sigmoid函数处理doc_weight字段后的值
3.使用随机函数
在使用ES的搜索结果时,如果希望给不同用户推荐的商品排序是不同的,可以使用随机函数对商品的打分进行控制。Script Score中的randomScore函数可以产生0~1的小数(不包含边界值),其使用方式为randomScore(,) ,其中,seed为随机数种子,fieldName为非必传参数,这时ES将使用Lucene文档的ID值作为该参数的值。
4.使用向量计算函数
ES支持向量数据类型,那么在ES中文档的向量数据是如何生成的呢?在一般情况下,索引中文档的向量是事先用模型计算完成的,如图6.6所示为以酒店搜索为例酒店的向量计算过程。
酒店的向量存储在ES中后,需要给定一个查询向量,对索引中的酒店文档向量按照向量相似度计算的算法进行查询。从7.X版本开始,ES的Script Score中提供了几种向量相似度计算的函数。
cosineSimilarity函数可以计算给定查询向量和文档向量之间的余弦相似度,因为余弦值可能有负数,但是脚本的返回值必须大于或等于0,所以一般对其进行加1处理, 请看下面的示例:
通过结果可以看到,酒店名称中包含“文雅”的有文档005和文档001,查询的酒店向量和文档005的酒店向量是相同的,因此计算其consin值为1,再加上之前的1,文档005的分数为2。文档001和查询向量的consin值为0.4875506,再加上之前的1,文档001的分数为1.4875506。 也就是说,最终的文档排序是和向量相似度正相关的。
dotProduct函数可以计算给定查询向量和文档向量之间的点乘值,因为点乘值也可能是负数,所以在返回值时需要保证该值为正数或者0。 可以使用sigmoid函数进行处理,示例如下:
1norm和l2norm函数可以计算给定查询向量和文档向量之间的距离,其中,l1norm用来计算向量之间的曼哈顿距离,l2norm用来计算向量之间的欧氏距离。 与余弦相似度不同,距离越小代表向量越相近,因此返回值取其倒数即可,请看示
5.使用向量计算函数参照标题1.4.2
1.4.3.8 在Java客户端中使用Script Score
1.5 二次打分
前面介绍的搜索打分都是针对整个匹配结果集的,如果一个搜索匹配了几十万个文档,对这些文档使用Function Score或者Script Score查询进行打分是非常耗时的,整个排序性能大打折扣。针对这种情况,ES提供了Query Rescore功能作为折中方案,它支持只针对返回文档的一部分文档进行打分。
Query Rescore工作的阶段是在原始查询打分之后,它支持对打分后Top-N的文档集合执行第二次查询和打分。通过设置window_size参数可以控制在每个分片上进行二次打分查询的文档数量,在默认情况下window_size的值为10。在默认情况下,文档的最终得分等于原查询和rescore查询的分数之和。 当然,还可以使用参数对这两部分的权重进行控制,后面将结合实例介绍这部分内容。
1.5.1示例
本节继续使用前面章节的索引,现在有一个比较简单的查询:查询价格大于300元的酒店。DSL如下:
从结果中可以看到,索引中有5个文档,匹配的文档数为4。因为使用的是范围查询,所以匹配的文档得分都为1。如果想提升在上述排序中前两个名称中包含“金都”的酒店文档排名,而这两个目标酒店的位置分别为1和3,当前的索引主分片数为1,那么应该设置window_size的值为3,使用二次打分对查询进行扩展的DSL如下:
在上面的DSL中,二次打分使用rescore进行封装,在rescore中可以设置二次打分的查询query和window_size,window_size设置为3意味着对每个分片的前3个文档进行二次打分。执行上述DSL的结果如下:
通过对比rescore前后的结果可以看到,原有的文档002和文档004分别排在第2和第3的位置,并且得分都是1。在rescore的查询中对TOP3且标题含有“金都”的文档进行了加分操作,因此文档002和文档004的得分得到了提升。因为文档004的标题更短,所以它的分数相对更高一些,处在第一个位置,文档002处在第二个位置。
在默认情况下,当存在二次打分时,文档的得分为:原始查询分数+二次打分分数。而用户可以为这两部分的分数设置权重,所以文档得分为:原始查询分数×原始查询权重+二次打分分数×二次打分权重。
1.5.2在Java客户端中使用二次打分