Elasticsearch这东西真没啥可说的,官方文档写的很详尽了,只不过是很多人英文水平比较弱,我说这话包括我自个儿哈,不过我硬着头皮是能看下去的,而且这东西也是支持SQL交互的,一般MySQL的写法在这里面也是兼容的,文档里也有写,不过写博客嘛,我就目前工作中碰到的查询语句,我会把SQL语言转化成同等的DSL查询,版本基于最新的
Elasticsearch 7.9.1
哈,大家仅供参考,官方文档才是唯一真理
不过今天9-30号,明天九点回家的火车。。。今晚得早睡觉了,国庆后我找个时间把这个博客写完,祝大家国庆玩儿的开心
首先准备一下前置数据,方便起见,Mapping
我就不创建了,演示的话我使用kibana
的 DevTools
来操作,没有这个软件的同学直接使用PostMan
即可
简要说明一下,后面出现的PUT /test
这种写法就是发送PUT
请求到 http://localhost:9200/test
这个URI
,因为本地的Elasticsearch默认启动在localhost:9200
,你们的Es如果不是本机地址的话换成相应的ES服务的地址和端口就可以了哈
- 创建一个名为
test
的索引(Mapping创建我直接省略掉了,想要了解的同学再去了解一下,这里不是重点)
PUT /test
{
"acknowledged" : true,
"shards_acknowledged" : true,
"index" : "test"
}
- 创建初始化的数据
POST /test/_doc
通过这个接口创建的数据,Elasticsearch会自动帮我们动态创建Mappings - 我用PHP Mock一些假数据,通过下面这个PHP脚本生成10000条测试数据,其中依赖了
fzaninotto/faker
这个扩展包
require_once __DIR__ . '/vendor/autoload.php';
$faker = Faker\Factory::create('zh_CN');
$client = new \GuzzleHttp\Client;
$data = [];
for ($i = 0; $i < 1000; $i++) {
$data = [
'name' => $faker->name,
'age' => $faker->numberBetween(15, 55),
'height' => $faker->numberBetween(150,190),
'weight' => $faker->numberBetween(80, 200), // 身高
'salary' => $faker->numberBetween(3, 50). 'K', // 薪水
'city' => $faker->city
];
$response = $client->request('POST','http://localhost:9200/test/_doc',[
'json' => $data,
'http_errors' => false,
]);
}
SQL查询
- 首先要了解一下Elasticsearch是支持SQL查询的,对应的查询端点是
GET /_sql
,注意哈,这个查询接口前面是没有索引名称的,下面写几个查询来演示一下
- 获取
height > 185
并且年龄在20-25
之间的所有用户可以这样书写SQL
{
"query" : "select * from test where height > 185 and age between 25 and 30"
}
查询结果如下:
columns
字段返回的是你查询的相应字段,rows
字段返回的是具体的匹配的数据
- 获取20-30岁男性平均身高最高的前十个城市,以及平均身高的值,并且过滤掉平均体重大于140的用户(相信这种一般就是我们工作中所需要统计的数据了)
{
"query" : "select city,avg(height),avg(weight) from test where age between 20 and 30 group by city having avg(weight) > 140 order by avg(height) desc limit 10"
}
查询结果如下
可以看到查询速度还是很快的,仅仅20ms,这样就得到了我们的数据
当然目前演示SQL都是跟MySQL比较类似的,Elasticsearch并没有做到完全像MySQL那样支持的,具体支持的函数不同的版本要参考不同的版本的Es的文档,这里给出 ElasticSearch SQL Access文档,查阅的时候注意去改一下Elasticsearch文档的地址,不同版本的Elasticsearch差别还是蛮大的,别你用7.6的Es你去看5.5的文档,很多东西都废掉掉了或者不建议使用了,白白浪费时间
DSL查询
刚才说的SQL查询,我相信大部分朋友熟悉任意一款支持SQL的数据库应用的话,都能够很快掌握,但是SQL的不足在于,他没有DSL查询强大,Elasticsearch提供的DSL查询可以提供完整的查询功能,比如我现在要计算一亿用户里面收入在1000-5000,5000-10000,10000-20000,20000-50000的人占总统计人口的比例,那问题就来了,可以写四个SQL,然后挨个算出所占比例,然后除以总基数,但是这样的话,查询就变成了串行,一个SQL耗时两秒,这四个就是八秒,Elasticsearch是提供了 _msearch的Api的,这个Api支持并发查询,他会在一个响应里面把你提交的所有请求的结果分为多个结果一次性返回给你,但是这个_msearch是不支持 sql访问的,另外,SQL也是不支持地理位置查询的,也没有文档评分的概念,在某些做文章或者社交类App的时候,一个文章(一条记录)索引到Elasticsearch里面可能被拆成N个词组,有的业务需要把部分词组的权重提高,以便用户搜索的时候优先出现含这个词组的相关记录,这种都是SQL不支持的,所以,个个人认为,复杂的业务查询我们尽量选用DSL来做,使代码有更好的扩展性,简单地业务代码来用SQL来处理就OK了,代码直观优先
- 还是以上面的两个SQL举例,我们转化为DSL查询如何来做
- 获取
height > 185
并且年龄在20-25
之间的所有用户可以这样书写DSL
GET /test/_search
{
"query": {
"bool": {
"must": [
{
"range": {
"height": {
"gt": 185
}
}
},
{
"range": {
"age": {
"lt": 30,
"gt": 25
}
}
}
]
}
}
}
简单观察一下,首先是请求的json
非常麻烦有没有,而且这个查询结果是等同于最上面的SQL查询的,但是查询的返回结果跟SQL返回的是不一致的,DSL查询返回的信息更加完全,便于我们排除错误,而且强调一点,DSL查询默认返回的记录条数是十条,如果想要返回更多的数据,我们需要设置页码大小,如下图
size
相当于是MySQL中的Limit,from相当于Offset,那么加了{ "size":2, "from":1}
也就是获取第2,第3条两条记录,from = 0
获取第一条数据
- 获取20-30岁男性平均身高最高的前十个城市,以及平均身高的值,并且过滤掉平均体重大于140的用户,同样的,写一下这个DSL查询
GET /test/_search
{
"query": {
"bool": {
"must": [
{
"range": {
"age": {
"gte": 20,
"lte": 30
}
}
}
]
}
},
"aggs":{
"groupByCity": {
"terms": {
"field": "city",
"size": 10,
"order": {
"avg_weight": "desc"
}
},
"aggs": {
"avg_height": {
"avg": {
"field": "height"
}
},
"avg_weight": {
"avg": {
"field": "weight"
}
},
"having": {
"bucket_selector": {
"buckets_path": {"w":"avg_weight"},
"script": "params.w > 140"
}
}
}
}
},
"size":0
}
注意,我在此处写的时候发生了一点问题,创建索引的时候没有指定字段类型,是插入数据的时候让ES帮我们生成的Mapping,City
字段ES把他设置为了text
类型,ES默认禁用text
类型字段来分组聚合,这里我把索引删掉,然后重新生成了一遍数据。得到结果如下
{
"took" : 0,
"timed_out" : false,
"_shards" : {
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 2255,
"relation" : "eq"
},
"max_score" : null,
"hits" : [ ]
},
"aggregations" : {
"groupByCity" : {
"doc_count_error_upper_bound" : -1,
"sum_other_doc_count" : 1577,
"buckets" : [
{
"key" : "昆明",
"doc_count" : 71,
"avg_height" : {
"value" : 171.57746478873239
},
"avg_weight" : {
"value" : 143.6056338028169
}
},
{
"key" : "北京",
"doc_count" : 69,
"avg_height" : {
"value" : 171.3768115942029
},
"avg_weight" : {
"value" : 142.1159420289855
}
},
{
"key" : "济南",
"doc_count" : 73,
"avg_height" : {
"value" : 171.05479452054794
},
"avg_weight" : {
"value" : 144.3972602739726
}
},
{
"key" : "西安",
"doc_count" : 60,
"avg_height" : {
"value" : 170.6
},
"avg_weight" : {
"value" : 147.08333333333334
}
},
{
"key" : "兰州",
"doc_count" : 65,
"avg_height" : {
"value" : 170.56923076923076
},
"avg_weight" : {
"value" : 143.8153846153846
}
}
]
}
}
}
hits
里面的数据不是我们需要的聚合数据的结果哈,Es每次查询的时候都会返回hits
的数据,如果不需要的话可以把size
置为0,就不返回了
实际我们需要的数据都在aggregations
里面,avg_height
字段下面的 value
就是平均身高,avg_weight
的value
则是平均体重
分页问题
这里简单说一下Elasticsearch的分页,Elasticsearch对分页支持并不友好,当我们使用from
和size
来对数据进行分页的时候,比如我要获取2000-2100条数据,Elasticsearch其实是把所有的数据全部获取出来了,然后截取了2000-2100条数据返给了你,那么当这个数据非常庞大以后,比如要获 1000000 - 10000010条数据的时候就会带来性能问题,所以ES自己也默认设置了最大分页数量,就是不允许超过10000,当然这个是可以在Setting
里面改掉的,真要有对于数据进行分页,更有甚者需要对于聚合后的数据进行分页的话,个人建议,定期把Elasticsearch里面的数据聚合统计后落到MySQL表里面持久化存储,而不要在ES里面直接查询,避免带来性能问题,目前公司做的商品订单统计的数据,每天二十万左右的订单,聚合统计后的数据也不过百十来条,落到MySQL非常方便,对于不是一个完整的自然日的情况,比如我要统计今天的数据,那么可以隔一段时间来查询一下ES,然后更新数据库的数据就OK了。
结语
写着写着发现真没什么好写的,官方文档很详尽,有需要的话对着自己ES版本去找找文档,大部分都有的,看不懂的话,粘贴出来谷歌翻译一下吧 Elasticsearch DSL查询,或者可以留言给我