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 阶段时将合并的统计信息与请求一起发送,这样分片就可以使用这些全局统计信息而不是它们自己的统计信息来进行评分。关于 dfs_query_then_fetch 的用法,你可以参阅我的另外一篇文章 “Elasticsearch:分布式计分”。

在大多数情况下,这个额外的往返应该非常便宜。但是,如果你的查询包含大量字段/术语或模糊查询,请注意单独收集统计信息可能并不便宜,因为必须在术语字典中查找所有术语才能查找统计信息。