一、前言

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


二、相关概念

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. Confluence 6 管理协同编辑 - 代理和 SSL 的考虑

    对于你如何连接  Synchrony 是与你的环境有关的.我们知道绝大部分的 Confluence 站点是运行在反向代理后面的,同时还使用了 SSL.这里是帮助你在你环境中识别正确的配置的一些信息和一 ...

  2. ES6笔记

    /** * Created by Administrator on 2017/4/13. */ /*---------------------Es6编码规范---------------------* ...

  3. LeetCode(113):路径总和 II

    Medium! 题目描述: 给定一个二叉树和一个目标和,找到所有从根节点到叶子节点路径总和等于给定目标和的路径. 说明: 叶子节点是指没有子节点的节点. 示例:给定如下二叉树,以及目标和 sum = ...

  4. 从认识面向对象到构造函数的标准写法(构造函数的继承、多态、ECMA6中新代替语法class) - 下

    笔记一个包含:认识面向对象.构造函数的封装.继承.多态.ECMA6中新代替语法class 下:包括构造函数的继承.多态.ECMA6中新代替语法class 构造函数的继承 从父一级延续下来的属性和功能( ...

  5. SpringMVC + MyBatis + Mysql + Redis(作为二级缓存) 配置

    2016年03月03日 10:37:47 标签: mysql / redis / mybatis / spring mvc / spring 33805 项目环境: 在SpringMVC + MyBa ...

  6. Appium 九宫格 手势解锁

    分析九宫格定位 整个九宫格是一个 view   self.driver.find_element_by_id("com.elc:id/gesturepwd_create_lockview&q ...

  7. fg、bg、jobs、&、nohup、ctrl+z、ctrl+c 命令

    fg.bg.jobs.&.nohup.ctrl+z.ctrl+c 命令 一.& 加在一个命令的最后,可以把这个命令放到后台执行,如 watch -n 10 sh test.sh &am ...

  8. Git基础(一) 创建项目仓库

    一.取得Git项目仓库的两种方法: 在现有目录中初始化仓库 git init 如果是在一个已经存在文件的文件夹(而不是空文件夹)中初始化Git仓库来进行版本控制的话,应该跟踪这些文件并提交,通过git ...

  9. How to trigger an Animation when TextBlock’s Text is changed during a DataBinding

    原文:http://michaelscherf.wordpress.com/2009/02/23/how-to-trigger-an-animation-when-textblocks-text-is ...

  10. springboot学习(一):创建项目

    package com.glory.demo.Controller; import org.springframework.stereotype.Controller; import org.spri ...