使用painless脚本为文档自定义打分是很常见的场景,对新人来说也是最容易造成性能问题的地方。本文中使用两个例子简单谈一下脚本性能优化。
目标
ES本身是基于倒排等数据结构实现的查询,因此在做类似Term、Match等可以利用底层数据结构的场景进行查询时,性能是很好的。
脚本和term等查询不一样,无法利用现有的各种数据结构,可以简单理解成循环:
docs = getDocs(xxx); // 获取满足条件的文档列表
for(Doc doc : docs) {
score = getScoreByScript(doc);
}
因此脚本的性能取决于两个地方:脚本的复杂度和满足条件的文档数。
例子1
我们有个场景是查询指定坐标指定范围内的POI列表,例如5公里内的景点列表。
由于我们的距离公式和ES默认的都不一致,如下:
/**
* 计算距离,返回单位:米
*/
public static Double getDistance(Double lat1, Double lng1, Double lat2, Double lng2) {
double diffLon = Math.abs(lng1 - lng2);
if (diffLon > 180)
diffLon -= 360;
return Math.sqrt(Math.pow(diffLon, 2) + Math.pow(lat1 - lat2, 2)) * 110.0 * 1000;
}
所以该同学把这段Java代码转成了Painless,在sort里使用这个该方法计算出距离。上线以后发现ES有了很多慢查询,对应的服务也95线、99线也比较高。
原因是其他脚本没有有效地缩小数据量,导致有几百万的数据需要使用该脚本做距离计算,给ES的CPU造成很大压力,查询性能也比较差。
该例子优化起来很简单,即使用ES自带的distance做较大范围的限制,例如需要5公里的数据,可以用ES的plain距离做限制,再加上之前的自定义脚本逻辑。由于ES的plain距离计算性能好很多,因此经过该过滤以后,自定义脚本的文档量少了很多,因此整体性能有了很大提升。
例子2
有个场景是对文章进行搜索,如果文章关联的城市是指定的几个城市,则给额外的加分。例如:
{
"query": {xxx},
"sort": [
{
"_script": {
"script": {
"source": "def score = 0;def cityIds = doc['cityIds']; def paramCityIds = params.cityIds; for (int i=0; i<cityIds.size(); i++){if (paramCityIds.contains(cityIds[i])){score += 100;}} return score;",
"lang": "painless",
"params": {
"cityIds": [2,1,3]
}
},
"type": "number",
"order": "desc"
}
}
]
}
问题和例子1一样,该功能的性能比较差。虽然脚本简单,但是满足的文档量比较大,带来的计算量也比较多,因此性能上不去。
这是一个比较常见的场景,问题的根源还是对ES的机制不够了解,优化起来也很简单,想办法利用到倒排就可以了。
ES里有个专门针对改场景的查询:constant_score,因此以上查询可以修改为:
{
"query": {
"should": [
{
"constant_score": {
"filter": {
"term": {
"cityIds": 2
}
},
"boost": 5
}
},
{
"constant_score": {
"filter": {
"term": {
"cityIds": 1
}
},
"boost": 5
}
},
{
"constant_score": {
"filter": {
"term": {
"cityIds": 3
}
},
"boost": 5
}
}
]
},
"sort": [
{
"_score": "desc"
]
}
性能即可得到极大改善。