MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

重排序:定制ElasticSearch搜索结果顺序

2022-08-086.5k 阅读

重排序概述

在 ElasticSearch 的搜索场景中,默认情况下,搜索结果是按照相关性得分(relevance score)进行排序的。相关性得分是 ElasticSearch 根据文档与查询条件的匹配程度计算得出的,它综合考虑了诸如词频(term frequency)、逆文档频率(inverse document frequency)等多种因素。然而,在许多实际应用场景下,仅依据默认的相关性得分排序并不能满足业务需求。例如,在电商搜索中,除了考虑商品与搜索关键词的相关性,可能还需要按照商品的销量、价格或者用户的个性化偏好等因素来调整搜索结果的顺序。这就引入了重排序(Rescoring)的概念。

重排序允许我们在 ElasticSearch 初始搜索结果的基础上,根据自定义的规则再次对结果进行排序,从而定制搜索结果的展示顺序,以更好地满足业务需求。重排序操作是在初始搜索结果集的一个子集上执行的,这样可以在保证性能的前提下,灵活地调整搜索结果的优先级。

重排序的执行时机与方式

ElasticSearch 提供了两种主要的重排序执行方式:query then fetchdfs query then fetch。这两种方式在搜索流程上略有不同,进而影响到重排序的效果和性能。

query then fetch

query then fetch 模式下,ElasticSearch 首先执行查询阶段(query phase)。在这个阶段,每个分片(shard)独立计算文档与查询条件的相关性得分,并根据得分返回一个排序列表,这个列表仅包含文档的 ID 和得分。然后,协调节点(coordinating node)收集各个分片返回的列表,并合并成一个全局排序列表,从中选取前 size 个文档的 ID。接下来进入取回阶段(fetch phase),协调节点根据选取的文档 ID 从各个分片获取完整的文档内容。

重排序操作通常在取回阶段之后执行。也就是说,先获取默认排序下的初始结果集,然后对这个结果集按照重排序规则进行二次排序。这种方式的优点是查询阶段的开销相对较小,因为只需要传输文档 ID 和得分,适用于大规模数据集的搜索。但缺点是重排序是在已经获取的结果集上进行,可能无法充分利用所有文档的信息,导致重排序的准确性受到一定影响。

dfs query then fetch

dfs query then fetch 模式相比 query then fetch 多了一个分布式词频统计(Distributed Frequency Scan,DFS)的步骤。在查询阶段之前,先执行 DFS 步骤,各个分片会统计词频信息并汇总到协调节点,协调节点根据这些信息计算全局的词频统计数据。然后再执行查询阶段,与 query then fetch 类似,每个分片计算文档得分并返回排序列表。但由于在查询前已经有了更准确的词频统计,所以计算出的相关性得分可能更加准确。

重排序同样在取回阶段之后执行。这种方式的优点是能够获得更准确的相关性得分,从而在重排序时可以基于更精确的初始结果。然而,DFS 步骤增加了额外的开销,特别是在大规模数据和复杂查询的情况下,性能可能会受到一定影响。

重排序的参数与配置

在 ElasticSearch 中进行重排序操作,需要通过 rescore 参数来配置。rescore 参数是一个数组,可以包含多个重排序规则,每个规则按照数组顺序依次应用。下面详细介绍 rescore 参数的各个配置项。

window_size

window_size 定义了重排序操作所基于的初始结果集的大小。默认情况下,ElasticSearch 只会对初始搜索结果中的前 size 个文档进行重排序。通过设置 window_size,可以扩大这个范围,从而在更多的文档上应用重排序规则。例如,如果 size 设置为 10,window_size 设置为 50,那么会从初始搜索结果的前 50 个文档中选取前 10 个进行最终的返回,这 10 个文档是经过重排序后的结果。

query

query 部分用于定义重排序的查询条件。它可以是任何 ElasticSearch 支持的查询类型,例如 matchtermfunction_score 等。通过这个查询条件,ElasticSearch 会为每个文档计算一个新的得分,然后根据这个新得分对文档进行重排序。

score_mode

score_mode 决定了如何将初始得分(默认相关性得分)和重排序得分进行合并。常见的 score_mode 有以下几种:

  • multiply:将初始得分和重排序得分相乘,这是默认的模式。这种模式下,如果初始得分和重排序得分都很高,最终得分会更高,适用于需要同时兼顾相关性和重排序因素的场景。
  • sum:将初始得分和重排序得分相加。这种模式下,即使某个得分较低,但另一个得分较高时,也可能使文档在重排序后获得较高的最终得分。
  • avg:计算初始得分和重排序得分的平均值。
  • max:取初始得分和重排序得分中的较大值作为最终得分。
  • min:取初始得分和重排序得分中的较小值作为最终得分。

max_score

