获得一致的评分
Elastic Stack Serverless
Elasticsearch 使用分片和副本的事实增加了获得良好评分的挑战。
假设同一个用户连续两次运行相同的请求,但文档返回的顺序不同,这不是一个非常糟糕的体验吗?不幸的是,如果您有副本(index.number_of_replicas
大于 0),这种情况可能会发生。原因是 Elasticsearch 以循环方式选择查询应该发送到的分片,因此如果您连续运行相同的查询两次,它很可能会发送到相同分片的不同副本。
现在为什么会成为问题?索引统计信息是评分的重要组成部分。由于已删除的文档,这些索引统计信息在相同分片的副本之间可能不同。您可能知道,当文档被删除或更新时,旧文档不会立即从索引中删除,它只是被标记为已删除,并且只有在下次合并此旧文档所属的段时才会从磁盘中删除。然而,出于实际原因,这些已删除的文档会被纳入索引统计信息。因此,假设主分片刚刚完成了一个大型合并,删除了大量已删除的文档,那么它的索引统计信息可能与副本(仍然有大量已删除的文档)有足够的差异,从而导致分数也不同。
解决此问题的推荐方法是使用一个字符串来标识登录的用户(例如用户 ID 或会话 ID)作为首选项。这确保了给定用户的所有查询始终命中相同的分片,因此分数在查询之间保持更加一致。
这种解决方法还有另一个好处:当两个文档具有相同的分数时,默认情况下它们将按其内部 Lucene doc id(与 _id
无关)排序。然而,这些 doc id 在相同分片的副本之间可能不同。因此,通过始终命中相同的分片,我们将获得具有相同分数的文档的更一致的排序。
如果您注意到具有相同内容的两个文档获得不同的分数,或者完全匹配的文档没有排在第一位,那么问题可能与分片有关。默认情况下,Elasticsearch 使每个分片负责生成自己的分数。然而,由于索引统计信息是分数的的重要贡献者,因此只有当分片具有相似的索引统计信息时,这种方法才能很好地工作。假设文档默认情况下均匀地路由到分片,那么索引统计信息应该非常相似,并且评分应该按预期工作。然而,如果您
- 在索引时使用路由,
- 查询多个索引,
- 或者索引中的数据太少
那么参与搜索请求的所有分片很可能没有相似的索引统计信息,并且相关性可能很差。
如果您有一个小数据集,解决此问题的最简单方法是将所有内容索引到一个具有单个分片的索引中(index.number_of_shards: 1
),这是默认值。然后,所有文档的索引统计信息将相同,并且分数将一致。
否则,解决此问题的推荐方法是使用dfs_query_then_fetch
搜索类型。这将使 Elasticsearch 执行到所有涉及的分片的初始往返,要求它们提供相对于查询的索引统计信息,然后协调节点将合并这些统计信息,并在要求分片执行query
阶段时将合并的统计信息与请求一起发送,以便分片可以使用这些全局统计信息而不是它们自己的统计信息来进行评分。
在大多数情况下,这个额外的往返应该非常便宜。然而,如果您的查询包含非常大量的字段/术语或模糊查询,请注意,仅收集统计信息可能并不便宜,因为必须在术语字典中查找所有术语才能查找统计信息。