原文作者:牛麦康纳
本篇主要学习DSL格式的ElasticSearch查询语法,了解Filter的作用,了解常用的聚合。
在开工之前我们需要强调一点,这也是我刚接触ES时进入的一个误区,虽然在某种程度上查询搜索ES与oracle、mysql等数据库有一些相似性,但是根本的区别是ES是个搜索引擎,他除开能过滤出我们想要的记录以外还增加了评分的能力,也就是“智能数据库”。了解这一点,才能方便我们领悟在搜索时什么时候用match,什么时候用filter。
回头看我们准备的数据样例:
{
"account_number":49,
"balance":29104,
"firstname":"Fulton",
"lastname":"Holt",
"age":23,
"gender":"F",
"address":"451 HumboldtStreet",
"employer":"Anocha",
"email":"fultonholt@anocha.com",
"city":"Sunriver",
"state":"RI"
}
首先这整个一条json属于一条document,document是我们搜索结果集返回的内容,我们在插入这条document时对它构建了反向索引。
其次记录被拆分后的每个属性是有差别的,ES将其拆分成String、number、IP、Date、boolean等类型,每个类型有个index属性,在做搜索匹配的时候我们处理的策略也是不同的。
Index属性有3个字典值analyzed、not_analyzed、no,分表代表着在反向索引时按分词来搜索、按原文来搜索、不能被搜索。其中String类型默认是analyzed,其他类型是not_analyzed。
先直接看一个例子了解下查询语句与返回内容(PS:这个例子性能上不是最优的,因为对state而言使用match和使用filter在效果上是一样的,但是策略和性能上是有去别的,后面有详解)
1、基本查询
请求如下:
GET my_index/customer/_search
{
"query": {
"match": {
"state": "UT"
}
},
"from": 10,
"size": 2,
"sort": [
{
"age": {
"order": "desc"
}
}
],
"_source": ["account_number","address","state","age"]
}
- query:与之相对的是filter,两者的区别后面会详细介绍。Match里是查询条件。
- size:代表结果取数返回记录数,像limit或rownum的作用。默认为10
- from:是标识从第几条记录开始取值。默认为0
- sort:标识按什么排序
- _source:标识返回集中的字段名,像select后的属性,默认是select*
响应如下:
{
"took": 30,
"timed_out": false,
"_shards": {
"total": 5,
"successful": 5,
"skipped": 0,
"failed": 0
},
"hits": {
"total": 20,
"max_score": null,
"hits": [
{
"_index": "my_index",
"_type": "customer",
"_id": "465",
"_score": null,
"_source": {
"account_number": 465,
"address": "916 Evergreen Avenue",
"state": "UT",
"age": 29
},
"sort": [
29
]
},
{
"_index": "my_index",
"_type": "customer",
"_id": "758",
"_score": null,
"_source": {
"account_number": 758,
"address": "149 Surf Avenue",
"state": "UT",
"age": 28
},
"sort": [
28
]
}
]
}
}
简单说返回内容包括2部分。
一部分是本次搜索的基本信息:
- Took:消耗的时间,单位是ms
- Shards:分片被检索的信息
- Hits.total:满足搜索条件的记录个数
- Hits.hits:返回结果集
另一部分是搜索结果集:
- Index:结果所在索引
- Type:结果所在类型
- Id:结果的ID
- Score:结果的评分(需要了解Lucene中df、tf的概念Lucene原理分析)
- Source:如前面所说select *里的内容
2、Match匹配:
包含分词的记录都会被列出,可以同时匹配多个条件,同一属性中多个分词满足条件是or的关系用空格隔开,评分越高标识越趋近于客户想要的答案。
此外,match还有几个变种(match_all、match_phrase、match_phrase_prefix等)就不一一介绍了,用到时候自己再翻翻文档。
3、Bool 匹配
跟关系型数据库的 where 里的一个过滤条件类似, bool 里的结果只有 true 和 false ,必须为 true 时才满足过滤条件。
GET my_index/customer/_search
{
"query": {
"bool":{
"must": [
{"match": {
"age": "20"
}},
{"match": {
"address": "Street"
}}
],
"should": [
{"match": {
"firstname": "Bean"
}},
{"match": {
"lastname": "Valenzuela"
}}
]
}
},
"from": 0,
"size": 10
}
Bool里可以有must、must_not、should、以及他们的组合:
- must表示条件必须全部满足
- must_not表示条件必须全部不能满足
- should标识条件只要满足部分
5、Filter:
过滤条件,我们进行搜索前直接去索引中通过某些不参与打分的条件过滤掉一些记录(时间、数值范围、精准字符等),以减少搜索范围提高后续搜索索引的效率,filter结果会被缓存。
回到我这个例子,其实只有email和address进行搜索时有打分的必要,因为只有针对他们进行索引才有匹配和更匹配的区分,其它浪费性能去打分没啥意义。
我要搜索年龄在 20-25 之间,余额在 20000-35000 之间,地址包含 Street ,或者邮箱包含 schultzmoreno 的这样的记录。
GET my_index/customer/_search
{
"query": {
"bool":{
"should": [
{"match": {
"address": "Street"
}},
{"match": {
"email": "schultzmoreno"
}}
],
"filter": {
"range": {
"age": {
"gte": 20,
"lte": 25
}
}
},
"filter": {
"range": {
"balance": {
"gte": 20000,
"lte": 35000
}
}
}
}
},
"from": 0,
"size": 10
}
从结果中我们可以看到,ES推送给我们评分最高的这条记录是匹配度最高的。
Elasticsearch具有称为聚合的功能,允许您对数据生成复杂的分组和分析,像是Oracle中的group by、avg等,而且更强大。
6、Metric聚合
先看运算,跟Oracle类似有sum、max、min、avg、count等,在ES中可以单独算某一种运算外,还提供了一个stats参数,一次请求把以上所有结果都返回出来。
请求:
GET my_index/customer/_search
{
"aggs": {
"my_name": {
"stats": {
"field": "age"
}
}
},
"size": 0
}
- Aggs:代表是启用聚合功能了,固定套路。
- My_name:给聚合返回的结果集起一个别名,单独看不出意义,嵌套聚合的时候体现价值。
- Stats:聚合的操作命令,这里是统计的命令,可以换成sum、max、min、avg、term等各种各样ES内置的命令。
- Field:指定被聚合的属性名。
- Size:指定结果集中要返回多少条被本次聚合命中的document,如果只关心聚合结果不关心命中的记录,size请指定为0。
返回:
{
"took": 24,
"timed_out": false,
"_shards": {
"total": 5,
"successful": 5,
"skipped": 0,
"failed": 0
},
"hits": {
"total": 1000,
"max_score": 0,
"hits": []
},
"aggregations": {
"my_name": {
"count": 1000,
"min": 20,
"max": 40,
"avg": 30.171,
"sum": 30171
}
}
}
Bucket聚合
桶子的意思,根据条件把数据按照木桶封装好,有点Oracle中group by的意思,理解这个脑子里得有点空间想象的能力。
请求:我要按照年龄把文档按桶子分分类。
GET my_index/customer/_search
{
"aggs": {
"my_name": {
"terms": {
"field": "age"
}
}
},
"size": 0
}
返回:
{
"took": 92,
"timed_out": false,
"_shards": {
"total": 5,
"successful": 5,
"skipped": 0,
"failed": 0
},
"hits": {
"total": 1000,
"max_score": 0,
"hits": []
},
"aggregations": {
"my_name": {
"doc_count_error_upper_bound": 0,
"sum_other_doc_count": 463,
"buckets": [
{
"key": 31,
"doc_count": 61
},
{
"key": 39,
"doc_count": 60
},
{
"key": 26,
"doc_count": 59
},
{
"key": 32,
"doc_count": 52
},
{
"key": 35,
"doc_count": 52
},
{
"key": 36,
"doc_count": 52
},
{
"key": 22,
"doc_count": 51
},
{
"key": 28,
"doc_count": 51
},
{
"key": 33,
"doc_count": 50
},
{
"key": 34,
"doc_count": 49
}
]
}
}
}
每个年龄都有一桶记录在里面,默认是按照桶里数据多少来排序的,可以在 field 后添加 "order" : { "_term" : "asc"} 指定按照内容来排序。
还可以与前面的运算嵌套使用,我想算一下每个年龄存款的平均水平:
GET my_index/customer/_search
{
"aggs": {
"my_name": {
"terms": {
"field": "age",
"order": {
"avg_balance": "desc"
}
},
"aggs": {
"avg_balance": {
"avg": {
"field": "balance"
}
}
}
}
},
"size": 0
}
最后我们用一个稍微复杂些的例子总结下本篇所有内容:
搜索住在UT州的,家庭地址里有“Street”这个关键字的,每个年龄的平均存款
GET my_index/customer/_search
{
"query": {
"bool": {
"must": [
{"match": {
"address": "Street"
}},
{"term": {
"state": {
"value": "ut"
}
}}
]
}
},
"aggs": {
"myresult": {
"terms": {
"field": "age",
"order": {
"avg_balance": "desc"
},
"missing": 0
},
"aggs": {
"avg_balance": {
"avg": {
"field": "balance"
}
}
}
}
},
"size": 0
}
这里有个细节,UT州我用的是小写ut,换成大写反而查不出记录,这还是跟Lucene和filter的原理有关的。Lucene在做反向索引的时候会对分词做转化,对于英语在索引里保存的都是小写的单词,在搜索时也用同样的处理方式将搜索关键字转化为小写以保证大小写都可以搜索得到。而语句中的term是属于filter的一种使用,而filter前面介绍过是直接省略关键字处理直接去索引里通过完全匹配进行过滤的。
这里又涉及到 filter 使用的一个技巧,虽然缓存有缓存的优势,但也不是有用没有的都要往缓存里丢,毕竟无意义的缓存还会涉及到缓存清理、缓存利用率不高等问题。一般像字典值、省市、年龄、颜色等我们尽量用缓存,家庭地址、身份证号此类差异性巨大的内容就没有缓存的必要。