一、前言

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


二、相关概念

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. STL 容器区别:vector、list、deque、set、map的底层实现

    https://blog.csdn.net/shawjan/article/details/45424405

  2. Java之动手动脑(三)

    日期:2018.10.12 星期五 博客期:017 这次留了两个动手动脑作业!我需要一个一个来说!先说第一个吧! Part 1 :随机生成1000个随机数 代码: //以下为 RandomMaker. ...

  3. ipone mac真机调试

    safiri 识别不了iPhone 真机  需要在iPhone上 做设置  safri-> 高级 ->web检查器  进行设置,然后重新启动  safri即可...

  4. linux文件与目录管理笔记

    ### Linux文件与目录管理 ---------- 绝对路径: / 相对路径:不以/开头的 当前目录 . 上一个工作目录 - 用户主目录 ~ root账户的主目录是/root 其他用户是/home ...

  5. Dinner

    问题 : Dinner 时间限制: 1 Sec  内存限制: 32 MB 题目描述 Little A is one member of ACM team. He had just won the go ...

  6. Nginx详解十七:Nginx深度学习篇之动静分离

    动静分离:通过中间件将动态请求和静态请求分离 作用:分离资源,减少不必要的请求消耗,减少请求延时 动静分离还有个好处就是,当动态请求的后端服务出问题了,只会影响动态的部分,静态资源不影响,照样加载 如 ...

  7. logical_backup: expdp/impdp

    Table of Contents 1. 注意事项 2. 前期准备 3. 常用参数及示例 4. 常用语句示例 5. 交互式命令 6. 技巧 6.1. 不生成文件直接导入目标数据库 6.2. 通过she ...

  8. 将labelme 生成的.json文件进行可视化的代码+label.png 对比度处理的matlab代码

    labelme_to_dataset 指令的代码实现: show.py文件 #!E:\Anaconda3\python.exe import argparse import json import o ...

  9. 通过自定义比较器排序(C#版)

    一.方法概述 自定义比较器需要实现接口IComparer<T> 二.示例过程 1.新建一个Product产品类 /// <summary> /// 产品类 /// </s ...

  10. 改变input type="file" 文字、样式等

    <div class="tac"> <input type="file" id="browsefile" class=&q ...