一、前言

上篇介绍了搜索结果高亮的实现方法,本篇主要介绍搜索结果相关性排序优化。


二、相关概念

2.1 排序

默认情况下,返回结果是按照「相关性」进行排序的——最相关的文档排在最前。

2.1.1 相关性排序(默认)

在 ES 中相关性评分 由一个浮点数表示,并在搜索结果中通过「 _score 」参数返回,默认是按照 _score 降序排列。

2.1.2 按照字段值排序

使用「 sort 」参数实现,可指定一个或多个字段。然而使用 sort 排序过于绝对,它会直接忽略文档本身的相关度,因此仅适合在某些特殊场景使用。

注:如果以字符串字段进行排序,除了索引一份用于全文查询的数据,还需要索引一份原始的未经分析器处理(即 not_analyzed )的数据。这就需要使用「 fields 」参数实现同一个字段多种索引方式,这里的「索引」是动词相当于「存储」的概念。

2.2 相关性算法

ES 5.X 版本将相关性算法由之前的「 TF/IDF 」算法改为了更先进的「 BM25 」算法。

2.2.1 TF/IDF 评分算法

ES版本 < 5 的评分算法,即词频/逆向文档频率。

① 词频( Term frequency )

搜索词在文档中出现的频率,频率越高,相关度越高。计算公式如下:

$$tf(t\ \ in\ \ d) = \sqrt{frequency}$$

搜索词「 t 」在文档「 d 」的词频「 tf 」是该词在文档中出现次数的平方根。

② 逆向文档频率( Inverse document frequency )

搜索词在索引(单个分片)所有文档里出现的频率,频率越高,相关度越低。用人话描述就是「物以稀为贵」,计算公式如下:

$$idf(t) = 1 + log \frac{docCount}{docFreq + 1}$$

搜索词「 t 」的逆向文档频率「 idf 」是索引中的文档总数除以所有包含该词的文档数,然后求其对数。

③ 字段长度归一值( Field length norm )

字段的长度,字段越短,相关度越高。计算公式如下:

$$norm(d) = \frac{1}{\sqrt{numTerms}}$$

字段长度归一值「 norm 」是字段中词数平方根的倒数。

注:前面公式中提到的「文档」实际上是指文档里的某个字段

2.2.2 BM25 评分算法

ES版本 >= 5 的评分算法;BM25 的 BM 是缩写自 Best Match, 25 貌似是经过 25 次迭代调整之后得出的算法。它也是基于 TF / IDF 算法进化来的。

对于给定查询语句「Q」,其中包含关键词「$q_{1}$,...$q_{n}$」,那么文档「D」的 BM25 评分计算公式如下:

$$score(D,Q) = \sum_{i=1}^NIDF(q_{i})\ ·\ \frac{f(q_{i},D)\ ·\ (k_{1}+1)}{f(q_{i},D)+k_{1}\ ·\ (1-b+b\ ·\ \frac{|D|}{avgdl})}$$

这个公式看起来很唬人,尤其是那个求和符号,不过分解开来还是比较好理解的。

总体而言,主要还是分三部分,TF - IDF - Document Length

  • IDF 的计算公式调整为如下所示,其中N 为文档总数, $n(q_{i})$ 为包含搜索词 $q_{i}$ 的文档数。

    $$IDF(q_{i}) = 1 + log\frac{N-n(q_{i})+0.5}{n(q_{i})+0.5}$$
  • $f(q_{i},D)$ 为搜索词 $q_{i}$ 在文档 D 中的「 TF 」,| D | 是文档的长度,avgdl 是平均文档长度。

    先不看 IDF 和 Document Length 的部分, 则公式变为 TF * ($k_{1}$ + 1) / (TF + $k_{1}$),

    相比传统的 TF/IDF 而言,BM25 抑制了 TF 对整体评分的影响程度,虽然同样都是增函数,但是 BM25 中,TF 越大,带来的影响无限趋近于 ($k_{1}$ + 1),这里 $k_{1}$ 值通常取 [1.2, 2.0],而传统的 TF/IDF 则会没有临界点的无限增长。
  • 至于文档长度 | D | 的影响,可以看到在命中搜索词的情况下,文档越短,相关性越高,具体影响程度又可以由公式中的 b 来调整,当设值为 0 的时候,就跟将 norms 设置为 false 一样,忽略文档长度的影响。
  • 最后再对所有搜索词的计算结果求和,就是 ES5 中一般查询的得分了。

