一般情况下,类似关系型数据库中影响返回结果的方式,在elasticsearch中也存在,包括sort、size等。但是这都是简单粗暴的对返回结果进行直接处理,很大的可能会影响返回文档的相关度。比如,通过sort操作对搜索结果按时间排序,这时排在前面的文档很可能相关度非常小,而相关度大的文档则因为时间排序被放在了下面。这显然不是我们想要的结果。elasticsearch提供了方法,允许我们用除了搜索之外的其他因素影响返回文档的顺序,同时兼顾文档的相关度。
简单粗暴地评分
首先先说一下elasticsearch的搜索评分逻辑。
查询的权重基于三个因素:词频、逆向文档频率和字段长度归一值。
- 词频:查询词在该文档中出现的频率。频率越高,权重越高。
- 逆向文档频率:查询词在所有文档中出现的频率。频率越高,权重越低。可以降低日常使用的高频率词的权重。
- 字段长度归一值:查询字段的长度。字段长度越长,查询词权重越高,反之越低。
不同词对搜索结果的影响基本取决于以上三个因素,这里不列出详细的计算公式。
如何影响文档评分
首先,影响文档评分的操作推荐是查询的时候进行,这样灵活性更好。这里不介绍简单的评分提升或者降低,直接介绍elasticsearch中控制文档评分的终极武器:function_scor。
function_score
function_score是query结构的一个子集,它对每一个符合查询的文档应用一个或一组函数,达到影响甚至替换原始查询评分的目的。这个操作可以很方便的实现复杂的查询逻辑。
Elasticsearch 预定义了一些函数:
- weight:为每个文档应用一个简单而不被规范化的权重提升值:当 weight 为 2 时,最终结果为 2 * _score 。
- field_value_factor:使用这个值来修改 _score ,如将 popularity 或 votes (受欢迎或赞)作为考虑因素。
- random_score:为每个用户都使用一个不同的随机评分对结果排序,但对某一具体用户来说,看到的顺序始终是一致的。
- 衰减函数 —— linear 、 exp 、 gauss:将浮动值结合到评分 _score 中,例如结合 publish_date 获得最近发布的文档,结合 geo_location 获得更接近某个具体经纬度(lat/lon)地点的文档,结合 price 获得更接近某个特定价格的文档。
- script_score:如果需求超出以上范围时,用自定义脚本可以完全控制评分计算,实现所需逻辑。
以上函数开箱即用,一般情况下,elasticsearch提供的函数就可以满足需求,如果有特殊要求,也可以使用最后一个script_score自己写控制评分脚本。但是script_score对性能有较大影响,能不用就不用。
下面简单说明几个需求,来看看elasticsearch是如何通过function_score实现的。
点击数影响评分
如果想要在原来搜索的基础上,加入点击数的影响,即将点击数高的文档放到搜索结果靠上的位置,但是搜索的评分仍然是主要的依据。
PUT /blogposts/post/1
{
"title": "About popularity",
"content": "In this post we will talk about...",
"votes": 6
}
这是一篇文章的文档,其中保存了点击数。这里可以通过field_value_factor实现相关需求。
GET /blogposts/post/_search
{
"query": {
"function_score": {
"query": {
"multi_match": {
"query": "popularity",
"fields": [ "title", "content" ]
}
},
"field_value_factor": {
"field": "votes"
}
}
}
}
function_score嵌入了一个query查询中,然后内部又设定了一个query,内部的query查询是主查询。在function_score内部还有一个field_value_factor函数,这个函数会对符合每一个主查询的文档使用。每个文档的最终评分都会进行如下的计算:new_score = old_score * number_of_votes
默认的field_value_factor计算是线性的,votes
的原始值直接用来计算,这通常不会产生好的结果。一般情况下,通过对数计算取代现行计算,可以取得更平滑的结果。
GET /blogposts/post/_search
{
"query": {
"function_score": {
"query": {
"multi_match": {
"query": "popularity",
"fields": [ "title", "content" ]
}
},
"field_value_factor": {
"field": "votes",
"modifier": "log1p"
}
}
}
}
将field_value_factor设为对数计算,计算公式:new_score = old_score * log(1 + number_of_votes)
。
field_value_factor提供了众多参数,设置相关计算的各种参数,这里不再一一列举。如果想要细致的调整搜索结果的话,参考官方文档进行。
对查询结果进行随机评分
作为网站的所有者,总会希望让广告有更高的展现率。在当前查询下,有相同评分 _score 的文档会每次都以相同次序出现,为了提高展现率,在此引入一些随机性可能会是个好主意,这能保证有相同评分的文档都能有均等相似的展现机率。
我们想让每个用户看到不同的随机次序,但也同时希望如果是同一用户翻页浏览时,结果的相对次序能始终保持一致。这种行为被称为一致随机。
random_score
函数会输出一个 0 到 1 之间的数,当种子 seed
值相同时,生成的随机结果是一致的。例如:
GET /blogposts/post/_search
{
"query": {
"function_score": {
"query": {
"multi_match": {
"query": "popularity",
"fields": [ "title", "content" ]
}
},
"random_score": {
"seed": "userID"
}
}
}
}
使用每个用户的id作为seed
传入,可以使每个用户的随机保持一致,同时在不同用户之间保持不同的随机性。
时间、空间上的“越近越好”
一般来说,搜索要求具有时间相关性。也就是说,用户只想看到时间较近的文档,而时间较远的文档,就算相关度较高,用户也不想看到。空间上也有相关的性质,比如以某个点为中心,周围一定距离以内的文档排在返回结果的前面,而超过一定距离的文档就算相关度较高也排在后面。elasticsearch提供了衰减函数,可以对文档的相关性按某个维度进行衰减。
这里不能直接使用sort。因为如果直接使用sort,会让相关度较低的文档排在前面,维度近了但是相关度很差,达不到相关度较高、同时某个维度较 近 的需求。
elasticsearch提供了三个衰减函数,分别是linear、exp和gauss(线性、指数和高斯函数),它们可以操作数值、时间以及经纬度地理坐标点这样的字段(一般衰减也没有用字符串做衰减的)。所有三个函数都能接受以下参数:
- origin:中心点 或字段可能的最佳值,落在原点 origin 上的文档评分 _score 为满分 1.0 。
- scale:衰减率,即一个文档从原点 origin 下落时,评分 _score 改变的速度。(例如,每 £10 欧元或每 100 米)。
- decay:从原点 origin 衰减到 scale 所得的评分 _score ,默认值为 0.5 。
- offset:以原点 origin 为中心点,为其设置一个非零的偏移量 offset 覆盖一个范围,而不只是单个原点。在范围
-offset <= origin <= +offset
内的所有评分 _score 都是 1.0 。
衰减曲线
比如,想要搜索某条博客,同时发表时间近的排在前面,引入基于时间的衰减函数。
GET /blogposts/post/_search
{
"query": {
"function_score": {
"query": {
"multi_match": {
"query": "popularity",
"fields": [ "title", "content" ]
}
},
"gauss" :{
"timestamp": {
"origin": "now timestamp",
"offset": "5d",
"scale": "10d"
}
}
}
}
}
按时间和地理空间做衰减,offset 和 scale 必须加上单位。
这里我们确定以当前的时间为衰减函数的中心, 5 天以内的文档,相关度不做处理;5 天到 15 天,衰减系数逐渐降低到0.5;15 天之外,系数继降低。
比如,想将地理空间和价格的影响引入搜索,可以这样实现:
GET /_search
{
"query": {
"function_score": {
"functions": [
{
"gauss": {
"location": {
"origin": { "lat": 51.5, "lon": 0.12 },
"offset": "2km",
"scale": "3km"
}
}
},
{
"gauss": {
"price": {
"origin": "50",
"offset": "50",
"scale": "20"
}
},
"weight": 2
}
]
}
}
}
这里地理空间类型,在衰减函数的参数里要加上单位,而普通的数值类型不用加单位。
通过衰减函数,可以在相关度为主的前提下,引入时间、空间、数值等其他因素,从而影响搜索的返回结果。比起简单粗暴的sort,衰减函数可以保证相关度高的依然排在靠前的位置。
终极武器script_score
elasticsearch支持用户自己写groovy脚本,来自定义复杂的评分影响逻辑。因为日常中不太实用,同时脚本对性能影响较大,所以这里不做介绍。
网友评论