elasticsearch从入门到删库跑路

0x00 前言

我觉着我干的事情一直比较杂,最近弄了一些 Elasticsearch 相关的事情,我本身并不是做服务端或者大数据那一类的,但是这里仍然因为工作上的原因使用了一些es,因此这里还是整理一下,目前常用的es相关知识,至少是我这个初学者比较容易遇到的一些操作,以及踩到的一些坑。有些信息出自官方文档,或者其他的blog,人类的本质果然是复读机。。很多blog不知道到底上哪儿复制的,有些东西感觉都不太对。,也可能是我版本的问题吧,这里我使用的版本是6.2.2,kibana同版本的嗷。

0x01 Elasticsearch 介绍

又到了喜闻乐见的复制粘贴介绍环节

Elasticsearch是高度可伸缩的开源全文搜索和分析引擎。它允许我们快速实时地存储、搜索、分析大数据。
Elasticsearch使用Lucene作为内部引擎,但是在你使用它做全文搜索时,只需要使用统一开发好的API即可,而不需要了解其背后复杂的Lucene的运行原理。它的目的是通过简单的RESTful API来隐藏Lucene的复杂性,从而让全文搜索变得简单。

0x02 常 规 操 作

建立索引

这一步我的理解其实比较像数据库的建表,需要额外注意的部分是,对嵌套字段的处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
PUT 索引名称(自定义)
{
"mappings":{
"type(自定义,类型名称)":{
"properties": {
下面可以列出各个字段的名称和类型,也可以不列出来。没有列出来的字段会在上传的时候自动建立
这里需要注意的是,如果说涉及到的嵌套字段的话,最好使用 nested这种类型来指定,像下面这样
关于嵌套字段的相关知识一会儿在下面说
"location":{
"type":"nested"
}
}
}
}
}

es相关的字段类型参考:
https://www.elastic.co/guide/en/elasticsearch/reference/6.2/mapping-types.html

在建立字段时,必须考虑好各个字段的类型,因为这个东西一旦建表之后就不能修改了,实在要改的话,只能重新建表了。(注意:这里删除字段再重新建也不行,嗨呀在这上面吃过亏好气啊)

字段查询相关的语句

  1. 查询所有数据
    1
    2
    3
    4
    5
    6
    7
    8
    POST index_name(自定义,索引名称)/_search
    {
    "query": {
    "match_all":{}
    },
    "from":1, 查询时的起始位置
    "size":1 查询时需要数据的大小,有默认值的
    }

这里通过from和size其实并不能查询到整个库的所有数据,如果需要查询所有的数据,或者是from+size大于10000的数据,需要使用滚动查询,后面会提这个东西,这里具体的原因似乎和分页一类的有关系,是es本身的一种限制,没有深究了。

  1. 查询某个字段是否为空

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    POST index_name(自定义,索引名称)/_search
    {
    "query": {
    "bool": {
    "must_not": {
    "exists": {
    "field": "字段名"
    }
    }
    }
    }
    }
  2. 查询某个字段的精确值

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    POST index_name(自定义,索引名称)/_search
    "query":{
    "bool": {
    "must": [
    {
    "match": {
    "字段名称.keyword": "需要查找的精确值"
    }
    }
    ]
    }
    }

更新&删除字段

比如索引中存在一个tag字段,我现在想将name为a的数据的tag字段都改成x

1
2
3
4
5
6
7
8
9
10
11
12
13
POST index_name(自定义,索引名称)/type(自定义,类型名称)/_update_by_query
{
"query":{
"terms":{
"name.keyword":[ 这里可以填入多个字段的值,keyword的作用后面再讲
"a"
]
}
},
"script":{
"source":"ctx._source['tag']='x';"
}
}

比如现在想删除所有存在name字段的数据中的name字段本身

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
删除字段
POST index_name(自定义,索引名称)/type(自定义,类型名称)/_update_by_query
{
"script":"ctx._source.remove(\"name\")",
"query":{
"bool":{
"must":[
{
"exists":{
"field":"name"
}
}
]
}
}
}

精确值删除

例如现在需要删除所有name为test的值

1
2
3
4
5
6
7
8
POST index_name(自定义,索引名称)/_delete_by_query
{
"query": {
"match": {
"name": "测试删除"
}
}
}

区间查询

这里举两个例子,时间区间查询,以及ip段区间查询
时间查询的话,需要给时间指定格式,或者本身在建立索引的时候,指定上格式一类的信息,
这里是查询time字段范围在2019-11-10 10:00:00至2019-11-20 10:00:00之间的语句