max_score 用于设置重排序得分的最大值。如果某个文档的重排序得分超过了 max_score,则会将其重排序得分截断为 max_score。这个参数主要用于控制重排序得分对最终结果的影响程度,避免某个文档因为重排序得分过高而过度影响整体排序。

重排序的代码示例

以下通过几个具体的代码示例来展示如何在 ElasticSearch 中使用重排序功能。假设我们有一个电商商品索引,包含商品名称、价格、销量等字段。

使用 match 查询进行重排序

首先,我们使用 match 查询对商品名称进行搜索,并根据销量对搜索结果进行重排序。

{
    "query": {
        "match": {
            "product_name": "手机"
        }
    },
    "rescore": [
        {
            "window_size": 50,
            "query": {
                "function_score": {
                    "field_value_factor": {
                        "field": "sales",
                        "modifier": "log1p",
                        "factor": 1
                    }
                }
            }
        }
    ]
}

在上述示例中,query 部分使用 match 查询搜索商品名称中包含 “手机” 的商品。rescore 部分定义了重排序规则,window_size 设置为 50,表示从初始搜索结果的前 50 个文档中进行重排序。query 内部使用 function_score,通过 field_value_factor 以销量字段 sales 为基础计算重排序得分,modifier 设置为 log1p 对销量进行对数变换,以避免销量过大对得分影响过于剧烈。

结合多种重排序规则

有时候,我们可能需要结合多个因素进行重排序。例如,在考虑销量的同时,还要考虑价格因素,价格越低得分越高。

{
    "query": {
        "match": {
            "product_name": "笔记本电脑"
        }
    },
    "rescore": [
        {
            "window_size": 50,
            "query": {
                "function_score": {
                    "field_value_factor": {
                        "field": "sales",
                        "modifier": "log1p",
                        "factor": 1
                    }
                }
            }
        },
        {
            "window_size": 50,
            "query": {
                "function_score": {
                    "field_value_factor": {
                        "field": "price",
                        "modifier": "reciprocal",
                        "factor": 1
                    }
                }
            },
            "score_mode": "sum"
        }
    ]
}

这里有两个重排序规则。第一个规则基于销量,与前面的示例类似。第二个规则基于价格,使用 reciprocal 修饰符,使得价格越低得分越高。score_mode 设置为 sum,表示将两个重排序得分相加,综合考虑销量和价格因素对搜索结果进行排序。

基于用户个性化偏好的重排序

在一些场景下,需要根据用户的个性化偏好进行重排序。假设我们有一个用户偏好索引,记录了每个用户对不同商品类别的喜好程度。

{
    "query": {
        "match": {
            "product_name": "运动装备"
        }
    },
    "rescore": [
        {
            "window_size": 50,
            "query": {
                "function_score": {
                    "script_score": {
                        "script": {
                            "source": "def userPreference = ctx._source.user_preference; def category = ctx._source.product_category; if (userPreference.containsKey(category)) { return userPreference.get(category); } return 0;",
                            "params": {
                                "user_id": "123"
                            }
                        }
                    }
                }
            }
        }
    ]
}

在这个示例中,script_score 使用脚本根据用户偏好计算重排序得分。通过 params 传入用户 ID,脚本根据用户偏好索引中该用户对商品类别的偏好值来为文档打分。如果商品类别在用户偏好中存在,则返回相应的偏好得分,否则得分为 0。这样就可以根据用户的个性化偏好对搜索结果进行重排序。

重排序的性能优化

虽然重排序功能为定制搜索结果顺序提供了强大的灵活性,但它也可能带来一定的性能开销。以下是一些优化重排序性能的建议。

合理设置 window_size

window_size 决定了重排序操作所基于的文档数量。如果设置过大,会增加重排序的计算量,导致性能下降。应根据业务需求和数据规模,合理评估并设置 window_size。例如,如果业务对相关性要求较高,且数据量较大,可以适当减小 window_size,在保证一定相关性的前提下提高性能。

避免复杂的重排序查询

重排序查询中的逻辑越复杂,计算量就越大。尽量使用简单的查询类型和操作符,避免在重排序查询中使用过于复杂的脚本或嵌套查询。例如,优先使用 field_value_factor 等简单的函数来计算得分,而不是编写复杂的脚本。

缓存重排序结果

如果搜索请求具有一定的重复性,可以考虑对重排序结果进行缓存。例如,使用 Redis 等缓存工具,将相同查询条件下的重排序结果缓存起来,下次遇到相同请求时直接从缓存中获取,减少 ElasticSearch 的计算压力。

结合索引优化

对涉及重排序的字段进行合理的索引设计也能提高性能。例如,对用于重排序的数值型字段(如价格、销量),可以考虑使用 doc_values 来提高数据的读取效率。同时,确保索引的分片数量和副本数量设置合理,避免过多的分片和副本导致的性能开销。

