一、需求背景:
虽然上一章讲到了完全匹配,但是有些时候查询结果还是很多,关键字在结果中的位置是杂乱的,这样搜索还是不太容易快速定位到自己想要的结果,这一章就是讲搜索结果的相关度排序。
类似如电商搜索:
排序优先级为:
- 关键词在结果中出现的位置越靠前,排序越靠前;
- 关键词在结果中的字符占比越高,排序越靠前。
再举个明显的例子,如下搜索“高”,先看“高”在左边的越靠前,然后看“高”字的文本长度占比越高越靠前:
为了控制搜索结果的相关度,elasticsearch提供了多种方式,通过脚本实现自定义评分逻辑是终极方式。脚本返回一个评分值,该值再与原_score再进行加法等运算。脚本编写很简单,我们跟随一个例子(基于6.0版本)来看看如何通过脚本实现自定义排序。
二、磨刀霍霍:
搭建本地开发环境(此处省略一万字)。
mapping的定义:
{
"es-type": {
"properties": {
"line_id": {
"type": "long"
},
"line_name": {
"type": "text",
"analyzer": "ngram_analyzer",
"fields": {
"keyword": {
"type": "keyword"
}
}
}
}
}
}
line_name的text字段用于全文本搜索。keyword字段没有分词,用于把搜索结果作为参数传入脚本函数,实现自定义排序逻辑。
写入测试数据:
执行搜索关键字“ip”:
- unction_score查询:是用来控制评分过程的终极武器,它允许为每个与主查询匹配的文档应用一个函数,以达到改变甚至完全替换原始查询评分 _score 的目的。
- script_score:用自定义脚本完全控制评分计算,实现所需逻辑。
- lang:指定脚本语言。
- params:指定作为变量传递到脚本中的任何命名参数。
返回结果:
根据排序规则一(关键词在结果中出现的位置越靠前,排序越靠前),"ip流量数据"、"iphone"和"iphone 6s"排在"昨日iphone价格数据"。根据排序规则二(关键词在结果中的字符占比越高,排序越靠前),"ip流量数据"和"iphone"的字符长度是一样的,所以评分一样,而"iphone 6s"字符长度比"iphone"长,“ip”字符占比低,排在后面。
elasticsearch支持多种脚本语言,经历各版本演变后,从5.0开始实现了自己专用的语言:Painless。Groovy已弃用,这次示例使用Painless实现,Painless是内置支持的。下面详细看下脚本代码:
String result = doc['line_name.keyword'].value.toLowerCase();
String keyText = params.keyword.toLowerCase();
double MAX_VALUE = 1000;
double position_score = 0;
double init = 100;
double weight = 0.9;
int maxIndex = 0;
for (int x = 0; x < keyText.length(); x++) {
char ch = keyText.charAt(x);
int pos = result.indexOf(ch, maxIndex);
if (pos >= 0) {
maxIndex = pos;
double score = init * (weight);
for (int i = 0; i < pos; i++) {
score = score * (1 - weight);
}
position_score = position_score + score;
}
}
double similarity_score = 0;
double similarity = Math.abs(1.0 * result.length() - 1.0 * keyText.length());
similarity_score = 10.0 d - similarity;
if (similarity_score < 0) {
similarity_score = 0;
}
double total = position_score * MAX_VALUE + similarity_score * 0.1;
return total;
获取文档值:doc['line_name.keyword']
传入排序的关键字:params.keyword
实现比较简单,根据位置和字符占比分别计算评分,将结果乘不同权重再相加。