1
2
3
4
5
6
7
8
9
10
11
12
POST index_name(自定义,索引名称)/_search
{
"query": {
"range": {
"time.keyword": {
"gte":"2019-11-10 10:00:00", gte为大于等于 gt为大于
"lte":"2019-11-20 10:00:00", lte为小于或等于 lt为小于
"format": "yyyy-MM-dd HH:mm:ss"
}
}
}
}

ip段查询,查询192.168.1.0至192.168.1.255这个ip段的数据,这里ip字段本身最好也是ip类型的,但是实际操作的时候发现它可以是字符串类型

1
2
3
4
5
6
7
8
9
10
11
POST index_name(自定义,索引名称)/_search
{
"query":{
"range":{
"ip":{
"gte":"192.168.1.0",
"lte":"192.168.1.255"
}
}
}
}

聚合查询

聚合查询可以用来统计某个字段的数量等等一类的情况,
例如,统计所有年龄出现的次数

1
2
3
4
5
6
7
8
9
10
11
12
13
GET index_name(自定义,索引名称)/type(自定义,类型名称)/_search
{
"size":0, 这里其实还是会进行一次所有数据量的查询,并展示结果,为了方便看后面聚合的数据的结果,所以这里给设置成了0
如果需要加上其他限制条件,例如name为test的age数据聚合计算,可以在聚合前先进行query查询相关字段的数据
"aggs":{
"test(这里可以自定义一个名称)":{
"terms":{
"field":"age",
"size":10 这里指定显示聚合结果的条目数量
}
}
}
}

或者计算所有不同的age字段的数量,举例来说,比如数据库中age字段的值只有10,20,30三种,那这里算出来的结果就是3

1
2
3
4
5
6
7
8
9
10
11
GET index_name(自定义,索引名称)/type(自定义,类型名称)/_search
{
"size":0,
"aggs":{
"test(这里可以自定义一个名称)":{
"cardinality":{
"field":"age"
}
}
}
}

0x03 一些遇到的坑

嵌套类型

例如数据中有类似这样的情况

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
{
"title": "Nest eggs",
"body": "Making your money work...",
"tags": [ "cash", "shares" ],
"comments": [
{
"name": "John Smith",
"comment": "Great article",
"age": 28,
"stars": 4,
"date": "2014-09-01"
},
{
"name": "Alice White",
"comment": "More like this please",
"age": 31,
"stars": 5,
"date": "2014-10-22"
}
]
}

在存储的时候其实这个结构是扁平的

1
2
3
4
5
6
7
8
9
10
{
"title": [ eggs, nest ],
"body": [ making, money, work, your ],
"tags": [ cash, shares ],
"comments.name": [ alice, john, smith, white ],
"comments.comment": [ article, great, like, more, please, this ],
"comments.age": [ 28, 31 ],
"comments.stars": [ 4, 5 ],
"comments.date": [ 2014-09-01, 2014-10-22 ]
}

这样就很明显会导致一个问题,当我想查询name为Alice,age为28的数据的时候,这条数据也是符合要求的,但是这明显不是查询的时候所期望的数据,这个时候就需要使用到嵌套类型了,这个需要在索引建立的时候就得设置好,回头看一下索引建立的部分,如果在这里的话应该这样写:

1
2
3
4
5
6
7
8
9
10
11
12
13
PUT 索引名称(自定义)
{
"mappings":{
"index_name(自定义,索引名称)":{
"properties": {
指定comments为嵌套类型,这样才能在组合查询嵌套中多个字段的时候,获取到正确的数据
"comments":{
"type":"nested"
}
}
}
}
}

另外值得一提的是,当指定了嵌套类型之后,kibana在搜索框查询这个嵌套字段的时候就不好使了,查询结果会是空的。这里原因未知,我用的是6.2.2的版本,当时一直以为自己设置嵌套的时候设置错了,在kibana中一直查不出来,但是使用api或者手动敲查询命令的话,都是可以的

嵌套类型的查询

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
POST index_name(自定义,索引名称)/_search
{
"query": {
"nested": {
"path": "comments", 这里需要先指定嵌套的字段路径
"query": {
"bool": {
"must": [
{
"match": {
"comments.age": 28
}
}
]
}
}
}
}
}

keyword的作用

keyword这个东西和分词有一些关系,在建立es索引后,可以在kibana的索引信息查看页面看到很多字符串类型的字段都有会有相关的keyword字段,这里当对这些字段进行查询的时候,不使用keyword的情况下,查询的是被分词的字段,例如上文中的 “name”:”Alice White” 存入es后如果直接使用name字段查询Alice White是查不出来的,可以使用Alice或White进行查询,如果想要精确的查询到”Alice White”,那查询的字段就需要是name.keyword

滚动查询

