短时间上手,ES实战。新手接触新东西,最重要的是理清学习路径,这里的目录,就是我个人的简化路径。深入还需要再多投入时间精力,慢慢研究才能做到深钻,本文目标是「快速上手」。
喜欢通过视频学习的同学可以看慕课:ElasticSearch入门。
推荐:
- Elasticsearch 是一个分布式、可扩展、实时的搜索与数据分析引擎。 它能从项目一开始就赋予你的数据以搜索、分析和探索的能力,这是通常没有预料到的。 它存在还因为原始数据如果只是躺在磁盘里面根本就毫无用处。基于Apache Lucene构建的开源搜索引擎。
- Kibana 是一款开源的数据分析和可视化平台,它是 Elastic Stack 成员之一,设计用于和 Elasticsearch 协作。您可以使用 Kibana 对 Elasticsearch 索引中的数据进行搜索、查看、交互操作。
简单讲,ES是一款数据库(server),Kibana是可视化客户端(client)。相比Lucene,ES易于使用、应用。应用优势:易于横向拓展,可支持PB级别的结构化与非结构化的数据处理。
适用场景:
应用安装#
首先安装Elasticsearch,Installation。
然后安装Kibana,安装 Kibana。
推荐英文文档。
单实例#
- 官网复制
tar
下载链接:https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-6.3.2.tar.gz
- 进入本地安装目录,wget下载
1
2
3
4
|
cd /Users/DragonSong/es_learn
wget https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-6.3.2.tar.gz
# 解压tar包
tar -zxvf elasticsearch-6.3.2.tar.gz
|
Head提供了友好的操作界面,可以辅助开发者操作ES。
- 进入GitHub搜索elasticsearch-head,获取zip下载链接,wget下载:https://github.com/mobz/elasticsearch-head/archive/master.zip
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
cd /Users/DragonSong/es_learn
wget https://github.com/mobz/elasticsearch-head/archive/master.zip
# 解压zip包
unzip master.zip
# 进入master目录,检查node版本
cd elasticsearch-head-master/
node -v
# npm装载环境
npm install
# 启动node服务
npm run start
# 浏览器查看界面
http://localhost:9100
# 修改es配置,解决跨域问题
cd ../elasticsearch-6.3.2
vi config/elasticsearch.yml
# shift+g跳到末行,添加内容:
http.cors.enabled: true
http.cors.allow-origin: "*"
# 保存退出
|
- 此时启动ES,开启Head node服务,进入
localhost:9100
即可看到工具已连接到ES
多节点#
分布式环境安装。
- 修改
elasticsearch.yml
,将当前节点设置为master
1
2
3
4
5
6
7
|
vi config/elasticsearch.yml
# 末尾增加配置:
cluster.name: dragon
node.name: master
node.master: true
# 绑定ip
network.host: 127.0.0.1
|
1
2
3
4
5
|
# 找到原后台服务,并kill
ps -ef | grep `pwd`
kill 20660
# 重启
./bin/elasticsearch -d
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
mkdir es_slave
# 复制tar包
cp elasticsearch-6.3.2.tar.gz es_slave
cd es_slave
# 解压
tar -zxvf elasticsearch-6.3.2.tar.gz
# 复制两个节点内容
cp -r elasticsearch-6.3.2 es_slave1
cp -r elasticsearch-6.3.2 es_slave2
# 进入slave1,修改配置
cd es_slave1
vi config/elasticsearch.yml
# 配置内容:
cluster.name: dragon
node.name: slave1
network.host: 127.0.0.1
# 端口需区分于master
http.port: 8200
discovery.zen.ping.unicast.hosts: ["127.0.0.1"]
# 上述过程重复与slave2即可
# 配置内容:
cluster.name: dragon
node.name: slave2
network.host: 127.0.0.1
# 端口需区分于master
http.port: 8100
discovery.zen.ping.unicast.hosts: ["127.0.0.1"]
|
扩充服务节点方式就是重复上述过程,易于操作。
Elasticsearch#
./bin/elasticsearch
启动
./bin/elasticsearch -d
后台启动
Ctrl+C
停止
curl 'http://localhost:9200/?pretty'
测试连接
Kibana#
- Kibana默认配置放在
config/kibana.yml
./bin/kibana
启动
localhost:5601 或者 http://YOURDOMAIN.com:5601
访问
装好跑起来,加载示例数据,使用Kibana操作下常规动作,了解下能做什么(what),想一下如何做到(how),以及结合应用的业务场景何时使用(when)。
用户手册#
对安装好的应用进行功能操作。
这里重点看Kibana 用户手册,从基础入门到可视化进行初步了解即可。
以及Elasticsearch: 权威指南,从基础入门,到聚合,参考自己要真实编码应用的方面,选择章节花费两个小时研究一下即可。
基本概念#
- 集群中包含了多个节点,服务启动后,head可以很方便地查看集群状态
- 索引:含有相同属性的文档集合
- 类型:索引可以定义多个类型,文档必属于一个类型
- 文档:可被索引的基本数据单位
- 分片:每个分片是一个Lucene索引,每个索引有多个分片,只能在创建索引时指定
- 备份:拷贝一份分片,就完成了分片备份,后期也可以自由修改
索引创建#
ES通过RESTFul API调用服务:
- API基本格式:
http://<ip>:<port>/<index>/<type>/<doc_id>
- 常见HTTP动词:
GET/PUT/POST/DELETE
非结构化的索引#
可以通过head插件界面创建索引。成功后返回:
1
2
3
4
5
|
{
"acknowledged": true,
"shards_acknowledged": true,
"index": "index_first"
}
|
创建index成功后,head首页即可看到不同节点存储的分片。(粗线表示主分片,细线表示备份)
索引信息中的mappings
为空时,即为非结构化的索引。
结构化的索引#
在head插件,复合查询tab中,输入:
1
2
3
4
5
6
7
8
9
10
|
http://localhost:9200/
languages/java/mappings
POST
{
"type_first": {
"properties": {
"title": "text"
}
}
}
|
提交这个POST请求,右侧返回如下信息,说明对应的mappings规则已成功建立:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
{
"_index": "languages",
"_type": "java",
"_id": "mappings",
"_version": 1,
"result": "created",
"_shards": {
"total": 2,
"successful": 1,
"failed": 0
},
"_seq_no": 0,
"_primary_term": 1
}
|
也可以到概览tab页查看索引信息,发现languages
这个index中已经保存了对应的mappings结构化信息。这一步可以使用更加易用的Postman进行操作,发送一个PUT将数据(index)写到es中,在body中选择raw->json,可以更好地定义json数据:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
|
{
"settings":{
"number_of_shards":3,
"number_of_replicas":1
},
"mappings":{
"java":{
"properties":{
"name":{
"type":"text"
},
"ranking":{
"type":"keyword"
},
"age":{
"type":"integer"
},
"date":{
"type":"date",
"format":"yyyy-MM-dd HH:mm:ss||yyyy-MM-dd||epoch_millis"
}
}
}
}
}
# kibana操作:
PUT index-name/type-name/_mapping
{
"type-name": {
"properties": {
"field-name": {
"type": "integer"
}
}
}
}
|
发送PUT后返回:
1
2
3
4
5
|
{
"acknowledged": true,
"shards_acknowledged": true,
"index": "newlanguages"
}
|
kibana查询mapping:
1
|
GET index-name/type-name/_mapping
|
此时在head插件的概览查看索引信息,可以看到mappings就是完全按照我们的json格式来定义的,这一步本质上,与rdbms中建表是同理的。
使用别名#
首先了解:如何在Elasticsearch里面使用索引别名。
代码使用别名操作实际数据,可以做到几个好处:
- 一个入口,可以无缝切换多个索引,不影响前端
- 分组多个索引,对数据进行自由分组,同一个别名可以同时整合多个索引中的数据
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
POST /_aliases
{
"actions": [
{
"add": {
"index": "index_new_name",
"alias": "alias_name"
}
},
{
"remove": {
"index": "index_old_name",
"alias": "alias_name"
}
}
]
}
|
数据插入#
doc的插入分两种类型:
结合rdbms使用经验,与我们插入数据id自增与自定义是类似的。
这里我们继续使用上面创建的newlanguages
index,向其中java
中插一条id=1的数据:
1
2
3
4
5
6
7
|
localhost:9200/newlanguages/java/1
{
"name":"java",
"ranking":"001",
"age":30,
"date":"1979-01-01"
}
|
返回说明生效:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
{
"_index": "newlanguages",
"_type": "java",
"_id": "1",
"_version": 1,
"result": "created",
"_shards": {
"total": 2,
"successful": 2,
"failed": 0
},
"_seq_no": 0,
"_primary_term": 1
}
|
改用POST,并去掉手动指定的id,让es自动帮助我们生成id:
1
2
3
4
5
6
7
|
localhost:9200/newlanguages/java/
{
"name":"java_02",
"ranking":"002",
"age":31,
"date":"1989-01-01"
}
|
返回如下信息说明生效:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
{
"_index": "newlanguages",
"_type": "java",
"_id": "aMzB22QBUDCHWKlAjWAs",
"_version": 1,
"result": "created",
"_shards": {
"total": 2,
"successful": 2,
"failed": 0
},
"_seq_no": 1,
"_primary_term": 1
}
|
可以看到id与之前的1是不同的,说明是不同的数据。head中的数据概览也能清晰地看到这条数据。
数据修改#
同样使用Postman发送一个POST,指定文档id,url后紧跟一个_update
关键词:
1
2
3
4
5
6
|
localhost:/9200/newlanguages/java/1/_update
{
"doc":{
"name":"更新后的名字:java01"
}
}
|
提交后显示为udpated
,说明数据已经更新完毕:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
{
"_index": "newlanguages",
"_type": "java",
"_id": "1",
"_version": 2,
"result": "updated",
"_shards": {
"total": 2,
"successful": 2,
"failed": 0
},
"_seq_no": 2,
"_primary_term": 1
}
|
脚本的修改方式,区别于上方的参数部分,url格式一致,并且es支持了多种脚本语言,这里使用内置的painless
,inline
中指定脚本的逻辑,ctx
表示es上下文,_source
则表示本次操作的文档,比如对其age
属性进行操作:
1
2
3
4
5
6
7
|
localhost:/9200/newlanguages/java/1/_update
{
"script":{
"lang":"painless",
"inline":"ctx._source.age += 2"
}
}
|
可以看到:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
{
"_index": "newlanguages",
"_type": "java",
"_id": "1",
"_version": 3,
"result": "updated",
"_shards": {
"total": 2,
"successful": 2,
"failed": 0
},
"_seq_no": 3,
"_primary_term": 1
}
|
结果为updated,而version也变为了3。同样在head的界面中可以查看这条数据实时的情况。age由原先的30变为了32,我们的脚本成功执行。
另一种写法是将参数值另外放到外层结构:
1
2
3
4
5
6
7
8
9
|
{
"script":{
"lang":"painless",
"inline":"ctx._source.age = params.age",
"params":{
"age":38
}
}
}
|
请求后数据version又迭代了一次:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
{
"_index": "newlanguages",
"_type": "java",
"_id": "1",
"_version": 4,
"result": "updated",
"_shards": {
"total": 2,
"successful": 2,
"failed": 0
},
"_seq_no": 4,
"_primary_term": 1
}
|
数据删除#
删除分两个层级:
删除一个文档,只需要指定文档id,并发送一个DELETE请求即可:
1
2
|
localhost:/9200/newlanguages/java/1
DELETE
|
结果状态变更为deleted:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
{
"_index": "newlanguages",
"_type": "java",
"_id": "1",
"_version": 5,
"result": "deleted",
"_shards": {
"total": 2,
"successful": 2,
"failed": 0
},
"_seq_no": 5,
"_primary_term": 1
}
|
而删除一个index可以在head插件界面直接操作,选定某个索引的动作,点击删除,输入确认的删除操作,此时该索引关联的数据都会被清空。同样也可以发送DELETE请求,url为:
1
2
|
localhost:/9200/indexname
DELETE index-name
|
数据查询#
分三种类型:
使用index/type/doc_id
即可简单直接地定位到一条数据,我们发送一个GET请求:
1
|
localhost:9200/newlanguages/java/5
|
即可得到响应:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
{
"_index": "newlanguages",
"_type": "java",
"_id": "5",
"_version": 1,
"found": true,
"_source": {
"name": "java5",
"ranking": "005",
"age": 51,
"date": "1959-01-01"
}
}
|
而条件查询则需要指定一些条件,请求改为POST,关键字改为_search
,body中详细描述查询条件,比如查某index下所有数据:
1
2
3
4
5
6
|
localhost:9200/newlanguages/_search
{
"query":{
"match_all":{}
}
}
|
会获取到所有数据:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
|
{
"took": 2,
"timed_out": false,
"_shards": {
"total": 3,
"successful": 3,
"skipped": 0,
"failed": 0
},
"hits": {
"total": 6,
"max_score": 1,
"hits": [
{
"_index": "newlanguages",
"_type": "java",
"_id": "2",
"_score": 1,
"_source": {
"name": "java2",
"ranking": "002",
"age": 30,
"date": "1929-01-01"
}
},
{
"_index": "newlanguages",
"_type": "java",
"_id": "4",
"_score": 1,
"_source": {
"name": "java4",
"ranking": "004",
"age": 41,
"date": "1909-01-01"
}
},
{
"_index": "newlanguages",
"_type": "java",
"_id": "5",
"_score": 1,
"_source": {
"name": "java5",
"ranking": "005",
"age": 51,
"date": "1959-01-01"
}
},
{
"_index": "newlanguages",
"_type": "java",
"_id": "aMzB22QBUDCHWKlAjWAs",
"_score": 1,
"_source": {
"name": "java_02",
"ranking": "002",
"age": 31,
"date": "1989-01-01"
}
},
{
"_index": "newlanguages",
"_type": "java",
"_id": "1",
"_score": 1,
"_source": {
"name": "java",
"ranking": "001",
"age": 30,
"date": "1979-01-01"
}
},
{
"_index": "newlanguages",
"_type": "java",
"_id": "3",
"_score": 1,
"_source": {
"name": "java3",
"ranking": "003",
"age": 31,
"date": "1949-01-01"
}
}
]
}
}
|
指定返回第一条数据:
1
2
3
4
5
6
7
8
|
localhost:9200/newlanguages/_search
{
"query":{
"match_all":{}
},
"from":1,
"size":1
}
|
指定关键词查询:
1
2
3
4
5
6
7
8
|
localhost:9200/newlanguages/_search
{
"query":{
"match":{
"name":"java3"
}
}
}
|
可以对多条结果指定排序规则:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
{
"query":{
"match":{
"name":"java"
}
},
"sort":[
{
"date":{
"order":"desc"
}
}
]
}
|
而聚合查询使用aggs
作为关键词,比如这里对数据以age字段进行分组:
1
2
3
4
5
6
7
8
9
|
{
"aggs":{
"group_by_age":{
"terms":{
"field":"age"
}
}
}
}
|
可以看到在聚合结果中,age为30、31的数据有两条:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
"aggregations": {
"group_by_age": {
"doc_count_error_upper_bound": 0,
"sum_other_doc_count": 0,
"buckets": [
{
"key": 30,
"doc_count": 2
},
{
"key": 31,
"doc_count": 2
},
{
"key": 41,
"doc_count": 1
},
{
"key": 51,
"doc_count": 1
}
]
}
}
|
当然可以对多个字段同时进行聚合:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
{
"aggs":{
"group_by_age":{
"terms":{
"field":"age"
}
},
"group_by_date":{
"terms":{
"field":"date"
}
}
}
}
|
利用stats
函数可以对某个字段进行统计计算:
1
2
3
4
5
6
7
8
9
|
{
"aggs":{
"stats_by_age":{
"stats":{
"field":"age"
}
}
}
}
|
聚合结果中对age维度的数量、最大、最小、均值、和进行了展示:
1
2
3
4
5
6
7
8
9
|
"aggregations": {
"stats_by_age": {
"count": 6,
"min": 30,
"max": 51,
"avg": 35.666666666666664,
"sum": 214
}
}
|
高级查询#
分类:
- 子条件查询:特定字段查询所指特定值
- Query context
在查询中,es首先判断文档是否满足条件,其次计算
_score
值,标识匹配程度(01是否匹配到02匹配得有多好)
常用查询:
- 全文本查询
针对文本类型数据
- 字段级别查询
针对结构化数据,如数字、日期
- Filter context
在查询中只判断文档是否满足条件,只有yes或no
- 复合条件查询:以一定逻辑组合子条件查询
例程:
1
2
3
4
5
6
7
|
{
"query":{
"match":{
"name":"java"
}
}
}
|
1
2
3
4
5
6
7
|
{
"query":{
"match_phrase":{
"name":"java"
}
}
}
|
与模糊匹配的区别是,习语匹配不会将词分为多个,严格分词"name":"java"
。
1
2
3
4
5
6
7
8
9
10
|
{
"query":{
"multi_match":{
"query":"java",
"fields":[
"name","alias"
]
}
}
}
|
1
2
3
4
5
6
7
|
{
"query":{
"query_string":{
"query":"java and python"
}
}
}
|
会同时返回带有java与python名字的数据:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
|
{
"took": 4,
"timed_out": false,
"_shards": {
"total": 3,
"successful": 3,
"skipped": 0,
"failed": 0
},
"hits": {
"total": 2,
"max_score": 1.2039728,
"hits": [
{
"_index": "newlanguages",
"_type": "java",
"_id": "7",
"_score": 1.2039728,
"_source": {
"name": "python",
"ranking": "007",
"age": 77,
"date": "1999-01-01",
"alias": "pyp"
}
},
{
"_index": "newlanguages",
"_type": "java",
"_id": "1",
"_score": 0.9808292,
"_source": {
"name": "java",
"ranking": "001",
"age": 30,
"date": "1979-01-01"
}
}
]
}
}
|
类似的也可以进行其他逻辑组合,如or
:
1
2
3
4
5
6
7
8
|
{
"query":{
"query_string":{
"query":"(java and python) or js",
"fields":["name","alias"]
}
}
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
{
"query":{
"term":{
"age":31
}
}
}
# 范围
{
"query":{
"range":{
"age":{
"gte":20,
"lte":32
}
}
}
}
# 日期范围
{
"query":{
"range":{
"date":{
"gte":"2000-01-01",
"lte":"2010-01-01"
}
}
}
}
|
1
2
3
4
5
6
7
8
9
10
11
|
{
"query":{
"bool":{
"filter":{
"term":{
"age":30
}
}
}
}
}
|
es用filter专门用来做数据过滤,对数据也会进行缓存,所以速度也会更快一些(相比query)。
- 固定分数查询
es针对每条数据进行
_score
评分,我们可以指定某一评分的数据(boost
):
1
2
3
4
5
6
7
8
9
10
11
12
|
{
"query":{
"constant_score":{
"filter":{
"match":{
"name":"java"
}
},
"boost":0.87546873
}
}
}
|
- 布尔查询
should
代表或,must
代表且,must_not
代表非。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
|
{
"query":{
"bool":{
"should":[
{
"match":{
"name":"python"
}
},
{
"match":{
"age":31
}
}
]
}
}
}
{
"query":{
"bool":{
"must":[
{
"match":{
"name":"python"
}
},
{
"match":{
"age":77
}
}
]
}
}
}
|
在此基础上,我们还可以通过filter
进行单字段条件的组合:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
{
"query":{
"bool":{
"should":[
{
"match":{
"name":"python"
}
},
{
"match":{
"age":31
}
}
],
"filter":[
{
"term":{
"alias":"pyp"
}
}
]
}
}
}
|
条件非:
1
2
3
4
5
6
7
8
9
10
11
|
{
"query":{
"bool":{
"must_not":{
"term":{
"name":"python"
}
}
}
}
}
|
aggregations是我目前首先要搞清楚的操作(前人使用了该操作)。
首先理解:类似于复杂SQL所能做到的一样,聚合之后的数据可以给到我们比较形象的分析结果,在API使用上,ES对聚合引入了两个抽象层:
- 桶(Buckets)
满足特定条件的文档的集合
- 指标(Metrics)
对桶内的文档进行统计计算
本质上是对文档(ES将数据以文档为单位进行组织,类似于RDBMS中的表,doc<->table)数据进行范围缩小、有用的数据计算。
对概念简单理解后,使用API多次,基本没应用问题了。
API#
俗话说,文档在手,天下我有。到最后落实到编码环节上,参考API文档必不可少。
参考Java API,根据自身使用的语言查看对应的API即可。
这里创建一个springboot工程进行基本的es操作:esExercise。
每一步都可以深入,细化成多篇文档,但要记住这里的重点是快速上手,所以只要前面这几步走过一遍,目标就达成了: