ElasticSearch常用操作:查询与聚合篇
[TOC]
0 说明
基于es 5.4和es 5.6,列举的是个人工作中经常用到的查询(只是工作中使用的是Java API),如果需要看完整的,可以参考官方相关文档
https://www.elastic.co/guide/en/elasticsearch/reference/5.4/search.html。
1 查询
先使用一个快速入门来引入,然后后面列出的各种查询都是用得比较多的(在我的工作环境是这样),其它没怎么用的这里就不列出了。
1.1 快速入门
1.1.1 查询全部
GET index/type/_search{ "query":{ "match_all":{} }}
或
GET index/type/_search
1.1.2 分页(以term为例)
GET index/type/_search{ "from":0, "size":100, "query":{ "term":{ "area":"GuangZhou" } }}
1.1.3 包含指定字段(以term为例)
GET index/type/_search{ "_source":["hobby", "name"], "query":{ "term":{ "area":"GuangZhou" } }}
1.1.4 排序(以term为例)
单个字段排序:
GET index/type/_search{ "query":{ "term":{ "area":"GuangZhou" } }, "sort":[ {"user_id":{"order":"asc"}}, {"salary":{"order":"desc"}} ]}
1.2 全文查询
查询字段会被索引和分析,在执行之前将每个字段的分词器(或搜索分词器)应用于查询字符串。
1.2.1 match query
{ "query": { "match": { "content": { "query": "里皮恒大", "operator": "and" } } }}
operator默认是or,也就是说,"里皮恒大"被分词为"里皮"和"恒大",只要content中出现两个之一,都会搜索到;设置为and之后,只有同时出现都会被搜索到。
1.2.2 match_phrase query
文档同时满足下面两个条件才会被搜索到:
- (1)分词后所有词项都要出现在该字段中
- (2)字段中的词项顺序要一致
{ "query": { "match_phrase": { "content": "里皮恒大" } }}
1.3 词项查询
词项搜索时对倒排索引中存储的词项进行精确匹配,词项级别的查询通过用于结构化数据,如数字、日期和枚举类型。
1.3.1 term query
{ "query": { "term": { "postdate": "2015-12-10 00:41:00" } }}
1.3.2 terms query
term的升级版,如上面查询的postdate字段,可以设置多个。
{ "query": { "terms": { "postdate": [ "2015-12-10 00:41:00", "2016-02-01 01:39:00" ] } }}
因为term是精确匹配,所以不要问,[]中的关系怎么设置and?这怎么可能,既然是精确匹配,一个字段也不可能有两个不同的值。
1.3.3 range query
匹配某一范围内的数据型、日期类型或者字符串型字段的文档,注意只能查询一个字段,不能作用在多个字段上。
数值:
{ "query": { "range": { "reply": { "gte": 245, "lte": 250 } } }}
支持的操作符如下:
gt:大于,gte:大于等于,lt:小于,lte:小于等于
日期:
{ "query": { "range": { "postdate": { "gte": "2016-09-01 00:00:00", "lte": "2016-09-30 23:59:59", "format": "yyyy-MM-dd HH:mm:ss" } } }}
format不加也行,如果写的时间格式正确。
1.3.4 exists query
返回对应字段中至少有一个非空值的文档,也就是说,该字段有值(待会会说明这个概念)。
{ "query": { "exists": { "field": "user" } }}
参考《从Lucene到Elasticsearch:全文检索实战》中的说明。
以下文档会匹配上面的查询:
文档 | 说明 |
---|---|
{"user":"jane"} | 有user字段,且不为空 |
{"user":""} | 有user字段,值为空字符串 |
{"user":"-"} | 有user字段,值不为空 |
{"user":["jane"]} | 有user字段,值不为空 |
{"user":["jane",null]} | 有user字段,至少一个值不为空即可 |
下面的文档不会被匹配:
文档 | 说明 |
---|---|
{"user":null} | 虽然有user字段,但是值为空 |
{"user":[]} | 虽然有user字段,但是值为空 |
{"user":[null]} | 虽然有user字段,但是值为空 |
{"foo":"bar"} | 没有user字段 |
1.3.5 ids query
查询具有指定id的文档。
{ "query": { "ids": { "type": "news", "values": "2101" } }}
类型是可选的,也可以以数据的方式指定多个id。
{ "query": { "ids": { "values": [ "2101", "2301" ] } }}
1.4 复合查询
1.4.1 bool query
因为工作中接触到关于es是做聚合、统计、分类的项目,经常要做各种复杂的多条件查询,所以实际上,bool query用得非常多,因为查询条件个数不定,所以处理的逻辑思路时,外层用一个大的bool query来进行承载。(当然,项目中是使用其Java API)
bool query可以组合任意多个简单查询,各个简单查询之间的逻辑表示如下:
属性 | 说明 |
---|---|
must | 文档必须匹配must选项下的查询条件,相当于逻辑运算的AND |
should | 文档可以匹配should选项下的查询条件,也可以不匹配,相当于逻辑运算的OR |
must_not | 与must相反,匹配该选项下的查询条件的文档不会被返回 |
filter | 和must一样,匹配filter选项下的查询条件的文档才会被返回,但是filter不评分,只起到过滤功能 |
一个例子如下:
{ "query": { "bool": { "must": { "match": { "content": "里皮" } }, "must_not": { "match": { "content": "中超" } } } }}
需要注意的是,同一个bool下,只能有一个must、must_not、should和filter。
如果希望有多个must时,比如希望同时匹配"里皮"和"中超",但是又故意分开这两个关键词(因为事实上,一个must,然后使用match,并且operator为and就可以达到目的),怎么操作?注意must下使用数组,然后里面多个match对象就可以了:
{ "size": 1, "query": { "bool": { "must": [ { "match": { "content": "里皮" } }, { "match": { "content": "恒大" } } ] } }, "sort": [ { "id": { "order": "desc" } } ]}
当然must下的数组也可以是多个bool查询条件,以进行更加复杂的查询。
上面的查询等价于:
{ "query": { "bool": { "must": { "match": { "content": { "query": "里皮恒大", "operator": "and" } } } } }, "sort": [ { "id": { "order": "desc" } } ]}
1.5 嵌套查询
先添加下面一个索引:
PUT /my_index{ "mappings": { "my_type": { "properties": { "user":{ "type": "nested", "properties": { "first":{"type":"keyword"}, "last":{"type":"keyword"} } }, "group":{ "type": "keyword" } } } }}
添加数据:
PUT my_index/my_type/1{ "group":"GuangZhou", "user":[ { "first":"John", "last":"Smith" }, { "first":"Alice", "last":"White" } ]}PUT my_index/my_type/2{ "group":"QingYuan", "user":[ { "first":"Li", "last":"Wang" }, { "first":"Yonghao", "last":"Ye" } ]}
查询:
较简单的查询:
{ "query": { "nested": { "path": "user", "query": { "term": { "user.first": "John" } } } }}
较复杂的查询:
{ "query": { "bool": { "must": [ {"nested": { "path": "user", "query": { "term": { "user.first": { "value": "Li" } } } }}, { "nested": { "path": "user", "query": { "term": { "user.last": { "value": "Wang" } } } } } ] } }}
1.6 补充:数组查询与测试
添加一个索引:
PUT my_index2{ "mappings": { "my_type2":{ "properties": { "message":{ "type": "text" }, "keywords":{ "type": "keyword" } } } }}
添加数据:
PUT /my_index2/my_type/1{ "message":"keywords test1", "keywords":["美女","动漫","电影"]}PUT /my_index2/my_type/2{ "message":"keywords test2", "keywords":["电影","美妆","广告"]}
搜索:
{ "query": { "term": { "keywords": "广告" } }}
Note1:注意设置字段类型时,keywords设置为keyword,所以使用term查询可以精确匹配,但设置为text,则不一定--如果有添加分词器,则可以搜索到;如果没有,而是使用默认的分词器,只是将其分为一个一个的字,就不会被搜索到。这点尤其需要注意到。
Note2:对于数组字段,也是可以做桶聚合的,做桶聚合的时候,其每一个值都会作为一个值去进行分组,而不是整个数组进行分组,可以使用上面的进行测试,不过需要注意的是,其字段类型不能为text,否则聚合会失败。
Note3:所以根据上面的提示,一般纯数组比较适合存放标签类的数据,就像上面的案例一样,同时字段类型设置为keyword,而不是text,搜索时进行精确匹配就好了。
1.7 滚动查询scroll
如果一次性要查出来比如10万条数据,那么性能会很差,此时一般会采取用scoll滚动查询,一批一批的查,直到所有数据都查询完处理完(es返回的scrollId,可以理解为是es进行此次查询的操作句柄标识,每发送一次该scrollId,es都会操作一次,或者说循环一次,直到时间窗口到期)。
使用scoll滚动搜索,可以先搜索一批数据,然后下次再搜索一批数据,以此类推,直到搜索出全部的数据来,scoll搜索会在第一次搜索的时候,保存一个当时的视图快照,之后只会基于该旧的视图快照提供数据搜索,如果这个期间数据变更,是不会让用户看到的,每次发送scroll请求,我们还需要指定一个scoll参数,指定一个时间窗口,每次搜索请求只要在这个时间窗口内能完成就可以了(也就是说,该scrollId只在这个时间窗口内有效,视图快照也是)。
GET spnews/news/_search?scroll=1m{ "query": { "match_all": {} }, "size": 10, "_source": ["id"]}GET _search/scroll{ "scroll":"1m", "scroll_id":"DnF1ZXJ5VGhlbkZldGNoAwAAAAAAADShFmpBMjJJY2F2U242RFU5UlAzUzA4MWcAAAAAAAA0oBZqQTIySWNhdlNuNkRVOVJQM1MwODFnAAAAAAAANJ8WakEyMkljYXZTbjZEVTlSUDNTMDgxZw=="}
2 聚合
2.1 指标聚合
相当于MySQL的聚合函数。
max
{ "size": 0, "aggs": { "max_id": { "max": { "field": "id" } } }}
size不设置为0,除了返回聚合结果外,还会返回其它所有的数据。
min
{ "size": 0, "aggs": { "min_id": { "min": { "field": "id" } } }}
avg
{ "size": 0, "aggs": { "avg_id": { "avg": { "field": "id" } } }}
sum
{ "size": 0, "aggs": { "sum_id": { "sum": { "field": "id" } } }}
stats
{ "size": 0, "aggs": { "stats_id": { "stats": { "field": "id" } } }}
2.2 桶聚合
相当于MySQL的group by操作,所以不要尝试对es中text的字段进行桶聚合,否则会失败。
Terms
相当于分组查询,根据字段做聚合。
{ "size": 0, "aggs": { "per_count": { "terms": { "size":100, "field": "vtype", "min_doc_count":1 } } }}
在桶聚合的过程中还可以进行指标聚合,相当于mysql做group by之后,再做各种max、min、avg、sum、stats之类的:
{ "size": 0, "aggs": { "per_count": { "terms": { "field": "vtype" }, "aggs": { "stats_follower": { "stats": { "field": "realFollowerCount" } } } } }}
Filter
相当于是MySQL根据where条件过滤出结果,然后再做各种max、min、avg、sum、stats操作。
{ "size": 0, "aggs": { "gender_1_follower": { "filter": { "term": { "gender": 1 } }, "aggs": { "stats_follower": { "stats": { "field": "realFollowerCount" } } } } }}
上面的聚合操作相当于是:查询gender为1的各个指标。
Filters
在Filter的基础上,可以查询多个字段各自独立的各个指标,即对每个查询结果分别做指标聚合。
{ "size": 0, "aggs": { "gender_1_2_follower": { "filters": { "filters": [ { "term": { "gender": 1 } }, { "term": { "gender": 2 } } ] }, "aggs": { "stats_follower": { "stats": { "field": "realFollowerCount" } } } } }}
Range
{ "size": 0, "aggs": { "follower_ranges": { "range": { "field": "realFollowerCount", "ranges": [ { "to": 500 }, { "from": 500, "to": 1000 }, { "from": 1000, "to": 1500 }, { "from": "1500", "to": 2000 }, { "from": 2000 } ] } } }}
to:小于,from:大于等于
Date Range
跟上面一个类似的,其实只是字段为日期类型的,然后范围值也是日期。
Date Histogram Aggregation
这个功能十分有用,可以根据年月日来对数据进行分类。
索引下面的文档:
DELETE my_blogPUT my_blog{ "mappings": { "article":{ "properties": { "title":{"type": "text"}, "postdate":{ "type": "date" , "format": "yyyy-MM-dd HH:mm:ss" } } } }}PUT my_blog/article/1{ "title":"Elasticsearch in Action", "postdate":"2014-09-23 23:34:12"}PUT my_blog/article/2{ "title":"Spark in Action", "postdate":"2015-09-13 14:12:22"}PUT my_blog/article/3{ "title":"Hadoop in Action", "postdate":"2016-08-23 23:12:22"}
按年对数据进行聚合:
GET my_blog/article/_search{ "size": 0, "aggs": { "agg_year": { "date_histogram": { "field": "postdate", "interval": "year", "order": { "_key": "asc" } } } }}{ "took": 18, "timed_out": false, "_shards": { "total": 5, "successful": 5, "failed": 0 }, "hits": { "total": 3, "max_score": 0, "hits": [] }, "aggregations": { "agg_year": { "buckets": [ { "key_as_string": "2014-01-01 00:00:00", "key": 1388534400000, "doc_count": 1 }, { "key_as_string": "2015-01-01 00:00:00", "key": 1420070400000, "doc_count": 1 }, { "key_as_string": "2016-01-01 00:00:00", "key": 1451606400000, "doc_count": 1 } ] } }}
按月对数据进行聚合:
GET my_blog/article/_search{ "size": 0, "aggs": { "agg_year": { "date_histogram": { "field": "postdate", "interval": "month", "order": { "_key": "asc" } } } }}
这样聚合的话,包含的年份的每一个月的数据都会被分类,不管其是否包含文档。
按日对数据进行聚合:
GET my_blog/article/_search{ "size": 0, "aggs": { "agg_year": { "date_histogram": { "field": "postdate", "interval": "day", "order": { "_key": "asc" } } } }}
这样聚合的话,包含的年份的每一个月的每一天的数据都会被分类,不管其是否包含文档。