通过Function Score Query优化Elasticsearch搜索结果(综合排序)
在使用 Elasticsearch 进行全文搜索时,搜索结果默认会以文档的相关度进行排序,如果想要改变默认的排序规则,也可以通过sort指定一个或多个排序字段。
但是使用sort排序过于绝对,它会直接忽略掉文档本身的相关度(根本不会去计算)。在很多时候这样做的效果并不好,这时候就需要对多个字段进行综合评估,得出一个最终的排序。
function\_score
在 Elasticsearch 中function_score是用于处理文档分值的 DSL,它会在查询结束后对每一个匹配的文档进行一系列的重打分操作,最后以生成的最终分数进行排序。它提供了几种默认的计算分值的函数:
weight:设置权重
field_value_factor:将某个字段的值进行计算得出分数。
random_score:随机得到 0 到 1 分数
衰减函数:同样以某个字段的值为标准,距离某个值越近得分越高
script_score:通过自定义脚本计算分值
它还有一个属性boost_mode可以指定计算后的分数与原始的_score如何合并,有以下选项:
multiply:将结果乘以_score
sum:将结果加上_score
min:取结果与_score的较小值
max:取结果与_score的较大值
replace:使结果替换掉_score
接下来本文将详细介绍这些函数的用法,以及它们的使用场景。
weight
weight 的用法最为简单,只需要设置一个数字作为权重,文档的分数就会乘以该权重。
他最大的用途应该就是和过滤器一起使用了,因为过滤器只会筛选出符合标准的文档,而不会去详细的计算每个文档的具体得分,所以只要满足条件的文档的分数都是 1,而 weight 可以将其更换为你想要的数值。
field\_value\_factor
field\_value\_factor 的目的是通过文档中某个字段的值计算出一个分数,它有以下属性:
field:指定字段名
factor:对字段值进行预处理,乘以指定的数值(默认为 1)
modifier将字段值进行加工,有以下的几个选项:
none:不处理
log:计算对数
log1p:先将字段值 +1,再计算对数
log2p:先将字段值 +2,再计算对数
ln:计算自然对数
ln1p:先将字段值 +1,再计算自然对数
ln2p:先将字段值 +2,再计算自然对数
square:计算平方
sqrt:计算平方根
reciprocal:计算倒数
举一个简单的例子,假设有一个商品索引,搜索时希望在相关度排序的基础上,销量(sales)更高的商品能排在靠前的位置,那么这条查询 DSL 可以是这样的:
{
"query": {
"function_score": {
"query": {
"match": {
"title": "雨伞"
}
},
"field_value_factor": {
"field": "sales",
"modifier": "log1p",
"factor": 0.1
},
"boost_mode": "sum"
}
}
}
这条查询会将标题中带有雨伞的商品检索出来,然后对这些文档计算一个与库存相关的分数,并与之前相关度的分数相加,对应的公式为:
_score = _score + log (1 + 0.1 * sales)
Java实现:
String index = "wareic";
String type = "product";
SearchRequestBuilder searchRequestBuilder = client.prepareSearch(index).setTypes(type);
searchRequestBuilder.setSearchType(SearchType.DFS_QUERY_THEN_FETCH);
//分页
searchRequestBuilder.setFrom(0).setSize(10);
//explain为true表示根据数据相关度排序,和关键字匹配最高的排在前面
searchRequestBuilder.setExplain(true); BoolQueryBuilder queryBuilder = QueryBuilders.boolQuery();
queryBuilder.must(QueryBuilders.matchQuery("title", "雨伞")); ScoreFunctionBuilder<?> scoreFunctionBuilder = ScoreFunctionBuilders.fieldValueFactorFunction("sales").modifier(Modifier.LN1P).factor(0.1f);
FunctionScoreQueryBuilder query = QueryBuilders.functionScoreQuery(queryBuilder,scoreFunctionBuilder).boostMode(CombineFunction.SUM); searchRequestBuilder.setQuery(query); SearchResponse searchResponse = searchRequestBuilder.execute().get();
System.out.println(searchResponse.toString()); long totalCount = searchResponse.getHits().getTotalHits();
System.out.println("总条数 totalCount:" + totalCount); //遍历结果数据
SearchHit[] hitList = searchResponse.getHits().getHits();
for (SearchHit hit : hitList) {
System.out.println("SearchHit hit string:" + hit.getSourceAsString());
}
random\_score
这个函数的使用相当简单,只需要调用一下就可以返回一个 0 到 1 的分数。
它有一个非常有用的特性是可以通过seed属性设置一个随机种子,该函数保证在随机种子相同时返回值也相同,这点使得它可以轻松地实现对于用户的个性化推荐。
衰减函数
衰减函数(Decay Function)提供了一个更为复杂的公式,它描述了这样一种情况:对于一个字段,它有一个理想的值,而字段实际的值越偏离这个理想值(无论是增大还是减小),就越不符合期望。这个函数可以很好的应用于数值、日期和地理位置类型,由以下属性组成:
原点(origin):该字段最理想的值,这个值可以得到满分(1.0)
偏移量(offset):与原点相差在偏移量之内的值也可以得到满分
衰减规模(scale):当值超出了原点到偏移量这段范围,它所得的分数就开始进行衰减了,衰减规模决定了这个分数衰减速度的快慢
衰减值(decay):该字段可以被接受的值(默认为 0.5),相当于一个分界点,具体的效果与衰减的模式有关
例如我们想要买一样东西:
它的理想价格是 50 元,这个值为原点
但是我们不可能非 50 元就不买,而是会划定一个可接受的价格范围,例如 45-55 元,±5 就为偏移量
当价格超出了可接受的范围,就会让人觉得越来越不值。如果价格是 70 元,评价可能是不太想买,而如果价格是 200 元,评价则会是不可能会买,这就是由衰减规模和衰减值所组成的一条衰减曲线
或者如果我们想租一套房:
它的理想位置是公司附近
如果离公司在 5km 以内,是我们可以接受的范围,在这个范围内我们不去考虑距离,而是更偏向于其他信息
当距离超过 5km 时,我们对这套房的评价就越来越低了,直到超出了某个范围就再也不会考虑了
衰减函数还可以指定三种不同的模式:线性函数(linear)、以 e 为底的指数函数(Exp)和高斯函数(gauss),它们拥有不同的衰减曲线:
将上面提到的租房用 DSL 表示就是:
{
"query": {
"function_score": {
"query": {
"match": {
"title": "公寓"
}
},
"gauss": {
"location": {
"origin": { "lat": 40, "lon": 116 },
"offset": "5km",
"scale": "10km"
}
},
"boost_mode": "sum"
}
}
}
我们希望租房的位置在40, 116
坐标附近,5km
以内是满意的距离,15km
以内是可以接受的距离。
衰减函数Java实现:
String index = "wareic";
String type = "product";
SearchRequestBuilder searchRequestBuilder = client.prepareSearch(index).setTypes(type);
searchRequestBuilder.setSearchType(SearchType.DFS_QUERY_THEN_FETCH);
//分页
searchRequestBuilder.setFrom(0).setSize(50);
//explain为每个匹配到的文档产生一大堆额外内容,设为 true就可以得到更详细的信息;
//输出 explain 结果代价是十分昂贵的,它只能用作调试工具 。千万不要用于生产环境
searchRequestBuilder.setExplain(false); BoolQueryBuilder queryBuilder = QueryBuilders.boolQuery();
queryBuilder.must(QueryBuilders.matchQuery("nicknames.nickname", "菜")); //原点(origin):该字段最理想的值,这个值可以得到满分(1.0)
double origin = 200;
//偏移量(offset):与原点相差在偏移量之内的值也可以得到满分
double offset = 30;
//衰减规模(scale):当值超出了原点到偏移量这段范围,它所得的分数就开始进行衰减了,衰减规模决定了这个分数衰减速度的快慢
double scale = 40;
//衰减值(decay):该字段可以被接受的值(默认为 0.5),相当于一个分界点,具体的效果与衰减的模式有关
double decay = 0.5; //高斯函数
// GaussDecayFunctionBuilder functionBuilder = ScoreFunctionBuilders.gaussDecayFunction("productID", origin, scale, offset, decay);
//以 e 为底的指数函数
ExponentialDecayFunctionBuilder functionBuilder = ScoreFunctionBuilders.exponentialDecayFunction("productID", origin, scale, offset, decay);
//线性函数
// LinearDecayFunctionBuilder functionBuilder = ScoreFunctionBuilders.linearDecayFunction("productID", origin, scale, offset, decay);
FunctionScoreQueryBuilder query = QueryBuilders.functionScoreQuery(queryBuilder,functionBuilder).boostMode(CombineFunction.SUM); searchRequestBuilder.setQuery(query); SearchResponse searchResponse = searchRequestBuilder.execute().get();
System.out.println(searchResponse.toString()); long totalCount = searchResponse.getHits().getTotalHits();
System.out.println("总条数 totalCount:" + totalCount); //遍历结果数据
SearchHit[] hitList = searchResponse.getHits().getHits();
for (SearchHit hit : hitList) {
System.out.println("SearchHit hit string:" + hit.getSourceAsString());
}
script\_score
虽然强大的 field\_value\_factor 和衰减函数已经可以解决大部分问题了,但是也可以看出它们还有一定的局限性:
这两种方式都只能针对一个字段计算分值
这两种方式应用的字段类型有限,field\_value\_factor 一般只用于数字类型,而衰减函数一般只用于数字、位置和时间类型
这时候就需要 script\_score 了,它支持我们自己编写一个脚本运行,在该脚本中我们可以拿到当前文档的所有字段信息,并且只需要将计算的分数作为返回值传回 Elasticsearch 即可。
注:使用脚本需要首先在配置文件中打开相关功能:
script.groovy.sandbox.enabled: true
script.inline: on
script.indexed: on
script.search: on
script.engine.groovy.inline.aggs: on
举一个之前做不到的例子,假如我们有一个位置索引,它有一个分类(category)属性,该属性是字符串枚举类型,例如商场、电影院或者餐厅等。现在由于我们有一个电影相关的活动,所以需要将电影院在搜索列表中的排位相对靠前。
之前的两种方式都无法给字符串打分,但是如果我们自己写脚本的话却很简单,使用 Groovy(Elasticsearch 的默认脚本语言)也就是一行的事:
return doc ['category'].value == '电影院' ? 1.1 : 1.0
接下来只要将这个脚本配置到查询语句中就可以了:
{
"query": {
"function_score": {
"query": {
"match": {
"name": "天安门"
}
},
"script_score": {
"script": "return doc ['category'].value == '电影院' ? 1.1 : 1.0"
}
}
}
}
或是将脚本放在elasticsearch/config/scripts
下,然后在查询语句中引用它:
category-score.groovy:
return doc ['category'].value == '电影院' ? 1.1 : 1.0
{
"query": {
"function_score": {
"query": {
"match": {
"name": "天安门"
}
},
"script_score": {
"script": {
"file": "category-score"
}
}
}
}
}
在script
中还可以通过params
属性向脚本传值,所以为了解除耦合,上面的 DSL 还能接着改写为:
category-score.groovy:
return doc ['category'].value == recommend_category ? 1.1 : 1.0
{
"query": {
"function_score": {
"query": {
"match": {
"name": "天安门"
}
},
"script_score": {
"script": {
"file": "category-score",
"params": {
"recommend_category": "电影院"
}
}
}
}
}
}
这样就可以在不更改大部分查询语句和脚本的基础上动态修改推荐的位置类别了。
同时使用多个函数
上面的例子都只是调用某一个函数并与查询得到的_score进行合并处理,而在实际应用中肯定会出现在多个点上计算分值并合并,虽然脚本也许可以解决这个问题,但是应该没人愿意维护一个复杂的脚本吧。这时候通过多个函数将每个分值都计算出在合并才是更好的选择。
在 function\_score 中可以使用functions属性指定多个函数。它是一个数组,所以原有函数不需要发生改动。同时还可以通过score_mode指定各个函数分值之间的合并处理,值跟最开始提到的boost_mode相同。下面举两个例子介绍一些多个函数混用的场景。
第一个例子是类似于大众点评的餐厅应用。该应用希望向用户推荐一些不错的餐馆,特征是:范围要在当前位置的 5km 以内,有停车位是最重要的,有 Wi-Fi 更好,餐厅的评分(1 分到 5 分)越高越好,并且对不同用户最好展示不同的结果以增加随机性。
那么它的查询语句应该是这样的:
{
"query": {
"function_score": {
"filter": {
"geo_distance": {
"distance": "5km",
"location": {
"lat": $lat,
"lon": $lng
}
}
},
"functions": [
{
"filter": {
"term": {
"features": "wifi"
}
},
"weight": 1
},
{
"filter": {
"term": {
"features": "停车位"
}
},
"weight": 2
},
{
"field_value_factor": {
"field": "score",
"factor": 1.2
}
},
{
"random_score": {
"seed": "$id"
}
}
],
"score_mode": "sum",
"boost_mode": "multiply"
}
}
}
注:其中所有以$开头的都是变量。
这样一个饭馆的最高得分应该是 2 分(有停车位)+ 1 分(有 wifi)+ 6 分(评分 5 分 \* 1.2)+ 1 分(随机评分)。
另一个例子是类似于新浪微博的社交网站。现在要优化搜索功能,使其以文本相关度排序为主,但是越新的微博会排在相对靠前的位置,点赞(忽略相同计算方式的转发和评论)数较高的微博也会排在较前面。如果这篇微博购买了推广并且是创建不到 24 小时(同时满足),它的位置会非常靠前。
{
"query": {
"function_score": {
"query": {
"match": {
"content": "$text"
}
},
"functions": [
{
"gauss": {
"createDate": {
"origin": "$now",
"scale": "6d",
"offset": "1d"
}
}
},
{
"field_value_factor": {
"field": "like_count",
"modifier": "log1p",
"factor": 0.1
}
},
{
"script_score": {
"script": "return doc ['is_recommend'].value && doc ['create_date'] > time ? 1.5 : 1.0",
params: {
"time": $time
}
}
}
],
"boost_mode": "multiply"
}
}
}
它的公式为:
_score * gauss (create_date, $now, "1d", "6d") * log (1 + 0.1 * like_count) * is_recommend ? 1.5 : 1.0
通过Function Score Query优化Elasticsearch搜索结果(综合排序)的更多相关文章
- Spring Boot 整合 Elasticsearch,实现 function score query 权重分查询
摘要: 原创出处 www.bysocket.com 「泥瓦匠BYSocket 」欢迎转载,保留摘要,谢谢! 『 预见未来最好的方式就是亲手创造未来 – <史蒂夫·乔布斯传> 』 运行环境: ...
- ES翻译之Function Score Query
Function Score Query 原文链接 function_score允许你修改通过查询获取文档的分数,很有用处,score function是计算昂贵的,以及在过滤一系列文档上计算分数是高 ...
- 一次 ElasticSearch 搜索优化
一次 ElasticSearch 搜索优化 1. 环境 ES6.3.2,索引名称 user_v1,5个主分片,每个分片一个副本.分片基本都在11GB左右,GET _cat/shards/user 一共 ...
- Elasticsearch搜索类型(query type)详解
关于我,邯郸人. 对这类话题感兴趣?欢迎发送邮件至donlianli@126.com 请支持原创http://www.cnblogs.com/donlianli/p/3857500.html e ...
- Elasticsearch Query DSL备忘(1)(Constant score query和Bool Query)
Query DSL (Domain Specific Language),基于json的查询方式 1.Constant score query,常量分值查询,目的就是返回指定的score,一般都结合f ...
- (转)通过HTTP RESTful API 操作elasticsearch搜索数据
样例数据集 这是编造的JSON格式银行客户账号信息文档,文档schema如下: { “account_number”: 0, “balance”: 16623, “firstname”: “Brads ...
- 【Elasticsearch 搜索之路】(一)什么是 Elasticsearch?
本篇文章对 Elasticsearch 做了基本介绍,在后续将通过专栏的方式持续更新,本系列以 Elasticsearch7 作为主要的讲解版本,欢迎各位大佬指正,共同学习进步! 一般涉及大型数据库的 ...
- Elasticsearch搜索资料汇总
Elasticsearch 简介 Elasticsearch(ES)是一个基于Lucene 构建的开源分布式搜索分析引擎,可以近实时的索引.检索数据.具备高可靠.易使用.社区活跃等特点,在全文检索.日 ...
- ElasticSearch搜索介绍四
ElasticSearch搜索 最基础的搜索: curl -XGET http://localhost:9200/_search 返回的结果为: { "took": 2, &quo ...
随机推荐
- TensorFlow学习之四
Tensorflow一些常用基本概念与函数(1) 摘要:本文主要对tf的一些常用概念与方法进行描述. 1.tensorflow的基本运作 为了快速的熟悉TensorFlow编程,下面从一段简单的代码开 ...
- 关于http以及aphace配置https
我是通过腾讯云配置的ssl. 网站:www.xian029.cn 免费申请,然后通过phpstudy 来配置的. 密码学: 研究密码编码与解码的学科,可以分为编码学和破译学. HTTPS ...
- Node.js 初识2
原文:https://www.cnblogs.com/zzuIvy/p/nodejs_1.html 测试:node.js部署网站 1.创建js2.js var http = require('http ...
- html背景图星际导航图练习
html <body> <div class="box1"> <div></div> ...
- java学习笔记(一):开始第一个java项目
这里使用IntelliJ IDEA 来新建第一个java项目 在新建项目向导,你可以选择你的项目支持的技术,你正在做一个普通的Java项目,只需单击下一步. 下一步,新建一个test的项目. 新建一个 ...
- jq怎么给图片绑定上传文件按钮
html代码 <img src="/img/zhengmian.png" alt="" class="file1"> <i ...
- MySQL优化(三) 表的设计
1.什么样的表才符合3范式(3 NF)? 表的范式,是首先符合1范式,才能满足2范式,进一步才能满足3范式:(现在最高级别是6范式) 第一范式:1NF 是对属性的原子性约束,要求表的属性(列)具有原子 ...
- hugepage优势
hugepage的优势与使用 +2投票 优势 通过使用hugepage分配可以提高性能,因为需要更少的页,因此需要更少Translation Lookaside Buffers (TLB,高速传送 ...
- 微信小程序设置背景铺满全屏
参考方法: 新版本升级取消了默认page的100%的特性 需要在app.wxss文件中加入如下代码: page{ height:100%; }
- python基础之Day6
一.元组 定义:t=(1,2,3,4) 总结:存多个值,值为任意类型 只有读的需求,没有改的需求 有序,不可变(元组里每个值对应的索引内存地址不能变) 在元素个数相同的情况下,元组比列表更节省空间 二 ...