重排序在不同业务场景中的应用

重排序在各种业务场景中都有广泛的应用,以下列举几个常见的场景。

电商搜索

在电商平台的搜索功能中,除了相关性,价格、销量、库存等因素都可能影响商品的展示顺序。通过重排序,可以根据用户的搜索意图和业务需求,灵活调整商品的排序。例如,在促销活动期间,可以将价格折扣大的商品排在前面;对于热门商品推荐,可以优先展示销量高的商品。

内容推荐

在新闻、文章等内容推荐场景中,除了考虑内容与用户兴趣的相关性,还可以根据文章的发布时间、浏览量、点赞数等因素进行重排序。比如,最新发布的文章可能具有更高的时效性,浏览量高的文章可能更受用户关注,通过重排序可以将这些因素融入到推荐结果中,提供更符合用户需求的内容。

企业内部搜索

在企业内部的文档搜索、知识库搜索等场景中,重排序也能发挥重要作用。例如,根据文档的访问频率、更新时间、作者的权限等因素对搜索结果进行排序,使得员工能够更快地找到最相关、最常用的文档。

重排序与相关性平衡

在使用重排序时,需要注意与文档相关性的平衡。虽然重排序可以根据业务需求调整搜索结果的顺序,但如果过度依赖重排序而忽视了文档与查询的相关性,可能会导致搜索结果与用户的原始意图偏差较大。

为了保持这种平衡,一方面可以合理调整 score_mode 参数,使得初始相关性得分和重排序得分能够以合适的方式合并。例如,在电商搜索中,如果相关性仍然是重要因素,可以选择 multiply 模式,让相关性得分和重排序得分相互影响,避免某个因素过度主导排序结果。另一方面,通过合理设置 window_size,确保在重排序时仍然基于一定数量的相关性较高的文档,而不是完全脱离相关性进行排序。

重排序在分布式环境中的考虑

ElasticSearch 是一个分布式搜索引擎,在分布式环境下进行重排序需要考虑一些特殊因素。

由于重排序是在协调节点上对各个分片返回的结果进行操作,所以网络延迟和分片负载均衡等因素可能会影响重排序的性能和结果一致性。为了应对这些问题,一方面要确保网络环境的稳定,减少网络延迟对结果的影响;另一方面,合理设置分片和副本,通过负载均衡来降低单个分片的负载压力。

此外,在分布式环境中,不同分片的数据可能存在一定的延迟或不一致性。这可能会导致重排序结果在不同时间或不同查询节点上略有差异。在设计业务逻辑时,需要考虑这种结果的微小不一致性对业务的影响,并根据实际情况采取相应的措施,如通过缓存来减少不一致性的暴露。

重排序的常见问题与解决方法

在使用重排序过程中,可能会遇到一些常见问题,以下是这些问题及相应的解决方法。

重排序后结果不符合预期

这可能是由于重排序规则设置不当导致的。首先,检查 query 部分的重排序查询条件是否准确表达了业务需求。例如,在使用 function_score 时,确认 field_value_factor 等函数的参数设置是否正确,是否正确引用了需要的字段。其次,检查 score_mode 是否选择合适,不同的 score_mode 会导致初始得分和重排序得分合并方式的不同,从而影响最终结果。

重排序性能问题

如前文所述,重排序可能带来性能开销。如果发现重排序后搜索响应时间过长,可以从以下几个方面排查。一是检查 window_size 是否设置过大,尝试适当减小 window_size 并观察性能变化。二是分析重排序查询的复杂度,简化复杂的查询逻辑。三是查看索引结构和配置,确保索引的设计有利于重排序操作,如对相关字段使用合适的索引类型和存储方式。

重排序与聚合冲突

在一些情况下,重排序可能与聚合操作产生冲突。例如,当对搜索结果进行重排序后,聚合结果可能与预期不符。这是因为重排序是在文档级别进行操作,而聚合是在分片级别进行统计。解决这个问题的方法是在设计查询时,充分考虑重排序和聚合的先后顺序以及它们对结果的影响。可以尝试先进行聚合操作,再对聚合结果对应的文档进行重排序;或者根据业务需求,调整聚合的方式和范围,使其与重排序结果相兼容。

通过深入理解重排序的原理、参数配置、性能优化以及常见问题的解决方法,开发人员可以在 ElasticSearch 中灵活运用重排序功能,定制出符合业务需求的搜索结果顺序,提升搜索体验和业务价值。在实际应用中,需要根据具体的业务场景和数据特点,不断优化重排序的设置,以达到最佳的搜索效果和性能表现。同时,随着业务的发展和数据的变化,也要持续关注重排序的效果,适时调整相关配置,确保搜索服务始终能够满足用户的需求。