了解“Query Then Fetch”与“DFS Query Then Fetch”的区别
在我们关于以开头短语匹配的上一篇文章中,我们运行到了返回分数可疑的情况。作为回顾,以下是相关的查询:
$ curl -XGET localhost:9200/startswith/test/_搜索?pretty -d '{
"query": {
"match_phrase_prefix": {
"title": {
"查询": "d",
"max_expansions": 5
}
}
}
}' | grep title
"_score" : 1.0, "_source" : {"title":"drunk"}
"_score" : 0.30685282, "_source" : {"title":"dzone"}
"_score" : 0.30685282, "_source" : {"title":"data"}
"_score" : 0.30685282, "_source" : {"title":"drive"}
看到文档“drunk”如何获得 1.0 的评分,而其余文档的评分为 0.3 了吗?既然它们同样匹配“d”的查询,这些文档不应该具有相同的评分吗?答案是肯定的,但这种评分差异背后有一个非常好的理由。
相关性评分
Elasticsearch(以及底层的 Lucene)所使用的评分算法的一部分包括“词频 - 逆文档频率 (TF-IDF)”统计信息,以帮助计算索引中相关文档的相关性。
关于 TF-IDF 的主题已经有很多论述,但它基本上说明了“一个词在文档中出现的次数越多,该文档的相关性就越高。但相关性会受到该词在整个索引中出现频率的抑制”。
稀有词仅存在于少数文档中,这意味着任何匹配稀有词的查询都会变得高度相关。相反,常用词随处可见,因此它们与查询的相关性较低。
当您执行搜索时,Elasticsearch 面临一个有趣的困境。您的查询需要找到所有相关文档……但这些文档分散在集群中的任意数量的分片中。
每个分片基本上都是一个 Lucene 索引,它维护着自己的 TF 和 DF 统计信息。分片只知道“pineapple”在分片内出现了多少次,而不知道整个集群的情况。
但是相关性算法使用 TF-IDF……它难道不需要知道整个索引的 TF 和 DF,而不是每个分片的吗?
默认搜索类型:Query Then Fetch
答案是肯定的,也是否定的。默认情况下,Elasticsearch 将使用一种称为“Query Then Fetch”的搜索类型。其工作方式如下:
- 将查询发送到每个分片
- 查找所有匹配的文档,并使用本地词频/文档频率计算分数
- 构建结果优先级队列(排序、使用 from/to 进行分页等)
- 将有关结果的元数据返回给请求Node。请注意,此时尚未发送实际文档,仅发送了分数
- 来自所有分片的评分在请求Node上进行合并和排序,并根据查询条件选择文档
- 最后,实际文档会从它们所在的各个分片中检索出来。
- 结果返回至客户端
该系统通常运行良好。在大多数情况下,您的索引中拥有“足够”的文档来平滑词频/文档频率统计信息。因此,虽然每个分片可能无法完全掌握整个集群的频率信息,但由于频率在各处都相当相似,结果也“足够好”。
但在本文开头提到的查询案例中,默认的搜索类型有时会失败。
DFS Query Then Fetch
在上一篇文章中,我们构建了一个索引,但未指定分片数量——Elasticsearch 使用了默认的 5 个分片。随后,我们在索引中插入了区区五个文档,并要求 ES 返回相关的结果和准确的评分。这不太公平,不是吗?
评分差异是由 Query Then Fetch 搜索类型引起的。每个分片仅包含 1 或 2 个文档(ES 使用的哈希算法确保了相对随机的分布)。当我们要求 Elastic 计算评分时,每个分片对这五个文档的索引只有很小的视角……因此评分是不准确的。
幸运的是,Elasticsearch 不会让您陷入困境。如果您遇到的情况中这种评分差异是个问题,ES 提供了一种名为“DFS Query Then Fetch”的搜索类型。其过程与 Query Then Fetch 几乎相同,区别在于它执行预查询来计算全局文档频率。
- 预查询每个分片以询问词频和文档频率
- 将查询发送到每个分片
- 查找所有匹配的文档,并使用从预查询计算出的全局词项/文档频率来计算分数。
- 构建结果优先级队列(排序、使用 from/to 进行分页等)
- 将有关结果的元数据返回给请求Node。请注意,此时尚未发送实际文档,仅发送了分数
- 来自所有分片的评分在请求Node上进行合并和排序,并根据查询条件选择文档
- 最后,实际文档会从它们所在的各个分片中检索出来。
- 结果返回至客户端
如果我们对之前的查询应用这种新的搜索类型,我们将获得合理的评分结果(例如,它们完全相同):
$ curl -XGET 'localhost:9200/startswith/test/_search?pretty=true&search_type=dfs_query_then_fetch' -d '{
"query": {
"match_phrase_prefix": {
"title": {
"查询": "d",
"max_expansions": 5
}
}
}
}' | grep title
"_score" : 1.9162908, "_source" : {"title":"dzone"}
"_score" : 1.9162908, "_source" : {"title":"data"}
"_score" : 1.9162908, "_source" : {"title":"drunk"}
"_score" : 1.9162908, "_source" : {"title":"drive"}
结论
当然,更高的准确性并非没有代价。预查询 (prequery) 会在分片之间导致额外的往返,这可能会根据索引大小、分片数量、查询速率等因素造成性能影响。在大多数情况下,这完全没有必要……拥有“足够”的数据就能为您解决问题。
但有时您会运行到奇怪的评分情况,在这种情况下,了解如何通过 DFS Query then Fetch 来调整搜索执行计划会很有用。