三、实际案例

3.1 现实需求

要求搜索文章时,搜索词出现在标题时的权重要比出现在内容中高,同时要考虑「引用次数」对最终排序的影响。

3.2 实现方法

3.2.1 调整搜索字段权重

通过调整字段的 boost 参数实现自定义权重,此处将标题的权重调整为内容的两倍。

private SearchQuery getKnowledgeSearchQuery(KnowledgeSearchParam param) {
...省略其余部分...
BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
boolQuery.must(QueryBuilders.termQuery("isDeleted", IsDeletedEnum.NO.getKey()));
boolQuery.should(QueryBuilders.matchQuery(knowledgeTitleFieldName, param.getKeyword()).boost(2.0f));
boolQuery.should(QueryBuilders.matchQuery(knowledgeContentFieldName, param.getKeyword()));
return new NativeSearchQueryBuilder()
.withPageable(pageable)
.withQuery(boolQuery)
.withHighlightFields(knowledgeTitleField, knowledgeContentField)
.build();
}
3.2.2 按引用次数提升权重

这里通过 function score 实现重打分操作。根据上面的需求,我们将使用 field value factor 函数指定「 referenceCount 」字段计算分数并与 _score 相加作为最终评分进行排序。

private SearchQuery getKnowledgeSearchQuery(KnowledgeSearchParam param) {
...省略其余部分...
// 引用次数更多的知识点排在靠前的位置
// 对应的公式为:_score = _score + log (1 + 0.1 * referenceCount)
ScoreFunctionBuilder scoreFunctionBuilder = ScoreFunctionBuilders
.fieldValueFactorFunction("referenceCount")
.modifier(FieldValueFactorFunction.Modifier.LN1P)
.factor(0.1f);
FunctionScoreQueryBuilder functionScoreQuery = QueryBuilders
.functionScoreQuery(boolQuery, scoreFunctionBuilder)
.boostMode(CombineFunction.SUM);
return new NativeSearchQueryBuilder()
.withPageable(pageable)
.withQuery(functionScoreQuery)
.withHighlightFields(knowledgeTitleField, knowledgeContentField)
.build();
}

上述的 function score 是 ES 用于处理文档分值的 DSL(领域专用语言),它预定义了一些计算分值的函数:

① weight

为每个文档应用一个简单的权重提升值:当 weight 为 2 时,最终结果为 2 * _score

② field_value_factor

通过文档中某个字段的值计算出一个分数且使用该值修改 _score,具有以下属性:

属性 描述
field 指定字段名
factor 对字段值进行预处理,乘以指定的数值,默认为 1
modifier 将字段值进行加工,默认为 none
boost_mode 控制函数与 _score 合并的结果,默认为 multiply
③ random_score

为每个用户都使用一个随机评分对结果排序,可以实现对于用户的个性化推荐。

④ 衰减函数

提供一个更复杂的公式,描述了这样一种情况:对于一个字段,它有一个理想值,而字段实际的值越偏离这个理想值就越不符合期望。具有以下属性:

属性 描述
origin(原点) 该字段的理想值,满分 1.0
offset(偏移量) 与原点相差在偏移量之内的值也可以得到满分
scale(衰减规模) 当值超出原点到偏移量这段范围,它所得的分数就开始衰减,衰减规模决定了分数衰减速度的快慢
decay(衰减值) 该字段可以被接受的值,默认为 0.5
⑤ script_score

支持自定义脚本完全控制评分计算

3.2.3 理解评分标准

