注: 作为本文的
样例数据
,都放在了文章末尾的3.样例数据小节中了。之后的讲解中,都以这些样例数据为例子。
1. 引言
基于样例数据的dept字段(keyword类型),执行如下两个查询
查询一:
# 直接使用term查询,查询dept为UI的数据,可以查询到id=7的文档
GET idx-susu-test-context/_search
{
"query": {
"term": {
"dept": {
"value": "UI"
}
}
}
}
查询二:
# 在term查询外面,套一层constant_score+filter查询,查询dept为UI的数据,也可以查询到id=7的文档
GET idx-susu-test-context/_search
{
"query": {
"constant_score": {
"filter": {
"term": {
"dept": "UI"
}
}
}
}
}
可以看到,在上面的两个查询,虽然查询的写法不一样,但最后都查询到了同一条数据。
既然如此,那么哪一种写法更优呢?
答案是第二种写法!
这是因为,第一种写法是基于query context进行的查询,而第二种写法用 constant_score 将 term 查询转化成为过滤器,它是基于filter context的查询。
- 进行query context查询时,ES除了要判断某个文档是否与查询值匹配,还要计算相关度评分(relevance score),并放入到返回结果的_score字段中!
- 而当进行filter context查询时,仅仅判断某个文档是否与查询值匹配,不但无需进行相关度评分的计算,而且对于高频率的filter查询,ES还会自动将查询结果缓存起来,以提高filter查询的性能。
通过在查询中加入"explain": true参数,可以看到各个查询的评分计算详情!
查询一:
# 直接使用term查询,可以查询到id=7的文档(添加explain参数!)
GET idx-susu-test-context/_search
{
"explain": true,
"query": {
"term": {
"dept": {
"value": "UI"
}
}
}
}
# 查询结果如下,从_socre和explanation中可以看到,本次查询进行了相关度计算(能看到计算的detail!)!
{
……
"hits" : {
"total" : {
"value" : 1,
"relation" : "eq"
},
"max_score" : 0.6931472,
"hits" : [
{
……
"_id" : "7",
"_score" : 0.6931472,
"_source" : {
"id" : 7,
"birth" : "1995-09-01 16:16:16",
"salary" : 20000,
"dept" : "UI",
"addr" : "上海浦东"
},
"_explanation" : {
"value" : 0.6931472,
"description" : "weight(dept:UI in 1) [PerFieldSimilarity], result of:",
"details" : [
{
"value" : 0.6931472,
"description" : "score(freq=1.0), product of:",
"details" : [
{
"value" : 2.2,
"description" : "boost",
"details" : [ ]
},
……
]
}
]
}
}
]
}
}
查询二:
# 在term查询外面,套一层constant_score+filter查询,也可以查询到id=7的文档(添加explain参数!)
GET idx-susu-test-context/_search
{
"explain": true,
"query": {
"constant_score": {
"filter": {
"term": {
"dept": "UI"
}
}
}
}
}
# 查询结果,从结果的_explanation可以看出,本次查询并没有进行相关度计算!_score评分为一个常量值(ConstantScore)
{
……
"hits" : [
{
"_shard" : "[idx-susu-test-context][0]",
"_node" : "rlSHf1LoS4O3M7wyMbn1WQ",
"_index" : "idx-susu-test-context",
"_type" : "_doc",
"_id" : "7",
"_score" : 1.0,
"_source" : {
"id" : 7,
"birth" : "1995-09-01 16:16:16",
"salary" : 20000,
"dept" : "UI",
"addr" : "上海浦东"
},
"_explanation" : {
"value" : 1.0,
"description" : "ConstantScore(dept:UI)",
"details" : [ ]
}
}
]
}
综上,就是为什么在上述示例情形(即:基于机构化数据如数值、keyword、boolean、date等类型进行过滤查询的情形)的查询中,更推荐使用constant_score查询的原因!
通过这示例,我们引出了query context和filter context,也知道了,在特定场景下,filter context方式比query context有更优的查询性能,那么接下来,就对这两者进行相应的分析。
2. query context 和filter context
2.1 relevance score
默认情况下
,Elasticsearch 按 relevance score 对匹配的搜索结果进行排序,该得分衡量每个文档与查询的匹配程度。
relevance score 是一个正浮点数,在搜索 API 的 _score 元字段中返回。 _score 越高,文档越相关。 虽然每种查询类型可以用不同的方式计算相关性分数,但是分数计算还取决于查询子句是在 query context 中还是在 filter context 中运行。
2.2 query context
在 query context 中,查询子句回答的是“此文档与该查询子句的匹配程度如何
” 的问题。除了确定文档是否匹配外,查询子句还计算 _score 元字段中的相关性得分。
每当将查询子句传递到 query 参数(例如 search API 中的 query 参数)时,query context 即生效。
2.3 filter context
在 filter context 中,查询子句回答问题 “此文档是否
与此查询子句匹配
?” 答案是简单的 “是” 或 “否” ,即不计算分数
。 filter context 主要用于过滤
结构化数据,例如
- 此timestamp字段是否在2015年到2016年之间?
- status字段是否被设置为“published”?
filter context查询除了不计算相关度评分,对于常用的 filter 查询,ES还会自动缓存,以提高下次查询的性能
(关于filter context的查询缓存,后面会有详细讲解)。
每当将查询子句传递到 filter 参数(例如 bool 查询中的 filter 或 must_not 参数,constant_score 查询中的 filter 参数或 filter 聚合)时,filter context context 即生效。
2.4 示例
GET /_search
{
"query": {
#query参数说明是query上下文
"bool": {
#bool和两个match子句在query上下文中,意味着他们使用score来计算如何与文档匹配。
"must": [
{
"match": {
"title": "Search" }},
{
"match": {
"content": "Elasticsearch" }}
],
"filter": [ #filter参数说明是filter上下文
{
"term": {
"status": "published" }}, # term和range应用于filter上下文,它们将过滤不匹配的文档,但不会影响匹配文档的score值。
{
"range": {
"publish_date": {
"gte": "2015-01-01" }}}
]
}
}
}
2.5 warning
在 query context 中为查询计算的分数表示为单精度 floating 数; 它们只有 24 位才能表示有效的精度。 超过有效位数的分数计算将被转换为 floats 而失去精度。
2.6 tip
所以,需要结合自己的实际使用情况来决定是使用query context还是filter context
- 在需要用_score来匹配文档的情况下使用query context,比如百度搜索时,相关度最高的要排在最前面
- 其他情况比如当查找一个精确值的时候,我只想通过某个条件,将数据给筛选出来,我并不关心匹配程度,那么此时就可以使用filter context
3. filter context的执行步骤
以下内容来自于ES 2.x的官网
在内部,Elasticsearch 会在运行非评分查询的时执行多个操作:
-
查找匹配文档.
term 查询在倒排索引中查找 XHDK-A-1293-#fJ3 然后获取包含该 term 的所有文档。本例中,只有文档 1 满足我们要求。
-
创建 bitset.
过滤器会创建一个 bitset (一个包含 0 和 1 的数组),它描述了哪个文档会包含该 term 。匹配文档的标志位是 1 。本例中,bitset 的值为 [1,0,0,0] 。在内部,它表示成一个 “roaring bitmap”,可以同时对稀疏或密集的集合进行高效编码。
-
迭代 bitset(s)
一旦为每个查询生成了 bitsets ,Elasticsearch 就会循环迭代 bitsets 从而找到满足所有过滤条件的匹配文档的集合。执行顺序是启发式的,但一般来说先迭代稀疏的 bitset (因为它可以排除掉大量的文档)。
-
增量使用计数.
Elasticsearch 能够缓存非评分查询从而获取更快的访问,但是它也会不太聪明地缓存一些使用极少的东西。非评分计算因为倒排索引已经足够快了,所以我们只想缓存那些我们 知道 在将来会被再次使用的查询,以避免资源的浪费。
为了实现以上设想,Elasticsearch 会为每个索引跟踪保留查询使用的历史状态。如果查询在最近的 256 次查询中会被用到,那么它就会被缓存到内存中。当 bitset 被缓存后,缓存会在那些低于 10,000 个文档(或少于 3% 的总索引数)的段(segment)中被忽略。这些小的段即将会消失,所以为它们分配缓存是一种浪费。
实际情况并非如此(执行有它的复杂性,这取决于查询计划是如何重新规划的,有些启发式的算法是基于查询代价的),理论上非评分查询 先于 评分查询执行。非评分查询任务旨在降低那些将对评分查询计算带来更高成本的文档数量,从而达到快速搜索的目的。
从概念上记住非评分计算是首先执行的
,这将有助于写出高效又快速的搜索请求。
注:更多的关于filter context查询的缓存详情,可以查看ES2.x官网的这个链接。
之所以直接把链接放出来,是因为我也没有在7.x版本的官网中,找到对于这一块的描述。
但是我确实在7.x版本的官网中,找到了相关的描述,跟上面连接中的描述是一致的!链接。
关于queryCache的一些优秀的博客:
- https://blog.csdn.net/allwefantasy/article/details/81039503
- https://www.cnblogs.com/chennanlcy/p/6591790.html
3. 样例数据
这里统一设置了一个名为idx-susu-test-context的索引,使用手动设置mappings的方式,尽可能多的插入了不同类型的字段,并且也插入了尽可能多情形的数据,作为本文的测试数据。
# 1.为了防止index已存在,先删除一下
DELETE idx-susu-test-context
# 2.手动设置index的mapping
PUT idx-susu-test-context/
{
"mappings": {
"properties": {
"id": {
"type": "long"
},
"birth": {
"type": "date",
"format": "yyyy-MM-dd HH:mm:ss||yyyy-MM-dd||epoch_millis"
},
"salary": {
"type": "float"
},
"detp": {
"type": "keyword"
},
"addr": {
"type": "text",
"analyzer": "ik_max_word"
}
}
}
}
# 3.批量插入数据,同时刷新index
POST idx-susu-test-context/_bulk?refresh=true
{
"index": {
"_id": "1" }}
{
"id": 1, "birth": "1980-01-01 00:00:00", "salary": 40000, "dept": "BIGDATA", "addr": "北京昌平区"}
{
"index": {
"_id": "2" }}
{
"id": 2, "birth": "1985-02-01 10:20:00", "salary": 35000, "dept": "BIGDATA", "addr": "北京海淀区"}
{
"index": {
"_id": "3" }}
{
"id": 3, "birth": "1989-11-01 05:00:10", "salary": 50000, "dept": "SALE", "addr": "北京朝阳区"}
{
"index": {
"_id": "4" }}
{
"id": 4, "birth": "1990-01-01 11:11:11", "salary": 40000, "dept": "IT", "addr": "北京昌平区"}
{
"index": {
"_id": "5" }}
{
"id": 5, "birth": "1991-06-01 12:12:12", "salary": 30000, "dept": "IT", "addr": "北京昌平区"}
{
"index": {
"_id": "6" }}
{
"id": 6, "birth": "2001-03-01 13:13:13", "salary": 40000, "dept": "IT", "addr": "北京昌平区"}
{
"index": {
"_id": "7" }}
{
"id": 7, "birth": "1995-09-01 16:16:16", "salary": 20000, "dept": "UI", "addr": "上海浦东"}
# 4.全量查询(按id升序排序)
GET idx-susu-test-context/_search
{
"sort": [
{
"id": {
"order": "asc"
}
}
]
}
注:每一行数据,表示的就是一个用户的信息,包括:id、生日(birth)、薪酬(salary)、部门(dept)、地址(addr)