业务搜索通常都需要对召回数据进行排序,返回更有价值的信息给用户。
ES默认按文本相关性排序,通常我们会通过嵌入脚本的形式来修改ES的打分机制,从而影响排序结果。
ES在5.x+版本后发明了一种语法类似javascript/groovy的专用脚本语言painless,我们需要写一个painless脚本,脚本中可以获取文本相关性得分,也可以获取文档的各个字段内容,也可以获取查询请求中传入的临时参数,综合来计算一个新的分数替代默认的文本相关性得分。
function_score
要实现自定义打分,需要使用funcion_score语法,它将query包装在内部,从而实现先召回 -> 再文本相关性评分 -> 最后自定义打分的功能。
这一块内容我建议大家详细的掌握一下各个参数的作用。
painless
这个脚本语言和javascript很像,从名字也可以看出它的目标就是降低学习成本,让你上手就可以做事情。
官方文档链接:https://www.elastic.co/guide/en/elasticsearch/painless/6.1/painless-getting-started.html。
这个脚本语法其实不用投入很多精力学习,它除了一些基础的if else之外,还提供了一些数据结构。
对于我们开发来说,最重要的是知道如何获取文本相关性,获取文档各个字段的值,获取请求传入的临时参数。
脚本在ES不同的请求中,获取上下文变量的方式不同,需要参考官方链接:https://www.elastic.co/guide/en/elasticsearch/reference/master/modules-scripting-fields.html。
painless调试
因为painless是专用ES的嵌入式脚本,没有独立的runtime环境,所以必须通过ES请求来执行它,这样脚本才能得到执行,同时也能访问到请求的上下文变量。
为了调试painless,我们需要学会一个Debug命令,参考官方链接:https://www.elastic.co/guide/en/elasticsearch/painless/6.1/painless-debugging.html。
一个初步的脚本结构是这样的,它使用内联的painless代码来做一些初步的调试:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
'query' => [ 'function_score' => [ 'query' => [ 'bool' => [ 'must' => [ [ // 中文分词或者拼音分词匹配皆可, 综合打分 'multi_match' => [ 'query' => '数据线', 'fields' => [ 'article_title', 'article_title.pinyin', 'article_content' ], 'type' => 'most_fields', ] ], ], // 是/否的判定, 写在filter context中,比如term过滤 'filter' => [ [ 'term' => ['article_is_anonymous' => false] ], [ 'nested' => [ 'path' => 'article_category', 'query' => [ 'term' => ['article_category.cate_name' => '手机'], ], ], ] ] ], ], 'functions' => [ [ 'script_score' => [ 'script' => [ "lang" => "painless", 'params' => [ 'cur_date' => intval(microtime(true) * 1000), ], 'source' => 'Debug.explain(params._source.article_title + params.cur_date + doc["article_is_anonymous"])' ], ] ], ], "boost_mode" => "multiply", ] ] |
主要关注4点:
- 最外层的query context内直接嵌套一层function_score,而function_score内重新创建query context。
- 与内层query context平级放置functions自定义打分函数,我的最终相关性希望是:文本相关性 * 脚本相关性,所以boost_mode=multiply;大家可以根据自己的需求和调试结果,换不同的boost_mode去尝试。
- 出于性能考虑,优先对非text字段使用doc获取内容(字段列存储,效率最高),其次如果字段配置了store=true则使用_fields获取内容,最后才考虑使用params._source获取整个文档并从中提取某个字段。
- 上述source直接inline了一段painless代码,ES第一次收到就会编译代码并缓存,这种适合比较短小的脚本,传输没有太大的代价。
stored painless
如果我们的painelss打分脚本逻辑比较重,那么可能不适合每次查询时内联在请求中,所以需要先把脚本提交到ES保存,随后请求中只需要携带脚本的ID即可,参考官方文档:https://www.elastic.co/guide/en/elasticsearch/reference/master/modules-scripting-using.html#modules-scripting-stored-scripts。
首先,把脚本上传到ES,需要给它指定一个ID唯一标识:
1 2 3 4 5 6 7 8 9 |
$resp = $client->putScript([ 'id' => 'my_scoring', 'body' => [ 'script' => [ 'lang' => 'painless', 'source' => 'Debug.explain(params._source.article_title + params.cur_date + doc["article_is_anonymous"])', ] ] ]); |
ES6.0非常人性化,因为以前这种打分脚本都是需要运维去部署到每个ES节点的目录下的,而现在完全不需要了。
查询的时候,不再需要携带脚本代码,而是指定一个ID即可:
1 2 3 4 5 6 7 8 9 10 11 12 |
'functions' => [ [ 'script_score' => [ 'script' => [ "id" => "my_scoring", 'params' => [ 'cur_date' => intval(microtime(true) * 1000), ], ], ] ], ], |
实现打分逻辑
这一块我也是刚开始摸索,给大家举个例子作为敲门砖吧:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
$resp = $client->putScript([ 'id' => 'my_scoring', 'body' => [ 'script' => [ 'lang' => 'painless', 'source' => <<< EOF // 热度作为一个排序因素 def hot = doc["article_like_count"].value * doc["article_comment_count"].value; if (hot == 0) { hot = 1; } def hot_score = Math.log1p(1); return hot_score; EOF ] ] ]); |
这个打分脚本的逻辑:文章的点赞和评论数相乘,然后通过log1p归一化成一个比较小的值,作为打分返回。
下面是查询时的函数配置:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
'functions' => [ [ 'script_score' => [ // 脚本打分 'script' => [ "id" => "my_scoring", 'params' => [ 'cur_date' => intval(microtime(true) * 1000), ], ], ] ], [ 'gauss' => [ // 时间衰减, 1天内不衰减, 2天的衰减一半 'article_pub_date' => [ 'origin' => intval(microtime(true) * 1000), 'offset' => '1d', 'scale' => '2d', 'decay' => 0.5, ] ] ], ], 'boost_mode' => 'multiply', 'score_mode' => 'multiply', |
一共有2个算分函数。
第一个是自定义painless脚本函数,返回的是基于热度的归一化分值。
第二个是内置衰减函数,基于时间进行分数衰减,1天内的不降分(也就是1分),2天之前的降到0.5分。
2个函数的得分指定了score_mode=multiply,所以两个分数进行直接相乘作为函数的总得分。
最后通过boost_mode=multiply指定,文本相关性得分_score将和上述函数总得分再次相乘,得到文档的最终相关性得分。
目前我还没有基于实际大规模的文档进行调研,脚本打分应该怎么归一化,用权重加和还是用乘法,我其实也分不清区别。
有相关经验的同学欢迎留言指教。
如果文章帮助您解决了工作难题,您可以帮我点击屏幕上的任意广告,或者赞助少量费用来支持我的持续创作,谢谢~

您好 可以交流下 es 召回的问题吗
可以加Q:120848369交流一下~