通过JAVA API 实现相关功能后,输出评分说明可以帮助我们更好的理解评分过程以及后续调整算法参数。

① 首先定义一个打印搜索结果的方法,设置 explain = true 即可输出 explanation 。

public void debugSearchQuery(SearchQuery searchQuery, String indexName) {
SearchRequestBuilder searchRequestBuilder = elasticsearchTemplate.getClient().prepareSearch(indexName).setTypes(indexName);
searchRequestBuilder.setSearchType(SearchType.DFS_QUERY_THEN_FETCH);
searchRequestBuilder.setFrom(0).setSize(10);
searchRequestBuilder.setExplain(true);
searchRequestBuilder.setQuery(searchQuery.getQuery());
SearchResponse searchResponse;
try {
searchResponse = searchRequestBuilder.execute().get();
long totalCount = searchResponse.getHits().getTotalHits();
log.info("总条数 totalCount:" + totalCount);
//遍历结果数据
SearchHit[] hitList = searchResponse.getHits().getHits();
for (SearchHit hit : hitList) {
log.info("SearchHit hit explanation:{}\nsource:{}", hit.getExplanation().toString(), hit.getSourceAsString());
}
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}

② 之后调用接口,其 explanation 结果展示如下:

19.491358 = sum of
19.309036 = sum of:
19.309036 = sum of:
19.309036 = weight(knowledgeTitle.pinyin:test in 181) [PerFieldSimilarity], result of:
19.309036 = score(doc=181,freq=1.0 = termFreq=1.0
), product of:
2.0 = boost
6.2461066 = idf, computed as log(1 + (docCount - docFreq + 0.5) / (docFreq + 0.5)) from:
2.0 = docFreq
1289.0 = docCount
1.5456858 = tfNorm, computed as (freq * (k1 + 1)) / (freq + k1 * (1 - b + b * fieldLength / avgFieldLength)) from:
1.0 = termFreq=1.0
1.2 = parameter k1
0.75 = parameter b
29.193172 = avgFieldLength
4.0 = fieldLength
0.0 = match on required clause, product of:
0.0 = # clause
1.0 = isDeleted:[0 TO 0], product of:
1.0 = boost
1.0 = queryNorm
0.18232156 = min of:
0.18232156 = field value function: ln1p(doc['referenceCount'].value * factor=0.1)
3.4028235E38 = maxBoost

其中 idf = 6.2461066,tfNorm = 1.5456858,boost = 2.0,由于此时只有一个搜索字段,因此 score = idf * tfNorm * boost = 19.309036;与此同时 field value function = 0.18232156;最终得分 sum = 19.309036 + 0.18232156 = 19.491358 。


四、结语

至此一个简单需求的相关性排序优化已经实现完毕,由于业务的关系暂时未涉及其他复杂的场景,所以此篇仅仅作为一个入门介绍。


五、参考博文

从零搭建 ES 搜索服务(六)相关性排序优化的更多相关文章

  1. 从零搭建 ES 搜索服务(二)基础搜索

    一.前言 上篇介绍了 ES 的基本概念及环境搭建,本篇将结合实际需求介绍整个实现过程及核心代码. 二.安装 ES ik 分析器插件 2.1 ik 分析器简介 GitHub 地址:https://git ...

  2. 从零搭建ES搜索服务(一)基本概念及环境搭建

    一.前言 本系列文章最终目标是为了快速搭建一个简易可用的搜索服务.方案并不一定是最优,但实现难度较低. 二.背景 近期公司在重构老系统,需求是要求知识库支持全文检索. 我们知道普通的数据库 like ...

  3. 从零搭建 ES 搜索服务(五)搜索结果高亮

    一.前言 在实际使用中搜索结果中的关键词前端通常会以特殊形式展示,比如标记为红色使人一目了然.我们可以通过 ES 提供的高亮功能实现此效果. 二.代码实现 前文查询是通过一个继承 Elasticsea ...

  4. 从零搭建 ES 搜索服务(三)同义词搜索

    一.前言 上篇介绍了 ES 的基础搜索,能满足我们基本的需求,然而在实际使用中还可能希望搜索「番茄」能将包含「西红柿」的结果也罗列出来,本篇将介绍如何实现同义词之间的搜索. 二.安装 ES 同义词插件 ...

  5. 从零搭建 ES 搜索服务(四)拼音搜索

    一.前言 上篇介绍了 ES 的同义词搜索,使我们的搜索更强大了,然而这还远远不够,在实际使用中还可能希望搜索「fanqie」能将包含「番茄」的结果也罗列出来,这就涉及到拼音搜索了,本篇将介绍如何具体实 ...

  6. 从零搭建一个Redis服务

    前言 自己在搭建redis服务的时候碰到一些问题,好多人只告诉你怎么成功搭建,但是并没有整理过程中遇到的问题,所有楼主就花了点时间来整理下. linux环境安装redis 安装中的碰到的问题和解决办法 ...

  7. 手动从零使用ELK构建一套搜索服务

    前言 这两天需要对接一个新的搜索业务,由于测试机器还没到位,所以就自己创造条件,通过在Windows上安装VM虚拟机,模拟整套环境,从而能快速进入核心业务的开发测试状态中. 系统环境安装配置 虚拟机V ...

  8. Maven 仓库搜索服务和私服搭建

    Maven 仓库搜索服务 使用maven进行日常开发的时候,一个常见问题就是如何寻找需要的依赖,我们可能只知道需要使用类库的项目名称,但是添加maven依赖要求提供确切的maven坐标,这时就可以使用 ...

  9. 搜索服务solr 一二事(1) - solr-5.5 使用自带Jetty或者tomcat 搭建单机版搜索服务器

    solr,什么是solr,就是你要吃的东西“馊了”,不能吃了,out of date~ 嘛...开个玩笑,发音就是‘搜了’,专门用于搜索的一个开源框架,lunce就不说了,不好用,麻烦 来讲讲solr ...

随机推荐

  1. xpath 根据根节点找数据

  2. 【linux】复制文件夹中文件,排除部分文件

    如下 cp `ls|grep -v -E '*json|out'|xargs` /home/data/ 用grep -v  表示排除, -E 表示正则 ls|grep -v -E '*json|out ...

  3. laravel 关联查询

  4. Kali安装问题解决方案

    一.对今天安装Kalilinux  失败的原因做一个简单的总结, 1.安装之前的电脑状况,电脑配置Windows7 64位旗舰版配置,虚拟版本是VMware Workstation Pro14 ver ...

  5. java----java工具类

    System: Runtime: Random: Scanner: Arrays: MessageFormat: Math: 日期: Comparable: cloneable接口: 数字处理: MD ...

  6. OrCAD Capture CIS 16.6 修改原理图的页面大小

    OrCAD Capture CIS 16.6 打开需要修改的原理图. 菜单:Options > Schematic Page Properties... 看图操作...

  7. 计蒜客 X的平方根(二分法)

    设计函数int sqrt(int x),计算 xx 的平方根. 输入格式 输入一个 整数 xx,输出它的平方根.直到碰到文件结束符(EOF)为止. 输出格式 对于每组输入,输出一行一个整数,表示输入整 ...

  8. C#enum使用Attribute求字段名

    用到了一些反射:(自己看吧) public enum UserState { /// <summary> /// 正常 /// </summary> [Remark(" ...

  9. Gradle Build速度加快方法汇总

    Android Studio用起来越来越顺手,但是却发现Build的速度实在不敢恭维,在google和度娘了几把(....)之后,大体就是分配更高的内存,步骤:Setting-->搜索gradl ...

  10. Redis都有哪些数据类型

    string 这是最基本的类型了,就是普通的set和get,做简单的kv缓存 hash 这个是类似map的一种结构,这个一般就是可以将结构化的数据,比如一个对象(前提是这个对象没嵌套其他的对象)给缓存 ...