从零搭建 ES 搜索服务(六)相关性排序优化

摘要:
2、 相关概念2.1排序默认情况下,返回的结果按“相关性”排序-最相关的文档排名第一。

一、前言

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


二、相关概念

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 + logfrac{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:{}
source:{}", 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 搜索服务(六)相关性排序优化》仅用于学习参考。如对内容有疑问,请及时联系本站处理。

上篇liquibase之快速入门SAP HTTP调用其他系统接口下篇

宿迁高防,2C2G15M,22元/月;香港BGP,2C5G5M,25元/月 雨云优惠码:MjYwNzM=

相关文章

[心得体会]mysql复习

1. 进入企业需要注意的事情(1) 查看测试服和本地的mysql版本是否一致(2) 确认sql_mode是否和线上版本一致 showVARIABLESLIKE'sql_mode'; (3) mysql sql_mode 常用设置详解: ONLY_FULL_GROUP_BY: 对于GROUP BY聚合操作,如果在SELECT中的列,没有在GROUP BY...

MySQL查询性能优化

1.为什么查询速度为变慢 在尝试编写快速的查询之前,需要清楚一点,真正重要是响应时间。如果把查询看作是一个任务,那么他由一系列子任务组成,每个子任务都会消耗一定的时间。如果要优化查询,实际上要优化其子任务,要么消除其中一些子任务,要么减少子任务的执行的次数,要么让子任务运行得更快。 MySQL在执行查询的时候有哪些子任务。哪些子任务运行的速度很慢,这里很难...

SQL性能调优

部分转自:http://www.cnblogs.com/luckybird/archive/2012/06/11/2544753.html 及http://www.cnblogs.com/kissdodog/p/3160560.html 着色部分为实际解决问题的过程 最常见的索引问题查找: 1、检查实际执行计划,使用图形化或者在执行语句前增加  set s...

某公司基于FineBI数据决策平台的试运行分析报告

一、数据平台的软硬件环境 二、组织机构和权限体系 组织机构:平台中已集成一套组织机构,可以建立部门、人员。也可以与现有系统的组织机构集成,将组织机构导入到平台中。 功能权限:通过配置功能点URL的方式实现各个用户相应的BI访问权限。用户第一次访问受保护的资源(某个功能点)时,会发出访问请求,服务器接收到请求后会验证用户权限,如果没有通过验证则返回登录页面...

数据库性能优化

MySQL基础表和数据 数据库访问优化法则 了解计算机系统的硬件基本性能指标,可以快速找到SQL的性能瓶颈点,下面是当前主流计算机性能指标数据。 从图上可以看到基本上每种设备都有两个指标: 延时(响应时间):表示硬件的突发处理能力; 带宽(吞吐量):代表硬件持续处理能力。 从上图可以看出,计算机系统硬件性能从高到代依次为: CPU——Cache(L1-L...

LeetCode 1366. 通过投票对团队排名 哈希排序

地址 https://leetcode-cn.com/problems/rank-teams-by-votes/ 现在有一个特殊的排名系统,依据参赛团队在投票人心中的次序进行排名,每个投票者都需要按从高到低的顺序对参与排名的所有团队进行排位。 排名规则如下: 参赛团队的排名次序依照其所获「排位第一」的票的多少决定。如果存在多个团队并列的情况,将继续考虑...