上文中提到了查询的时候使用from和size,无法查询到10000以后的数据,但是实际的使用场景下肯定是会有查询10000以后数据的情况的,这个是时候就需要使用滚动查询了,使用方法也比较简单,缺点的话,可能就是没有from和size那样方便控制起始位置了

1
2
3
4
5
6
7
POST index_name(自定义,索引名称)/_search?scroll=1m  设置滚动窗口的时间为一分钟
{
"query": {
"match_all":{}
},
"size": 100 设置滚动一次查询的数据量
}

在执行一次这个查询之后,返回的数据中会有一个_scroll_id,后续的查询就可以直接通过这个id了

1
2
3
4
5
POST /_search/scroll
{
"scroll":"1m",
"scroll_id":"获得到的_scroll_id"
}

在这个窗口期内就可以通过反复执行这个scroll查询,读取到整个数据库的数据,这里的_scroll_id,我这儿试的时候,发现它在第一次滑动查询之后就不变了,在这个窗口期内可以反复使用,每次查询都会返回后续的数据

0x04 快照备份与恢复

这里简单提一下备份吧,最近在做这个,网上查了一下信息做参考,也踩到一点儿坑
es支持多种备份的方式

  • 共享的文件系统,如NAS
  • Amazon S3
  • HDFS (Hadoop Distributed File System)
  • Azure Cloud
    这里我用的是第一种

注册快照仓库

1
2
3
4
5
6
7
PUT  _snapshot/my_backup  最后这里是快照仓库名称
{
"type": "fs",
"settings": {
"location": "/data/backups/elasticsearch"
}
}

“/data/backups/elasticsearch” 这个目录建立的时候需要注意es的用户和组必须有它的读写权限,并且需要在es配置文件中配置上

1
path.repo: /data/backups/elasticsearch

这个es配置文件名字是 elasticsearch.yml 路径为 /etc/elasticsearch/elasticsearch.yml,在修改配置之后,需要重启es服务。

进行备份

如果想要等到备份完成可以在后面加上 ?wait_for_completion=true,这里具体看使用情况吧,我在kibana中使用这个的时候导致了超时错误,我觉着没必要阻塞在这儿,可以用下面的请求来查看状态,当然这里还是看场景。

1
2
3
4
PUT _snapshot/my_backup/snapshot_name  最后这里是快照名称
{
"indices": "index_1,index_2" 指定需要备份的索引
}

备份过程中查看状态

1
GET _snapshot/my_backup/snapshot_name/_status

主要由如下几种状态:

  • INITIALIZING 集群状态检查,检查当前集群是否可以做快照,通常这个过程会非常快
  • STARTED 正在转移数据到仓库
  • FINALIZING 数据转移完成,正在转移元信息
  • DONE 完成
  • FAILED 备份失败

取消备份

1
DELETE _snapshot/my_backup/snapshot_name

恢复备份

1
POST _snapshot/my_backup/snapshot_name/_restore

指定恢复目标索引

1
2
3
4
5
6
POST _snapshot/my_backup/snapshot_name/_restore
{
"indices": "index_1",
"rename_pattern": "index_(.+)",
"rename_replacement": "restored_index_$1"
}

上面的indices, 表示只恢复索引’index_1’
renamepattern: 表示重命名索引以’index‘开头的索引.
rename_replacement: 表示将所有的索引重命名为’restored_index_xxx’.如index_1会被重命名为restored_index_1.

如果指定了索引,这里第二项就可以直接写索引的名称,不用再加上(.+)这种通配符一类的东西,且最后的$1也可以不用,rename_replacement名字可以直接指定

0x05 删库跑路

这里我说的删库其实是删除索引,操作十分的简单。。

1
DELETE index_name(自定义,索引名称)

说归说,不要乱来嗷,你看我都是在备份之后才提这玩意儿的(手动滑稽)

0x06 总结&其他的事情

es的常规操作其实网上到处都能找到,但是我用数据一类的东西比较少,有些东西其实是第一次遇到,不知道是不是es本身的特性,例如keyword分词,嵌套扁平化,无法直接查10000之后的数据只能滚动查询等等。今天看的备份,差不多来回折腾以及和提供es服务那边部门的同事聊了一会儿,es那个备份,不论什么方式似乎都是基于NFS的,通过挂载远程文件系统,备份完再摘掉啥的,在我们公司似乎因为一些安全上的限制,这个玩意不太好整。溜了溜了,好菜啊,辣鸡mac又开始发烫了。

0x07 参考

  1. https://www.elastic.co/guide/cn/elasticsearch/guide/current/nested-objects.html
  2. https://ox0spy.github.io/post/elasticsearch/elasticsearch-snapshot-restore/