最近在学习Elasticsearch,进行match查询时发现数据都是同样的内容,但是命中的结果得分却不相同,感到很困惑,示例如下:
{
"took" : 1,
"timed_out" : false,
"_shards" : {
"total" : 5,
"successful" : 5,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : 4,
"max_score" : 1.0,
"hits" : [
{
"_index" : "megacorp",
"_type" : "employee",
"_id" : "2",
"_score" : 1.0,
"_source" : {
"first_name" : "Jane",
"last_name" : "Smith",
"age" : 32,
"about" : "I like to collect rock albums",
"interests" : [
"music"
]
}
},
{
"_index" : "megacorp",
"_type" : "employee",
"_id" : "4",
"_score" : 1.0,
"_source" : {
"first_name" : "Li",
"last_name" : "Haijing",
"age" : "35",
"about" : "I like to shopping foods",
"interests" : [
"forestry"
]
}
},
{
"_index" : "megacorp",
"_type" : "employee",
"_id" : "1",
"_score" : 1.0,
"_source" : {
"first_name" : "John",
"last_name" : "Smith",
"age" : 25,
"about" : "I love to go rock climbing",
"interests" : [
"sports",
"music"
]
}
},
{
"_index" : "megacorp",
"_type" : "employee",
"_id" : "3",
"_score" : 1.0,
"_source" : {
"first_name" : "Douglas",
"last_name" : "Fir",
"age" : 35,
"about" : "I like to build cabinets",
"interests" : [
"forestry"
]
}
}
]
}
}
其中id为1和2的文档中的"last_name" 都为"Smith",于是我对last_name进行搜素
GET /megacorp/employee/_search
{
"query" : {
"match" : {
"last_name" : "Smith"
}
}
}
执行结果如下:
{
"took" : 4,
"timed_out" : false,
"_shards" : {
"total" : 5,
"successful" : 5,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : 2,
"max_score" : 0.6931472,
"hits" : [
{
"_index" : "megacorp",
"_type" : "employee",
"_id" : "2",
"_score" : 0.6931472,
"_source" : {
"first_name" : "Jane",
"last_name" : "Smith",
"age" : 32,
"about" : "I like to collect rock albums",
"interests" : [
"music"
]
}
},
{
"_index" : "megacorp",
"_type" : "employee",
"_id" : "1",
"_score" : 0.2876821,
"_source" : {
"first_name" : "John",
"last_name" : "Smith",
"age" : 25,
"about" : "I love to go rock climbing",
"interests" : [
"sports",
"music"
]
}
}
]
}
}
从结果可见同样的数据返回的得分并不一样
id 1 0.6931472
id 2 0.2876821
显然这和我们预期的结果并不一致,于是开始分析不一致的原因,以下内容参考以下网址
https://www.jianshu.com/p/c7529b98993e
相关性打分
ES使用的打分算法包含了称之为“TF-IDF”的统计信息来帮助计算处于那个索引中的文档的相关性。
TF词频(Term Frequency),TF表示词条在文档d中出现的频率。
IDF逆向文件频率(Inverse Document Frequency),IDF的主要思想是:如果包含词条t的文档越少,IDF越大,则说明词条t具有很好的类别区分能力。
TFIDF基本思想就是“一个项在文档中出现的次数越多,那么这个文档更加相关;但相关性会被这个项在整个文档库中的次数削弱”。
稀有项出现在相对少的文档中,那么任何查询匹配了一个稀有项的相关性就变得很高。相反,平常项到处都有,他们的相关性就低了。
当用户执行一个搜索时,ES面对一个有趣的困境。你的查询需要找到所有相关的文档,但是这些文档分布在你的cluster中的任何数目的shard中。
每个shard是一个Lucene的索引,保存了自身的TF和IDF统计信息。一个shard只知道在其自身中出现的次数,而非整个cluster。
但是相关算法使用了TF-IDF,它必须要知道对于整个索引的而不是对每个shard的TF和IDF吗?
答案:是也不是。因为ES支持不同的索引类型
默认搜索类型:query then fetch
默认情形下,ES会使用一个称之为Query then fetch
的搜索类型。它运作的方式如下:
- 发送查询到每个shard
- 找到所有匹配的文档,并使用本地的TF-IDF信息进行打分
- 对结果构建一个优先队列(排序,标页等)
- 返回关于结果的元数据到请求节点。注意,实际文档还没有发送,只是分数
- 来自所有shard的分数合并起来,并在请求节点上进行排序,文档被按照查询要求进行选择
- 最终,实际文档从他们各自所在的独立的shard上检索出来
- 结果被返回给用户
这个系统一般是能够良好地运作的。大多数情形下,你的索引有足够的文档来平衡本地的TF-IDF统计信息。因此,尽管每个shard不一定拥有完整的关于整个cluster的frequency信息,结果仍然足够好,因为fequency在每个地方基本上是类似的。
但是在我们开头提起的那个查询实例中,默认搜索类型是失败的(备注:因我的文档数据较少只有四条,但有5个shard,因此每次搜索都是失败的)。
dfs query then fetch
ES通常使用5个shard,每个shard仅仅包含一个或者两个文档(ES使用hash确保随机分布)。当我们要求ES计算分数时候,每个shard仅仅拥有关于五个文档的一个很窄的视角。所以分数是不准确的。
幸运的是,ES并没有让你无所适从。如果你遇到了这样的打分偏离的情形,ES提供了一个称为“DFS Query Then Fetch”。这个过程基本和Query Then Fetch类型,除了它执行了一个预查询来计算整体文档的frequency。
- 预查询每个shard,询问Term frequency和Document frequency
- 发送查询到每个shard
- 找到所有匹配的文档,并使用全局的Term Frequency/Inverse Document Frequency信息进行打分
- 对结果构建一个优先队列(排序,标页等)
- 返回关于结果的元数据到请求节点。注意,实际文档还没有发送,只是分数
- 来自所有shard的分数合并起来,并在请求节点上进行排序,文档被按照查询要求进行选择
- 最终,实际文档从他们各自所在的独立的shard上检索出来
- 结果被返回给用户
如果我们使用这个新的搜索类型,返回的分数就是相同的
{
"took" : 5,
"timed_out" : false,
"_shards" : {
"total" : 5,
"successful" : 5,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : 2,
"max_score" : 0.6931472,
"hits" : [
{
"_index" : "megacorp",
"_type" : "employee",
"_id" : "2",
"_score" : 0.6931472,
"_source" : {
"first_name" : "Jane",
"last_name" : "Smith",
"age" : 32,
"about" : "I like to collect rock albums",
"interests" : [
"music"
]
}
},
{
"_index" : "megacorp",
"_type" : "employee",
"_id" : "1",
"_score" : 0.6931472,
"_source" : {
"first_name" : "John",
"last_name" : "Smith",
"age" : 25,
"about" : "I love to go rock climbing",
"interests" : [
"sports",
"music"
]
}
}
]
}
}
结论
当然,更好准确性不是免费的。预查询本身会有一个额外的在shard中的轮询,这个当然会有性能上的问题(跟索引的大小,shard的数量,查询的频率等)。在大多数情形下,是没有必要的,拥有足够的数据可以解决这样的问题。
但是有时候,你可能会遇到奇特的打分场景,在这些情况中,知道如何使用DFS query then fetch
去进行搜索执行过程的微调还是有用的。