结构化搜索(Structured search) 是指有关探询那些具有内在结构数据的过程。比如日期、时间和数字都是结构化的:它们有精确的格式,我们可以对这些格式进行逻辑操作。比较常见的操作包括比较数字或时间的范围,或判定两个值的大小。
1.精确值查找
当使用精确值查找时,我们会使用过滤器,由于它不会计算相关度,所以它的查询速度相对较快。
我们首先来看最为常用的 term 查询, 可以用它处理数字、布尔值、日期以及文本。使用方式如下:
GET /index/type/_search
{
"query" : {
"constant_score" : { //表示该次查询不计算相关度,常使用它将查询转化为过滤
"filter" : {
"term" : {
"filed" : value
}
}
}
}
}
2.bool组合过滤器
在第一部分中,我们讲了单个过滤器的使用,在日常生活中,我们经常要使用多个过滤器来进行筛选结果,比如我们要选择华为品牌,64G, 价格在2k-4k之间的手机,我们要使用组合过滤器对结果进行筛选。
一个过滤器包括三种取值:
(1)must: 表示必须匹配,相当于AND。
(2)must_not: 表示必须不匹配,相当于NOT。
(3)should:表示任一匹配即可,相当于OR。
使用案例如下(和传统的sql语句相同,bool过滤器也支持嵌套):
查询出产品为三星NOTE7,或者 产品为华为而且价格为30的记录
GET /index/type/_search
{
"query" : {
"filtered" : {
"filter" : {
"bool" : {
"should" : [
{ "term" : {"productID" : "三星NOTE7"}},
{ "bool" : {
"must" : [
{ "term" : {"productID" : "华为"}},
{ "term" : {"price" : 30}}
]
}}
],
"minimum_should_match": 1
}
}
}
}
}
注意:
1 . bool 查询会为每个文档计算相关度评分 _score ,再将所有匹配的 must 和 should 语句的分数 _score 求和,最后除以 must 和 should 语句的总数。其中must_not 语句不会影响评分;它的作用只是将不相关的文档排除。
2 .所有 must 语句必须匹配,所有 must_not 语句都必须不匹配,默认情况下,没有 should 语句是必须匹配的,只有一个例外:那就是当没有 must 语句的时候,至少有一个 should 语句必须匹配。可以通过设置minimum_should_match
参数来设置必须匹配的should语句,他可以使数字也可以是百分比。
3.查询多个精确值
term查询在搜索单个值十分有用,但是我们如何搜索满足多个条件的值呢?比如要搜索价格为20的手机,相当于传统sql语句中的in
,当然使用bool查询是一种方式,es给我们提供了terms
查询,使用方式如下:
{
"terms" : {
"price" : [20, 30]
}
}
注意:term和terms的操作是包含而非精确相等,比如我们使用该语句{ "term" : { "tags" : "search" } }
进行过滤时,他会和以下两个文档同时匹配:
{ "tags" : ["search"] }
{ "tags" : ["search", "open_source"] }
如果向要精确匹配,最好的操作就是增加一个字段,用以存储该字段包含项的数量,用以上为例
{ "tags" : ["search"], "tag_count" : 1 }
{ "tags" : ["search", "open_source"], "tag_count" : 2 }
在搜索条件时,将语句修改为bool查询的must
操作,让tags
的值为search
,且tag_count
的值为1
。
"bool" : {
"must" : [
{ "term" : { "tags" : "search" } },
{ "term" : { "tag_count" : 1 } }
]
}
4.范围
到目前为止,我们都介绍了如何处理相等查询,对于数字过滤查询有时会更有用,比如在淘宝上搜索手机,可以在首页的头部可以选择价格区间,用来更快的找到满足自己价位的手机。
"range" : {
"price" : {
"gte" : greaterValue,
"lte" : lessValue
}
}
gt: > 大于(greater than)
lt: < 小于(less than)
gte: >= 大于或等于(greater than or equal to)
lte: <= 小于或等于(less than or equal to)
range查询不只是能用在数字上,其他类型也可以使用,比如日期,字符串
- 日期查询
eg: 查询过去一小时内的所有文档
"range" : {
"timestamp" : {
"gt" : "now-1h"
}
}
日期计算还可以应用到具体的时间,并非只能是now这样的占位符,只需要增加一个双管符号||
并紧跟一个一个日期表达式。更多详细内容请看时间格式参考文档。
eg: 小于2020-01-07加一个月也就是 2020-02-07之前,2020-01-07之后的所有文档
"range" : {
"timestamp" : {
"gt" : "2020-01-07 00:00:00",
"lt" : "2020-01-07 00:00:00||+1M"
}
}
2.字符串范围
es在给字符串范围操作时,是使用的字典序或这字母顺序进行过滤的。
eg:查询文档字段title中只包含a的文档。
"range" : {
"title" : {
"gte" : "a",
"lt" : "b"
}
}
注意:数字和日期范围的过滤会提高搜索性能,但是字符串有可能并非如此,在对字符串过滤时,es会对每个词项都进行term
过滤。
5.关于缓存
es对非评分的行为,会对结果进行缓存。
1. 查找匹配文档
2. 创建bitset对结果进行缓存,该bit有一个只包含0和1的数组,用来标记那个文档匹配。
比如我们有三个doc
doc1: { price : 20, productid : 1 }
doc2: { price : 30, productid : 2 }
doc3: { price : 40, productid : 3 }
进行过滤查询 : term: { productid : 3 }
此时会创建bitset数组 [ 0, 0, 1]
3. 如果有多个过滤查询,会迭代bitsets,对结果集进行合并,一般会先迭代稀疏的bitset,因为会过滤大量的文档。
4. es会根据该索引最近的所查询的历史状态,如果该该查询在最近的的256次查询被用到过,则缓存到内存中。如果可以则会判断该索引所在的段是否‘足够大’, 因为比较小的段最后会被合并,分配缓存会被浪费,如果所在的段较小,该缓存会被忽略。
注意:
1. 缓存中的bitset是增量更新的,如果新增加一个文档,并不会对所有的bitset缓存进行重新计算,而是只会计算该新文档,然后将结果保存到bitset数组中。
2. bitset并不会依赖他所在查询的上下文,这样使得缓存可以加速查询中经常使用的部分,从而降低较少、易变的部分所带来的消耗。
如下图所示:会查询在收件箱且没有读过的,或者不在收件箱的。其中1 和2的 bitset相同,尽管一个是 must ,一个是 must_not
"bool": {
"should": [
{ "bool": {
"must": [
{ "term": { "folder": "inbox" }}, 1
{ "term": { "read": false }}
]
}},
{ "bool": {
"must_not": {
"term": { "folder": "inbox" } 2
}
}}
]
}
3. es会基于使用频率自动缓存查询,如果一个非评分查询在最近的256次查询中被使用过,且所在的段足够大,才会缓存。