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