7,multi_match表示多字段匹配关键词,我们试着在name和sex里找,只要包含男的我们就返回该数据
{ "query": { "multi_match": { "query": "男", "fields": ["name","sex"] } } }
8,range查询,顾明思意就是范围查询,例如我们这里要查询年龄在19到28的人的数据:
{ "query": { "range": { "age" : { "gte" : 19, "lt" : 29 } } } }
这里需要解释下:range 查询可同时提供包含(inclusive)和不包含(exclusive)这两种范围表达式,可供组合的选项如下:
- gt: > 大于(greater than)
- lt: < 小于(less than)
- gte: >= 大于或等于(greater than or equal to)
- lte: <= 小于或等于(less than or equal to)
可以依据自己的需求自由进行组合。
9,exists允许你过滤文档,只查找那些在特定字段有值的文档,无论其值是多少,为了验证,需要注意,这里的有值即使是空值也算有值,只要不是null
{ "query": { "exists": { "field": "sex" } } }
10,wildcard,通配符查询,其中【?】代表任意一个字符【*】代表任意的一个或多个字符,例如我们想查名字结尾为林的文档:
{ "query": { "wildcard": { "name": "*林" } } }
11, prefix,前缀查询,我们为了找到所有姓名以森开头的文档,可以使用这种方式:
{ "query": { "prefix": { "name": "森" } } }
12,regexp,正则匹配,ES兼容了正则的查询方式,例如我们想查询性别为汉字字符的文档
{ "query": { "regexp": { "sex": "[\u4e00-\u9fa5]" } } }
13,fuzzy,纠错检索,让输入条件有容错性,例如我要检索性别为woman的数据,但是我拼错了,输入的是wman,用fuzzy照样可以检索到:
{ "query": { "fuzzy": { "sex": "wman" } } }
复合查询
复合查询通俗的说就是多个条件拼接查询,就是用Bool去拼接一系列的查询条件,来完成表达式的查询方式,其实就是将普通条件进行重新组合,常用的有四种复合类型:
- filter:只过滤符合条件的文档,与must唯一的区别是:不计算相关系得分,但因为有缓存,所以性能高
- must:用must连接的多个条件必须都满足,是and的关系,逻辑&与的关系。
- should:用should连接的多个条件只要满足一个即可,是or的关系,逻辑||或的关系
- must_not:用must_not绑定的条件表示一定不能满足该条件,是not的关系,逻辑^非的关系。
用这些条件的连接词将多个查询条件连接起来就能进行复杂的复合查询了。
filter使用
过滤器,文档必须匹配该过滤条件,跟must子句的唯一区别是,filter不影响查询的score,我们这里设置要查询的内容为【年龄在10-19岁之间 且 性别为男 且 姓名开头为森】的员工,查询语句为:
{"query":{ "bool": { "filter": [ {"term": {"sex": "男"}}, {"range": {"age": { "gte" : 18, "lt" : 29}}}, {"prefix": {"name": "森"}} ] } } }
返回结果为:
{ "took": 7, // 请求花了多少时间 "timed_out": false, //有没有超时 "_shards": { //执行请求时查询的分片信息 "total": 3, //查询的分片数量 "successful": 3, // 成功返回结果的分片数量 "skipped": 0, // 跳过的分片数量 "failed": 0 // 失败的分片数量 }, "hits": { "total": { "value": 2, //查询返回的文档总数 "relation": "eq" }, "max_score": 0.0, //计算所得的最高分 "hits": [ { "_index": "tml-userinfo", //索引 "_type": "_doc", //类型 "_id": "2", //标识符 "_score": 0.0, //得分 "_source": { //发送到索引的Json对象 "age": 28, "sex": "男", "name": "森小林" } }, { "_index": "tml-userinfo", "_type": "_doc", "_id": "1", "_score": 0.0, "_source": { "age": 18, "sex": "男", "name": "森小辰" } } ] } }
must使用
文档必须匹配must查询条件,我们这里设置要查询的内容为【年龄在10-19岁之间 且 性别为男 且 姓名开头为森】的员工,查询语句为:
{"query":{ "bool": { "must": [ {"term": {"sex": "男"}}, {"range": {"age": { "gte" : 18, "lt" : 29}}}, {"prefix": {"name": "森"}} ] } } }
should使用
文档应该匹配should子句查询的一个或多个,这里我们来查询【年龄在10-19岁之间 或 性别为男 或 姓名开头为森】,查询语句为:
{"query":{ "bool": { "should": [ {"term": {"sex": "男"}}, {"range": {"age": { "gte" : 18, "lt" : 29}}}, {"prefix": {"name": "森"}} ] } } }
must_not使用
文档不能匹配该查询条件,这里我们来查询【年龄不在10-19岁之间 且 性别不为男 且 姓名开头不为森】,查询语句为:
{"query":{ "bool": { "must_not": [ {"term": {"sex": "男"}}, {"range": {"age": { "gte" : 18, "lt" : 29}}}, {"prefix": {"name": "森"}} ] } } }
复合条件查询
我们在真实使用复合查询的时候肯定不仅仅要查单种条件的复合关系,还需要查多种关联条件,这里我们查一个【年龄在10-19岁之间 且 性别为男 且 姓名开头为森】或【姓名以男字结尾】但【年龄不能是28岁的】,需要注意:Boolean在同时有must和should的时候,should就被过滤掉了,因为should表示有也可以没有也可以,所以我们常把must放到should字句里,确保should的子句能执行
{ "query": { "bool": { "should": [ { "wildcard": { "name": "*男" } }, { "bool": { "must": [ { "term": { "sex": "男" } }, { "range": { "age": { "gte": 18, "lt": 29 } } }, { "prefix": { "name": "森" } } ] } } ], "must_not": [ { "term": { "age": 28 } } ] } } }
聚合查询
聚合查询实际上是一种统计和计算,按照官方文档的解释共有四类
- Metric(指标): 指标分析类型,如计算最大值、最小值、平均值等等 (对桶内的文档进行聚合分析的操作)
- Bucket(桶): 分桶类型,类似SQL中的GROUP BY语法 (满足特定条件的文档的集合)
- Pipeline(管道): 管道分析类型,基于上一级的聚合分析结果进行在分析
- Matrix(矩阵): 矩阵分析类型(聚合是一种面向数值型的聚合,用于计算一组文档字段中的统计信息)
一般聚合的写法如下:
"aggregations" : { "<aggregation_name>" : { <!--聚合的名字 --> "<aggregation_type>" : { <!--聚合的类型 --> <aggregation_body> <!--聚合体:对哪些字段进行聚合 --> } [,"meta" : { [<meta_data_body>] } ]? <!--元 --> [,"aggregations" : { [<sub_aggregation>]+ } ]? <!--在聚合里面在定义子聚合 --> } [,"<aggregation_name_2>" : { ... } ]* <!--聚合的名字 --> }
其中aggregations 也可简写为 aggs,虽然Elasticsearch有四种聚合方式,但在一般实际开发中,用到的比较多的就是Metric和Bucket。其实从功能上只有前两类:
- 一类是进行数学计算聚合使用的【max、min、avg、sum、stats 】,类似sql里的函数,也就是ES里的统计聚合
- 另一类则是进行分组聚合使用的,类似sql里的group,也就是ES里的桶聚合。
剩余的两类就是程序上的辅助聚合方式,一种是管道二次处理,一种似是分桶同时处理统计计算。
桶聚合
简单来说桶就是满足特定条件的文档的集合。当聚合开始被执行,每个文档里面的值通过计算来决定符合哪个桶的条件,如果匹配到,文档将放入相应的桶并接着开始聚合操作。桶也可以被嵌套在其他桶里面。我们先来看一个桶聚合,例如我们想要进行如下聚合【按照年龄聚合,0-10,10-20,20-30】,查询语句如下:
{ "size": 0, "aggs": { "user_age_info": { //聚合的名字 "range": { //聚合的类型 "field": "age", "ranges": [ { "from": 0, "to": 10 }, { "from": 11, "to": 20 }, { "from": 21, "to": 30 } ] } } } }
返回结果为:
{ "took": 2, "timed_out": false, "_shards": { "total": 3, "successful": 3, "skipped": 0, "failed": 0 }, "hits": { "total": { "value": 15, "relation": "eq" }, "max_score": null, "hits": [] }, "aggregations": { "user_age_info": { "buckets": [ { "key": "0.0-10.0", "from": 0.0, "to": 10.0, "doc_count": 4 //该范围内命中4个文档 }, { "key": "11.0-20.0", "from": 11.0, "to": 20.0, "doc_count": 7 //该范围内命中7个文档 }, { "key": "21.0-30.0", "from": 21.0, "to": 30.0, "doc_count": 4 //该范围内命中4个文档 } ] } } }
指标聚合
指标聚合分为两种,分别是单值分析和多值分析,各自有几个查询关键字:
- 单值分析只输出一个分析结果:min,max,avg,sum,cardinality,cardinality表示求唯一值,即不重复的字段有多少(相当于mysql中的distinct)
- 多值分析输出多个分析结果:stats, extended_stats, percentile, percentile_rank, top hits,Percentiles表示对指定字段的值按从小到大累计每个值对应的文档数的占比,返回指定占比比例对应的值。默认按照[ 1, 5, 25, 50, 75, 95, 99 ]来统计,例如我们想统计各个年龄百分区间内员工的数量会用到,Percentile Ranks通过文档值求百分比,例如我们想统计某个年龄在整个年龄list占比百分区间
统计计算这里我们分别使用一下以上几种方式进行分析,最后再结合分组进行一些复合的统计计算查询。
单值分析
单值分析以max举例,这里我们想查询,所有员工信息中,【年龄最大的员工】,则查询参数为:
{ "size":0, "aggs":{ "max_age":{ "max":{ "field":"age" } } } }
返回结果为:
{ "took": 19, "timed_out": false, "_shards": { "total": 3, "successful": 3, "skipped": 0, "failed": 0 }, "hits": { "total": { "value": 15, "relation": "eq" }, "max_score": null, "hits": [] }, "aggregations": { "avg_age": { "value": 18.0 } } }
其它几种单值查询sum、min、avg、cardinality 也类似。
多值分析
多值分析以stats举例,我想统计所有员工年龄的各种单值分析状态,
{ "size":0, "aggs":{ "age_stats":{ "stats":{ "field":"age" } } } }
返回值为:
{ "took": 3, "timed_out": false, "_shards": { "total": 3, "successful": 3, "skipped": 0, "failed": 0 }, "hits": { "total": { "value": 15, "relation": "eq" }, "max_score": null, "hits": [] }, "aggregations": { "age_stats": { "count": 15, "min": 8.0, "max": 28.0, "avg": 18.0, "sum": 270.0 } } }
分页查询
分页查询方式有很多种,我们就适用场景各自聊一种,也就是在浅度分页和深度分页中各自适用方式。还是3个节点的集群,共拥有12个分片,包括4个主分片(S0、S1、S2、S3)和8个复制分片(R0、R1、R2、R3),每个主分片对应两个副本分片,节点1是主节点(Master节点)负责整个集群状态
浅度分页适用场景
一个搜索请求到来的时候集群是如何响应的?我们举个例子来回顾下,如果我在ES集群的3个节点全部4个分片上共存储了400条人员信息数据,每个分片100条,接下来我要分页获取所有人员中【按年龄排序户籍地为乌拉特前旗】的每页10条的第3页的全部人员数据。也就是依据年龄去排序所有数据,然后取from为20,size为10的10条数据。
- size:显示应该返回的结果数量,默认是10。
- from:显示查询数据的偏移量,即应该跳过的初始结果数量,默认是0,我们这里取第三页的数据,则from应该设置为20
那么依照这样的需求我们请求发送到集群会怎么处理呢?处理流程如下【请求会被随机转发主分片或副本分片,采取随机轮询的方式,我们这里假定都是主分片处理】:
- 客户端发送请求到E-Node1节点,该节点成为协调节点,E-Node1上的【S2、S3】各建立一个大小为from+size(30)的优先级队列来存放查询结果;
- 协调节点将请求转发到所有分片上【主分片或副本分片,采取随机轮询的方式】,假设这里都是主分片处理请求,那么请求被转发到了E-Node2上的【S1】和E-Node3上的【S0】,它们各建立一个大小为from+size(30)的优先级队列来存放查询结果
- 每个shards在内部执行查询(搜索户籍地为乌拉特前旗,且按照年龄进行排序),把from+size(30)条记录存到内部的优先级队列(top N表)中
- 每个分片将自己的搜索结果(其实就是一些doc id),返回给协调节点
- query phase:E-Node1获取到各个shards数据后,进行合并排序,选择30*4共120条记录里的前 from + size 30条数据以及用于排序的 _score 存到优先级队列即可,以便 fetch 阶段使用。
- fetch phase:协调节点E-Node2获取到整体的top30后,取其中的第20-30条也就是第三页的数据中的10个doc id,根据doc id去各个节点上拉取实际的document数据,最终返回给客户端
这样一个数据量在这种场景下还是可以hold的,但是如果查询量比较大呢?假设我们每个分片上存储了10万条数据,共计40万条数据,我们要取第1万页的数据,也就是from为10000,size为10,那么我们再看一遍流程如下:
- client发送分页查询请求到E-Node1(coordinating node)上,E-Node1上的【S2、S3】各建立一个大小为from+size(10010)的优先级队列来存放查询结果;内存、IO损耗
- 协调节点将请求转发到E-Node2【S1】和E-Node3【S0】上,它们各建立一个大小为from+size(10010)的优先级队列来存放查询结果;内存、IO损耗
- 每个shards在内部执行查询(按照年龄进行排序),把from+size(10010)条记录存到内部的优先级队列(top N表)中;CPU损耗
- 每个shards把缓存的from+size(10010)条记录返回给ES1;网络带宽损耗
- query phase:E-Node1获取到各个shards数据后,进行合并排序,选择10010*4共40040条记录里的前 from + size 10010条数据以及用于排序的 _score 存到优先级队列即可,以便 fetch 阶段使用。CPU损耗
- fetch phase:协调节点E-Node1获取到整体的top10010后,取其中的第10001-10010条也就是第10000页的数据中的10个doc id,根据doc id去各个节点上拉取实际的document数据,最终返回给客户端
以上的各个阶段可以看到,当页码很深的时候,我们拿10条数据是多么的不容易,性能损耗是多么严重,所以ES对这种获取方式的数据条数做了限制:
[root@localhost elasticsearch-5.7.4]# curl -XGET 'http://11.12.84.126:9200/_audit_0102/_log_0102/_search?size=2&from=10000&pretty=true' { "error" : { "root_cause" : [ { "type" : "query_phase_execution_exception", "reason" : "Result window is too large, from + size must be less than or equal to: [10000] but was [10010]. See the scroll api for a more efficient way to request large data sets. This limit can be set by changing the [index.max_result_window] index level parameter." } ], "type" : "search_phase_execution_exception", "reason" : "all shards failed", "phase" : "query", "grouped" : true, "failed_shards" : [ { "shard" : 0, "index" : "_audit_0102", "node" : "f_CQitYESZedx8ZbyZ6bHA", "reason" : { "type" : "query_phase_execution_exception", "reason" : "Result window is too large, from + size must be less than or equal to: [10000] but was [10010]. See the scroll api for a more efficient way to request large data sets. This limit can be set by changing the [index.max_result_window] index level parameter." } } ] }, "status" : 500 }
from+size最多限制10000,超过限制即报错,当然这个参数可以通过如下的方式调整,例如调整到50000
curl -XPUT "http://11.12.84.126:9200/_audit_0102/_settings" -d '{ "index": { "max_result_window": 50000 } }'
但就算调整了也只是一种临时方案,硬件极限承载能力并不是通过调整配置能解决的,需要更换策略。从报错信息也可以看出,ES推荐使用Scroll的方式:
See the scroll api for a more efficient way to request large data sets
深度分页适用场景
Scroll 类似于sql中的cursor,使用scroll,每次只能获取一页的内容(按照上边的场景,每次获取10条),然后会返回一个scroll_id。根据返回的这个scroll_id可以不断地获取下一页的内容,所以scroll并不适用于有跳页的情景。Scroll的流程分为两个步骤
- 第一次搜索完成之后,将所有复合条件的搜索结果缓存起来,类似于对结果集做了一个快照;
- 在需要返回数据时,从该快照中按照scroll返回数据;在scroll快照生成之后,在快照有效期范围内,对于该索引的增删改都不会影响快照的结果。
以下是具体操作:
初始化
初始化的时候请求接口还需要index和type【6版本后没有了】信息,初始化的作用时将所有复合条件的搜索结果缓存起来,类似于对结果集做了一个快照,之后的搜索就是游标在快照上的滚动了。
GET UserInfo/_search?scroll=5m { "query": { "bool": { "filter": [ { "term": { "home": "乌拉特前旗" } } ] } }, "size": 10, "from": 0, "sort": [ { "age": { "order": "desc" }, "_id": { "order": "desc" } } ] }
其中:scroll=5m表示设置scroll_id保留5分钟可用;使用scroll必须要将from设置为0【不允许跳页】;size决定后面每次调用_search搜索返回的数量【这里为10条】。需要注意:实际返回给协调节点的数量为:分片的数量*size,也就是每次返回40条,共进行10001次请求和返回行为
搜索
然后我们可以通过数据返回的_scroll_id读取下一页内容,每次请求将会读取下10条数据,直到数据读取完毕或者scroll_id保留时间截止,请求的接口不再使用索引名了,而是 _search/scroll,其中GET和POST方法都可以使用
GET _search/scroll { "scroll_id": "DnF1ZXJ5VGhlbk【全网唯一Scrollid】", "scroll": "5m" }
需要注意:每次都要传参数 scroll,刷新搜索结果的缓存时间,相对于流程我们再来看一下请求过程:
- client发送分页查询请求到E-Node1(coordinating node)上E-Node1上的【S2、S3】各建立一个大小为from+size(10)的优先级队列来存放查询结果;内存无损耗、IO无损耗
- 协调节点将请求转发到ES2【S1】和ES3【S0】上,它们各建立一个大小为from+size(10)的优先级队列来存放查询结果;内存无损耗、IO无损耗
- 每个shards在内部执行查询(按照年龄进行排序),把from+size(10)条记录存到内部的优先级队列(top N表)中;CPU无损耗
- 每个shards把缓存的from+size(10)条记录返回给E-Node1;网络带宽无损耗
- query phase:E-Node1获取到各个shards数据后,发起下一次请求,直到轮询发起10001次请求,共获得40040条数据后执行合并排序,进行合并排序,选择10010*4共40040条记录里的前 from + size 10010条数据以及用于排序的 _score 存到优先级队列即可,以便 fetch 阶段使用。CPU无损耗、快照堆积
- fetch phase:协调节点E-Node1获取到整体的top100010后,取其中的第10001-10010条也就是第10000页的数据中的10个doc id,根据doc id去各个节点上拉取实际的document数据,最终返回给客户端
删除Scroll上下文快照
scroll的搜索上下文会在scroll的保留时间截止后自动清除,但是我们知道scroll是非常消耗资源的,所以一个建议就是当不需要scroll数据的时候,尽可能快的把scroll_id显式删除掉,因为每一个 scroll_id 不仅会占用大量的资源,而且会生成历史快照【在初始化请求时初始化数据快照,后续游标在快照上移动】,资源占用是很大的【5版本前scroll_id每次返回是变化的,5版本后就不变了】。又是一个经典的以空间换时间的例子
行文至此,已洋洋洒洒4万言,对ElasticSearch的原理和使用有了一个整体的认识,希望和大家共同进步。
