一、场景说明
比如我们在CSDN中根据输入的关键词搜索博客文章,需要先根据关键词的相似度匹配排序,然后根据博客热度进行二次排序,保证热度比较高的博客文章优先被搜索到,提高用户的搜索体验。
那么,如何在ES中对检索结果进行二次排序呢?
二、误区说明
索引blog,有三个字段,博客标题title,博客内容content,博客的访问量access_num。
PUT /blog { "mappings": { "properties": { "content": { "type": "text", "analyzer": "ik_max_word", "search_analyzer": "ik_smart" }, "title": { "type": "text", "analyzer": "ik_max_word", "search_analyzer": "ik_smart" }, "access_num": { "type": "integer" } } } }
检索关键词:java入门
要求:查询结果先根据匹配评分排序,再根据访问量排序。
SQL查询实现:
POST /_sql?format=txt { "query": "SELECT title FROM blog where MATCH(title, 'java入门') order by SCORE() DESC,access_num DESC" }
一般情况下,我们的实现思路都是按照上面的sql进行二次排序。
问题说明:
假如现在有100万条记录能匹配上java入门关键字,先对这100万匹配的数据根据评分SCORE排序,然后再根据访问量排序,会出现如下问题
问题一:
一般全文检索时,只需要返回前面几千条记录即可,一般用户只关注匹配度最高的结果。
问题二:
通过order by SCORE() DESC,access_num DESC这样进行二次排序,那些评分较低、但是访问量较高的文章,会被排在后面,用户体验非常差。
问题三:
ES默认的match匹配打分规则,只是基于检索词的相似度词频打分,没有关联索引中的其他字段,不能实现基于title的相似度和访问量access_num组合评分。
三、优化方案
简单的一个优化思路是,先从查询结果中返回评分最高的100条记录,这100条记录再根据访问量倒序排序。
这样即可以保证评分最高的优先返回,也能保证访问量更高的被优先访问。
实现的SQL如下:
SELECT * FROM ( SELECT title, access_num FROM blog WHERE MATCH ( title, 'java入门' ) ORDER BY SCORE () DESC LIMIT 100 ) ORDER BY access_num DESC
存在的问题:
1、ES中不支持这种子查询,只能先查询出前100条记录,然后在程序中进行二次排序。
2、这种文章的相关度和访问量的二次排序太简单粗暴,可能文章名完全匹配的文章由于访问量比较低而不能优先被访问到,用户体验度始终不好。
补充:
通过再次评分机制rescore,可以实现对指定数据的查询结果进行二次排序。
四、终极优化
针对这种情况,更好的方式是给特定索引的全文检索查询定制打分规则,关联更多的字段信息去打分,这样就可以直接根据评分倒序排序获取前100条记录即可。
function_score 查询 是用来控制评分过程的终极武器,它允许为每个与主查询匹配的文档应用一个函数,以达到改变甚至完全替换原始查询评分 _score 的目的。
GET /blog/_search { "query": { "function_score": { "query": { "match": { "title": "java入门" } }, "functions": [ { "script_score": { "script": { "params": { "access_num_ratio": 2.5 }, "lang": "painless", "source": "doc['access_num'].value * params.access_num_ratio " } } } ] } } }
总结
本文主要是通过对ES中如何实现对查询结果的二次排序问题的思考,延展说明了ES中通过设置索引中的多个字段的权重来联合打分,从而优化用户的检索体验。