ElasticSearch之——聚合、Windows集群、JavaApi、Java代码实现增删改查

1.ES的聚合

聚合(aggregations)可以让我们极其方便的实现对数据的统计、分析。例如:

  • 什么品牌的手机最受欢迎?
  • 这些手机的平均价格、最高价格、最低价格?
  • 这些手机每月的销售情况如何?

实现这些统计功能的比数据库的sql要方便的多,而且查询速度非常快,可以实现近实时搜索效果。

要注意:参与聚合的字段,必须不能分词

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

Select * from 表名 where 分组前条件 group by 分组字段 having 分组后筛选条件
where:
where用在分组前,对分组前的数据进行筛选
where后不能使用聚合函数
having:
having用在分组后,对分组后的数据进行筛选
having后可以使用聚合函数
聚合函数:
count
max
min
sum
avg

根据性别分组: group by
男组 - 男桶
女组 - 女桶
对分组后的数据进行聚合: 进行统计
聚合函数 - 度量
聚合函数: 统计当前组中的数据信息
度量: 测量桶中的数据信息

1.1 基本概念

Elasticsearch中的聚合,包含多种类型,最常用的两种,一个叫,一个叫度量

桶(bucket)

桶的作用,是按照某种方式对数据进行分组,每一组数据在ES中称为一个,例如我们根据国籍对人划分,可以得到中国桶英国桶日本桶……或者我们按照年龄段对人进行划分:0-10,10-20,20-30,30-40等。

Elasticsearch中提供的划分桶的方式有很多:

  • Date Histogram Aggregation:根据日期阶梯分组,例如给定阶梯为周,会自动每周分为一组
  • Histogram Aggregation:根据数值阶梯分组,与日期类似,需要知道分组的间隔(interval)
  • Terms Aggregation:根据词条内容分组,词条内容完全匹配的为一组,类似数据库group by
  • Range Aggregation:数值和日期的范围分组,指定开始和结束,然后按段分组
  • ……

综上所述,我们发现bucket aggregations 只负责对数据进行分组,并不进行计算,因此往往bucket中往往会嵌套另一种聚合:metrics aggregations即度量

度量(metrics)

分组完成以后,我们一般会对组中的数据进行聚合运算,例如求平均值、最大、最小、求和等,这些在ES中称为度量

比较常用的一些度量聚合方式:

  • Avg Aggregation:求平均值
  • Max Aggregation:求最大值
  • Min Aggregation:求最小值
  • Percentiles Aggregation:求百分比
  • Stats Aggregation:同时返回avg、max、min、sum、count等
  • Sum Aggregation:求和
  • Top hits Aggregation:求前几
  • Value Count Aggregation:求总数
  • ……

为了测试聚合,我们先批量导入一些数据

创建索引:

1
2
3
4
5
6
7
8
9
10
11
12
13
PUT /car
{
"mappings": {
"properties": {
"color": {
"type": "keyword"
},
"make": {
"type": "keyword"
}
}
}
}

注意:在ES中,需要进行聚合、排序、过滤的字段其处理方式比较特殊,因此不能被分词,必须使用keyword数值类型。这里我们将color和make这两个文字类型的字段设置为keyword类型,这个类型不会被分词,将来就可以参与聚合

导入数据,这里是采用批处理的API,大家直接复制到kibana运行即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
POST /car/_bulk
{ "index": {}}
{ "price" : 10000, "color" : "红", "make" : "本田", "sold" : "2014-10-28" }
{ "index": {}}
{ "price" : 20000, "color" : "红", "make" : "本田", "sold" : "2014-11-05" }
{ "index": {}}
{ "price" : 30000, "color" : "绿", "make" : "福特", "sold" : "2014-05-18" }
{ "index": {}}
{ "price" : 15000, "color" : "蓝", "make" : "丰田", "sold" : "2014-07-02" }
{ "index": {}}
{ "price" : 12000, "color" : "绿", "make" : "丰田", "sold" : "2014-08-19" }
{ "index": {}}
{ "price" : 20000, "color" : "红", "make" : "本田", "sold" : "2014-11-05" }
{ "index": {}}
{ "price" : 80000, "color" : "红", "make" : "宝马", "sold" : "2014-01-01" }
{ "index": {}}
{ "price" : 25000, "color" : "蓝", "make" : "福特", "sold" : "2014-02-12" }

1.2 聚合为桶

首先,我们按照 汽车的颜色color来划分,按照颜色分桶,最好是使用TermAggregation类型,按照颜色的名称来分桶。

1
2
3
4
5
6
7
8
9
10
11
GET /car/_search
{
"size" : 0,
"aggs" : {
"popular_colors" : {
"terms" : {
"field" : "color"
}
}
}
}
  • size: 查询条数,这里设置为0,因为我们不关心搜索到的数据 ,只关心聚合结果,提高效率
  • aggs:声明这是一个聚合查询,是aggregations的缩写
    • popular_colors:给这次聚合起一个名字,可任意指定。
      • terms:聚合的类型,这里选择terms,是根据词条内容(这里是颜色)划分
        • field:划分桶时依赖的字段

结果:

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
{
"took": 33,
"timed_out": false,
"_shards": {
"total": 5,
"successful": 5,
"skipped": 0,
"failed": 0
},
"hits": {
"total": 8,
"max_score": 0,
"hits": []
},
"aggregations": {
"popular_colors": {
"doc_count_error_upper_bound": 0,
"sum_other_doc_count": 0,
"buckets": [
{
"key": "红",
"doc_count": 4
},
{
"key": "绿",
"doc_count": 2
},
{
"key": "蓝",
"doc_count": 2
}
]
}
}
}
  • hits:查询结果为空,因为我们设置了size为0
  • aggregations:聚合的结果
  • popular_colors:我们定义的聚合名称
  • buckets:查找到的桶,每个不同的color字段值都会形成一个桶
    • key:这个桶对应的color字段的值
    • doc_count:这个桶中的文档数量

通过聚合的结果我们发现,目前红色的小车比较畅销!

1.3 桶内度量

前面的例子告诉我们每个桶里面的文档数量,这很有用。 但通常,我们的应用需要提供更复杂的文档度量。 例如,每种颜色汽车的平均价格是多少?

因此,我们需要告诉Elasticsearch使用哪个字段使用何种度量方式进行运算,这些信息要嵌套在内,度量的运算会基于内的文档进行

现在,我们为刚刚的聚合结果添加 求价格平均值的度量:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
GET /car/_search
{
"size" : 0,
"aggs" : {
"popular_colors" : {
"terms" : {
"field" : "color"
},
"aggs":{
"avg_price": {
"avg": {
"field": "price"
}
}
}
}
}
}
  • aggs:我们在上一个aggs(popular_colors)中添加新的aggs。可见度量也是一个聚合
  • avg_price:聚合的名称
  • avg:度量的类型,这里是求平均值
  • field:度量运算的字段

结果:

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
{
"took" : 0,
"timed_out" : false,
"_shards" : {
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 8,
"relation" : "eq"
},
"max_score" : null,
"hits" : [ ]
},
"aggregations" : {
"popular_colors" : {
"doc_count_error_upper_bound" : 0,
"sum_other_doc_count" : 0,
"buckets" : [
{
"key" : "红",
"doc_count" : 4,
"avg_price" : {
"value" : 32500.0
}
},
{
"key" : "绿",
"doc_count" : 2,
"avg_price" : {
"value" : 21000.0
}
},
{
"key" : "蓝",
"doc_count" : 2,
"avg_price" : {
"value" : 20000.0
}
}
]
}
}
}

可以看到每个桶中都有自己的avg_price字段,这是度量聚合的结果

1.4 桶内嵌套桶

刚刚的案例中,我们在桶内嵌套度量运算。事实上桶不仅可以嵌套运算, 还可以再嵌套其它桶。也就是说在每个分组中,再分更多组。

比如:我们想统计每种颜色的汽车中,分别属于哪个制造商,按照make字段再进行分桶

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
GET /car/_search
{
"size" : 0,
"aggs" : {
"popular_colors" : {
"terms" : {
"field" : "color"
},
"aggs":{
"avg_price": {
"avg": {
"field": "price"
}
},
"maker":{
"terms":{
"field":"make"
}
}
}
}
}
}
  • 原来的color桶和avg计算我们不变
  • maker:在嵌套的aggs下新添一个桶,叫做maker
  • terms:桶的划分类型依然是词条
  • filed:这里根据make字段进行划分

部分结果:

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
{
"took": 16,
"timed_out": false,
"_shards": {
"total": 5,
"successful": 5,
"skipped": 0,
"failed": 0
},
"hits": {
"total": 8,
"max_score": 0,
"hits": []
},
"aggregations": {
"popular_colors": {
"doc_count_error_upper_bound": 0,
"sum_other_doc_count": 0,
"buckets": [
{
"key": "红",
"doc_count": 4,
"maker": {
"doc_count_error_upper_bound": 0,
"sum_other_doc_count": 0,
"buckets": [
{
"key": "本田",
"doc_count": 3
},
{
"key": "宝马",
"doc_count": 1
}
]
},
"avg_price": {
"value": 32500
}
},
{
"key": "绿",
"doc_count": 2,
"maker": {
"doc_count_error_upper_bound": 0,
"sum_other_doc_count": 0,
"buckets": [
{
"key": "丰田",
"doc_count": 1
},
{
"key": "福特",
"doc_count": 1
}
]
},
"avg_price": {
"value": 21000
}
},
{
"key": "蓝",
"doc_count": 2,
"maker": {
"doc_count_error_upper_bound": 0,
"sum_other_doc_count": 0,
"buckets": [
{
"key": "丰田",
"doc_count": 1
},
{
"key": "福特",
"doc_count": 1
}
]
},
"avg_price": {
"value": 20000
}
}
]
}
}
}
  • 我们可以看到,新的聚合maker被嵌套在原来每一个color的桶中。
  • 每个颜色下面都根据 make字段进行了分组
  • 我们能读取到的信息:
    • 红色车共有4辆
    • 红色车的平均售价是 $32,500 美元。
    • 其中3辆是 Honda 本田制造,1辆是 BMW 宝马制造。

课堂代码

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
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249

GET _analyze
{
"text": "小米手机",
"analyzer": "ik_max_word"
}
# 排序
# 默认情况下ES会对查询结果根据评估分值进行排序
# 如果设置了排序字段则,分值失效(ES不再计算分值)
GET heima/_search
{
"query": {
"match": {
"title": "小米手机"
}
},
"sort": [
{
"price": {
"order": "asc"
}
}
]
}

# 高亮显示
GET /heima/_search
{
"query": {
"match": {
"title": "手机"
}
},
"highlight": {
"pre_tags": "<em>",
"post_tags": "</em>",
"fields": {
"title": {}
}
}
}
# 分页查询, 将查询结果分页展示
GET heima/_search
{
"query": {
"match": {
"title": "小米手机"
}
},
"size": 2
}

# bool-filter过滤
GET /heima/_search
{
"query": {
"bool": {
"must": [
{
"match": {
"title": "小米手机"
}
},
{
"range": {
"price": {
"gt": 2000,
"lt": 5200
}
}
}
]
}
}
}

GET /heima/_search
{
"query": {
"bool": {
"must": [
{
"match": {
"title": "小米手机"
}
}
],
"filter": [
{
"range": {
"price": {
"gt": 2000,
"lt": 3200
}
}
}
]
}
}
}
# 指定查询结果字段
GET heima/_search
{
"_source": ["title","price"],
"query": {
"match": {
"title": "小米手机"
}
}
}

GET heima/_search
{
"_source": {
"includes": ["title","price"]
},
"query": {
"match": {
"title": "小米手机"
}
}
}
GET heima/_search
{
"_source": {
"excludes": ["title"]
},
"query": {
"match": {
"title": "小米手机"
}
}
}
# =========================聚合
# 数据准备
PUT /car
{
"mappings": {
"properties": {
"color": {
"type": "keyword"
},
"make": {
"type": "keyword"
}
}
}
}
GET /car/_mapping

POST /car/_bulk
{ "index": {}}
{ "price" : 10000, "color" : "红", "make" : "本田", "sold" : "2014-10-28" }
{ "index": {}}
{ "price" : 20000, "color" : "红", "make" : "本田", "sold" : "2014-11-05" }
{ "index": {}}
{ "price" : 30000, "color" : "绿", "make" : "福特", "sold" : "2014-05-18" }
{ "index": {}}
{ "price" : 15000, "color" : "蓝", "make" : "丰田", "sold" : "2014-07-02" }
{ "index": {}}
{ "price" : 12000, "color" : "绿", "make" : "丰田", "sold" : "2014-08-19" }
{ "index": {}}
{ "price" : 20000, "color" : "红", "make" : "本田", "sold" : "2014-11-05" }
{ "index": {}}
{ "price" : 80000, "color" : "红", "make" : "宝马", "sold" : "2014-01-01" }
{ "index": {}}
{ "price" : 25000, "color" : "蓝", "make" : "福特", "sold" : "2014-02-12" }

# ===========
GET /car/_search
{
"query": {
"match_all": {}
}
}
# 聚合分桶
GET car/_search
{
"query": {
"term": {
"make": "福特"
}
},
"size": 0,
"aggs": {
"aaa": {
"terms": {
"field": "color"
}
}
}
}
# 分完桶后,对桶中的数据进行度量
# 求每一个桶中汽车价格的平均值(度量本质上也是聚合)
GET car/_search
{
"size": 0,
"aggs": {
"colors_name": {
"terms": {
"field": "color"
},
"aggs": {
"avg_name": {
"avg":{
"field": "price"
}
},
"max_name":{
"max": {
"field": "price"
}
}
}
}
}
}


GET car/_search
{
"size": 0,
"aggs": {
"colors_name": {
"terms": {
"field": "color"
},
"aggs": {
"make_name": {
"terms": {
"field": "make"
},
"aggs": {
"max_name": {
"max": {
"field": "price"
}
}
}
},
"min_name":{
"min": {
"field": "price"
}
}
}
}
}
}

2.ES集群

我们之前安装的是单机的ES,线上部署肯定不能只用一台,因为一旦服务宕机,整个搜索服务就不可用了。因此必须使用集群来解决。

那么问题来了:什么是集群呢?

2.1.什么是集群

来看下维基百科对集群的介绍:

1543286678122

集群是==一组计算机==高度紧密协作,完成计算工作。其中的每个计算机称为一个==节点==。根据这些计算机的协作方式不同或者目的不同,我们将集群分成三类:

  • 高可用集群
  • 负载均衡集群
  • 科学计算集群(分布式处理)

上述几种集群方式并非必须独立使用,我们在系统架构时经常会组合使用。

2.1.1 高可用集群

High availability Cluster高可用群集,简称HAC。其设计思想是为了避免出现单点故障问题,在故障时可以==快速恢复,快速继续提供服务==。

1543287433625

如图所示,集群中两台计算机node01和node02,两者共享资源,处理业务也基本一致,互为==主从==。当node01工作时,node02就处于待命状态。所有业务在Node01上运行,若发生故障服务和资源会转移到Node02上。

这种架构保证了服务的高可用,但是闲置的节点是对资源的一种浪费。

2.1.2 负载均衡集群

Load Balancing负载均衡,集群中的每一台计算机都来完成相同业务,不分主次。当用户请求到达时,通过某种算法,让请求均衡的分发到集群中的每个节点,充分利用每个节点的资源。如图所示:

1543287806342

因为每个节点业务相同,如果某个节点出现故障,只需要把请求分发到其它节点即可。

2.1.3 科学计算集群

因为硬件设备的限制,单台计算机的处理性能是有上限的,如果计算需要的资源超过了单台计算机的能力,该怎么办呢?此时就可以使用科学计算集群。

我们把复杂任务拆分成一个个小的子任务,然后分配到集群中的不同节点上完成,最后再把计算结果汇总。这样大量低廉的PC机互联起来,组成一个”超级计算机”以解决复杂的计算任务。

1543288401040

这样的方式也称为==分布式运算或者分布式集群==,集群中的每个节点完成==不同任务==。

2.2.WEB应用的集群模式

上述计算机协作的集群方式任何领域都可以使用,在web开发中也是如此,不过有一些细节的不同。我们以一个电商网站为例,看看几种架构方式:

2.2.1 单体应用

所有业务在一个系统中完成:

image-20200104114208435

出现的问题:

  • 系统庞大,功能耦合,难以维护
  • 并发能力差,容易出现单点故障
  • 无法针对不同功能进行优化

2.2.2 分布式架构

按照上面的分布式集群概念,集群中的每个节点完成不同业务。在web开发中也是如此,我们把完整系统进行拆分,形成独立系统,然后部署到不同的tomcat节点,不同节点通过网络通信,相互协作。

image-20200104114653959

这样就将复杂系统细分,降低了业务间的耦合,但是却带来了另一个问题,就是单个节点故障会导致整个系统不完整。为了保证高可用,还需要对集群做备份,实现负载均衡。

2.2.3 高可用分布式集群架构

为了解决上面所述的单点故障问题,我们可以为分布式系统中的每个节点都部署负载均衡节点,即:每个业务系统都有一个负载均衡的小集群。

image-20200104115507599

2.3.ElasticSearch的集群

单点的elasticsearch存在哪些可能出现的问题呢?

  • 单台机器存储容量有限
  • 单服务器容易出现单点故障,无法实现高可用
  • 单服务的并发处理能力有限

所以,为了应对这些问题,我们需要对elasticsearch搭建集群

2.3.1.数据分片

首先,我们面临的第一个问题就是数据量太大,单点存储量有限的问题。

我们可以把数据拆分成多份,每一份存储到不同机器节点(node),从而实现减少每个节点数据量的目的。这就是数据的分布式存储,也叫做:数据分片(Shard)

image-20200104124440086

此处,我们把数据分成3片:shard0、shard1、shard2

2.3.2.数据备份

数据分片解决了海量数据存储的问题,但是如果出现单点故障,那么分片数据就不再完整,这又该如何解决呢?

没错,就像大家为了备份手机数据,会额外存储一份到移动硬盘一样。我们可以给每个分片数据进行备份,存储到其它节点,防止数据丢失,这就是数据备份,也叫数据副本(replica)

数据备份可以保证高可用,但是每个分片备份一份,所需要的节点数量就会翻一倍,成本实在是太高了!

为了在高可用和成本间寻求平衡,我们可以这样做:

  • 首先对数据分片,存储到不同节点
  • 然后对每个分片进行备份,放到对方节点,完成互相备份

这样可以大大减少所需要的服务节点数量,如图,我们以3分片,每个分片备份一份为例:

image-20200104124551912

现在,每个分片都有1个备份,存储在3个节点:

  • node0:保存了分片0和1
  • node1:保存了分片0和2
  • node2:保存了分片1和2

2.4.Windows搭建集群

进入ElasticSearch的bin目录:

image-20200104121859074

然后打开3个控制台,分别输入下面的3个指令:

1
2
3
4
5
6
# node0指令:
elasticsearch.bat -E node.name=node0 -E cluster.name=elastic -E path.data=node0_data
# node1指令:
elasticsearch.bat -E node.name=node1 -E cluster.name=elastic -E path.data=node1_data
# node2指令:
elasticsearch.bat -E node.name=node2 -E cluster.name=elastic -E path.data=node2_data

命令解释:

  • elasticsearch.bat:运行elasticsearch.bat文件
  • -E node.name-E是环境参数,node.name是指定节点名称
  • cluster.name:指定集群名称
  • path.data:指定数据目录地址,相对路径是相对于ElasticSearch的安装目录

2.5.配置Kibana访问集群

2.5.1.修改配置

在Kibana中的config中打开kibana.yml文件:

image-20200104123006061

找到这样一行代码:

1
# elasticsearch.hosts: ["http://localhost:9200"]

前面的#是注释,需要删除以打开注释。然后在后面的数组中添加ES的集群地址:

1
elasticsearch.hosts: ["http://localhost:9200","http://localhost:9201","http://localhost:9202"]

2.5.2.重启并访问

重启kibana,然后再左侧的菜单点击monitor:

image-20200104123219341

在页面中点击按钮,打开监控功能,可能需要等待几秒钟。

image-20200104123234801

然后可以看到kibana提供的监控功能:

image-20200104123303523

可以看到启动的3个节点的信息:

image-20200104123425983

2.6.测试集群

2.6.1.配置分片和副本信息

还记得创建索引库的API吗?

  • 请求方式:PUT

  • 请求路径:/索引库名

  • 请求参数:json格式:

    1
    2
    3
    4
    5
    {
    "settings": {
    "属性名": "属性值"
    }
    }

    settings:就是索引库设置,其中可以定义索引库的各种属性,之前我们没有配置,是默认值。

settings中就可以配置索引库的分片和副本信息,语法如下:

1
2
3
4
5
6
7
PUT /heima
{
"settings": {
"number_of_shards": 3, # 设置分片数为3
"number_of_replicas": 1 # 设置副本数为1
}
}

这里有两个配置:

  • number_of_shards:分片数量,这里设置为3
  • number_of_replicas:副本数量,这里设置为1,每个分片一个备份,一个原始数据,共2份。

2.6.2.查看分片结果

进入monitor页面,然后选择查看索引库(indices)信息:

image-20200104123901902

可以看到我们刚刚加入的索引库:

image-20200104124032092

点击进入,然后拉到页面最底部:

image-20200104124127516

可以看到每个分片在节点上的信息:

  • node0:保存了分片0和1
  • node1:保存了分片0和2
  • node2:保存了分片1和2

这个结果与我们上面画图分析是一致的。

2.6.3.集群动态伸缩

现在,我们让node1宕机,停止node1的控制台进程即可。

然后查看Kibana中的节点状态:

image-20200104124825356

发现node1已经宕机了,此时数据是不安全的。

稍等片刻,再次查看:

image-20200104125003684

发现分片数据进行了重新分配,node1上的分片被重新分配了。此时集群依然是健康的。

我们重新启动node1看看:

image-20200104125153682

分片再次重新分配,那么每个节点都存储了部分分片。

3.ES的Java客户端

在elasticsearch官网中提供了各种语言的客户端:https://www.elastic.co/guide/en/elasticsearch/client/index.html

而Java的客户端就有两个:

image-20200104164045946

不过Java API这个客户端(Transport Client)已经在7.0以后过期了,而且在8.0版本中将直接废弃。所以我们会学习Java REST Client:

image-20200104164428873

然后再选择High Level REST Client这个。

Java REST Client 其实就是利用Java语言向 ES服务发 Http的请求,因此请求和操作与前面学习的REST API 一模一样。

image-20210630113645669

不过,为了后面学习,我们需要准备一些数据,导入到ES中

3.1.准备数据库数据

我们需要从数据库导入数据到ES中,因此需要做一些准备:

  • 引入依赖
  • 执行sql,准备数据
  • 引入实体类
  • 引入mybatis相关配置
  • 引入mapper和service代码

3.1.1.创建maven项目es-demo,并引入依赖

在项目的pom文件中引入一些依赖:

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
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>cn.itcast.demo</groupId>
<artifactId>es-demo</artifactId>
<version>1.0.0-SNAPSHOT</version>

<dependencies>
<!-- Junit单元测试 -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
<!--elastic客户端-->
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-high-level-client</artifactId>
<version>7.4.2</version>
</dependency>
<!--数据库驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.47</version>
</dependency>
<!--mybatis-->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.2</version>
</dependency>
<!--lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.8</version>
</dependency>
<!--JSON工具-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.49</version>
</dependency>
<!--common工具-->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.8.1</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.2</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
</plugins>
</build>
</project>

3.1.2.执行sql

我们导入课前资料提供的Sql:tb_user.sql。或者执行sql语句:

表结构:

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
-- ----------------------------
-- Table structure for tb_user
-- ----------------------------
DROP TABLE IF EXISTS `tb_user`;
CREATE TABLE `tb_user` (
`id` bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT,
`name` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '' COMMENT '姓名',
`age` int(10) UNSIGNED NOT NULL DEFAULT 0 COMMENT '年龄',
`gender` varchar(2) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '男' COMMENT '性别',
`note` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '' COMMENT '备注',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 13 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;

-- ----------------------------
-- Records of tb_user
-- ----------------------------
INSERT INTO `tb_user` VALUES (1, '张三', 30, '男', '张三同学在学Java');
INSERT INTO `tb_user` VALUES (2, '李四', 21, '男', '李四同学在传智学Java');
INSERT INTO `tb_user` VALUES (3, '王五', 22, '男', '王五同学在学php');
INSERT INTO `tb_user` VALUES (4, '张伟', 20, '男', '张伟同学在传智播客学Java');
INSERT INTO `tb_user` VALUES (5, '李娜', 28, '女', '李娜同学在传智播客学Java');
INSERT INTO `tb_user` VALUES (6, '李磊', 23, '男', '李磊同学在传智播客学Java');
INSERT INTO `tb_user` VALUES (7, '韩梅梅', 24, '女', '韩梅梅同学在传智播客学php');
INSERT INTO `tb_user` VALUES (8, '柳岩', 21, '女', '柳岩同学在传智播客学表演');
INSERT INTO `tb_user` VALUES (9, '刘亦菲', 18, '女', '刘亦菲同学在传智播客学唱歌');
INSERT INTO `tb_user` VALUES (10, '范冰冰', 25, '女', '范冰冰同学在传智播客学表演');
INSERT INTO `tb_user` VALUES (11, '郑爽', 23, '女', '郑爽同学在传智播客学习如何装纯');
INSERT INTO `tb_user` VALUES (12, '唐嫣', 26, '女', '唐嫣同学在传智播客学习如何耍酷');

SET FOREIGN_KEY_CHECKS = 1;

3.1.3.引入实体类

实体类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package cn.itcast.es.pojo;


import lombok.Data;

@Data
public class User {

private Long id;

private String name;// 姓名

private Integer age;// 年龄

private String gender;// 性别

private String note;// 备注
}

3.1.4.引入mybatis配置

课前资料中提供了配置:

image-20200104180104872

把:jdbc.propertiesmybatis-config.xmlUserMapper.xml复制到项目中:

image-20200104180244813

3.1.5.引入mapper和Service

mapper:

1
2
3
4
5
6
7
8
9
10
11
12
13
package cn.itcast.es.mapper;

import cn.itcast.es.pojo.User;

import java.util.List;


public interface UserMapper {

User findById(Long id);

List<User> findAll();
}

service:

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
package cn.itcast.es.service;

import cn.itcast.es.mapper.UserMapper;
import cn.itcast.es.pojo.User;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;

import java.io.IOException;
import java.io.InputStream;
import java.util.List;


public class UserService {
private UserMapper userMapper;

public UserService(){
try {
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
userMapper = sqlSessionFactory.openSession(true).getMapper(UserMapper.class);
} catch (IOException e) {
throw new RuntimeException(e);
}
}

public User findById(Long id){
return userMapper.findById(id);
}

public List<User> findAll(){
return userMapper.findAll();
}
}

提供了根据id查询和查询所有两个功能。

3.2.连接ElasticSearch

在官网上可以看到连接ES的教程:https://www.elastic.co/guide/en/elasticsearch/client/java-rest/current/java-rest-high-getting-started-initialization.html

首先需要与ES建立连接,ES提供了一个客户端RestHighLevelClient。

代码如下:

1
2
3
4
5
6
7
RestHighLevelClient client = new RestHighLevelClient(
RestClient.builder(
new HttpHost("localhost", 9200, "http"),
new HttpHost("localhost", 9201, "http"),
new HttpHost("localhost", 9202, "http")
)
);

ES中的所有操作都是通过RestHighLevelClient来完成的:

image-20200105103815463

为了后面测试方便,我们写到一个单元测试中,并且通过@Before注解来初始化客户端连接。

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
public class ElasticDemo {

private RestHighLevelClient client;

/**
* 建立连接
*/
@Before
public void init() throws IOException {
client = new RestHighLevelClient(
RestClient.builder(
new HttpHost("localhost", 9200, "http"),
new HttpHost("localhost", 9201, "http"),
new HttpHost("localhost", 9202, "http")
)
);
}

/**
* 关闭客户端连接
*/
@After
public void close() throws IOException {
client.close();
}
}

4.Java实现创建库和映射

开发中,往往库和映射的操作一起完成,官网详细文档地址:https://www.elastic.co/guide/en/elasticsearch/client/java-rest/7.x/_index_apis.html

这里我们主要实现库和映射的创建。查询、删除等功能大家可参考文档自己实现。

image-20200105093038617

4.1.思路分析

按照官网给出的步骤,创建索引包括下面几个步骤:

  • 1)创建CreateIndexRequest对象,并指定索引库名称
  • 2)指定settings配置
  • 3)指定mapping配置
  • 4)发起请求,得到响应

其实仔细分析,与我们在Kibana中的Rest风格API完全一致:

1
2
3
4
5
6
7
8
9
10
PUT /heima
{
"settings": {
"number_of_shards": 3,
"number_of_replicas": 1
},
"mappings": {

}
}

4.2.设计映射规则

Java代码中设置mapping,依然与REST中一致,需要JSON风格的映射规则。因此我们先在kibana中给User实体类定义好映射规则。

User包括下面的字段:

  • Id:主键,在ES中是唯一标示,数字,可以选择long类型
  • name:姓名,字符串类型,但是无需分词,使用keyword,也无需查找,index为false
  • age:年龄,整数,可以使用integer
  • gender:性别,字符串类型,但是无需分词,使用keyword
  • note:备注,用户详细信息,字符串类型。需要分词,使用text

映射如下:

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
PUT /user
{
"settings": {
"number_of_shards": 3,
"number_of_replicas": 1
},
"mappings": {
"properties": {
"id": {
"type": "long"
},
"name":{
"type": "keyword"
},
"age":{
"type": "integer"
},
"gender":{
"type": "keyword"
},
"note":{
"type": "text",
"analyzer": "ik_max_word"
}
}
}
}

4.3.代码实现

我们在上面新建的ElasticDemo类中新建单元测试,完成代码,思路就是之前分析的4步骤:

  • 1)创建CreateIndexRequest对象,并指定索引库名称
  • 2)指定settings配置
  • 3)指定mapping配置
  • 4)发起请求,得到响应
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
private RestHighLevelClient client;

@Test
public void testCreateIndex() throws IOException {
// 1.创建CreateIndexRequest对象,并指定索引库名称
CreateIndexRequest request = new CreateIndexRequest("user");
// 2.指定settings配置
request.settings(Settings.builder()
.put("index.number_of_shards", 3)
.put("index.number_of_replicas", 1)
);
// 3.指定mapping配置
request.mapping("{\n" +
" \"properties\": {\n" +
" \"id\": {\n" +
" \"type\": \"long\"\n" +
" },\n" +
" \"name\":{\n" +
" \"type\": \"keyword\"\n" +
" },\n" +
" \"age\":{\n" +
" \"type\": \"integer\"\n" +
" },\n" +
" \"gender\":{\n" +
" \"type\": \"keyword\"\n" +
" },\n" +
" \"note\":{\n" +
" \"type\": \"text\",\n" +
" \"analyzer\": \"ik_max_word\"\n" +
" }\n" +
" }\n" +
" }",
XContentType.JSON);
// 4.发起请求,得到响应
CreateIndexResponse response = client.indices().create(request, RequestOptions.DEFAULT);

System.out.println("response = " + response.isAcknowledged());
}

返回结果:

1
response = true

课堂代码

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
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
package com.itheima.es;

import org.apache.http.HttpHost;
import org.elasticsearch.action.admin.indices.delete.DeleteIndexRequest;
import org.elasticsearch.action.support.master.AcknowledgedResponse;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestClient;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.client.indices.CreateIndexRequest;
import org.elasticsearch.client.indices.CreateIndexResponse;
import org.elasticsearch.client.indices.GetIndexRequest;
import org.elasticsearch.client.indices.GetIndexResponse;
import org.elasticsearch.cluster.metadata.MappingMetaData;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.xcontent.XContentType;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;

import java.io.IOException;
import java.util.Map;

public class EsDemo {
private RestHighLevelClient client =null ;
@Before
public void init(){
// 创建客户端对象
client = new RestHighLevelClient(
RestClient.builder(
//new HttpHost("localhost", 9200, "http"),
new HttpHost("192.168.190.149", 9200, "http")));
}
// ====================TODO:操作索引库========================
/**
* TODO:创建索引库
*/
@Test
public void testIndexCreate() throws IOException {
// 创建添加索引库的请求信息对象 指定索引库的名称
CreateIndexRequest createIndexRequest = new CreateIndexRequest("user");
CreateIndexResponse createIndexResponse =
client.indices().create(createIndexRequest,RequestOptions.DEFAULT);
System.out.println(createIndexResponse.isAcknowledged());
}

/**
TODO: 操作ES流程
1.获取客户端对象
2.创建请求语义对象(用于封装本次请求的行为)
3.发送请求给ES,并接收响应结果
4.解析响应结果
*/
/**
* 查看索引库
* @throws IOException
*/
@Test
public void testIndexGet() throws IOException {
GetIndexRequest request = new GetIndexRequest("car");
GetIndexResponse response = client.indices().get(request, RequestOptions.DEFAULT);
// 获取索引库的配置信息
Map<String, Settings> settings = response.getSettings();
System.out.println(settings.toString());
// 获取索引库的映射信息
Map<String, MappingMetaData> mappings = response.getMappings();
MappingMetaData metaData = mappings.get("car");
Map<String, Object> sourceAsMap = metaData.sourceAsMap();
System.out.println(sourceAsMap.toString());
}

/**
* 删除索引库
* @throws IOException
*/
@Test
public void testIndexDelete() throws IOException{
DeleteIndexRequest request = new DeleteIndexRequest("user");
AcknowledgedResponse deleteIndexResponse =
client.indices().delete(request, RequestOptions.DEFAULT);
System.out.println(deleteIndexResponse.isAcknowledged());
}

/**
* 创建索引库时,为索引库添加属性和设置类型映射
* @throws IOException
*/
@Test
public void testIndexCreate1() throws IOException{
// 创建描述语义的对象
CreateIndexRequest createIndexRequest = new CreateIndexRequest("user");
// TODO:设置索引库的属性
//createIndexRequest.settings(Settings.builder()
// .put("index.number_of_shards", 3)
// .put("index.number_of_replicas", 1)
//);
// TODO:设置索引库的类型映射
//createIndexRequest.mapping(" {\n" +
// " \"properties\": {\n" +
// " \"id\": {\n" +
// " \"type\": \"long\"\n" +
// " },\n" +
// " \"name\":{\n" +
// " \"type\": \"keyword\"\n" +
// " },\n" +
// " \"age\":{\n" +
// " \"type\": \"integer\"\n" +
// " },\n" +
// " \"gender\":{\n" +
// " \"type\": \"keyword\"\n" +
// " },\n" +
// " \"note\":{\n" +
// " \"type\": \"text\",\n" +
// " \"analyzer\": \"ik_max_word\"\n" +
// " }\n" +
// " }\n" +
// " }",XContentType.JSON);
createIndexRequest.source("{\n" +
" \"settings\": {\n" +
" \"number_of_shards\": 3,\n" +
" \"number_of_replicas\": 1\n" +
" },\n" +
" \"mappings\": {\n" +
" \"properties\": {\n" +
" \"id\": {\n" +
" \"type\": \"long\"\n" +
" },\n" +
" \"name\":{\n" +
" \"type\": \"keyword\"\n" +
" },\n" +
" \"age\":{\n" +
" \"type\": \"integer\"\n" +
" },\n" +
" \"gender\":{\n" +
" \"type\": \"keyword\"\n" +
" },\n" +
" \"note\":{\n" +
" \"type\": \"text\",\n" +
" \"analyzer\": \"ik_max_word\"\n" +
" }\n" +
" }\n" +
" }\n" +
"}",XContentType.JSON);
CreateIndexResponse createIndexResponse =
client.indices().create(createIndexRequest,RequestOptions.DEFAULT);
System.out.println(createIndexResponse.isAcknowledged());
}

@After
public void close() throws IOException {
if (client!=null)
this.client.close();
}
}

5.Java实现文档的CRUD

文档操作包括:新增文档、查询文档、修改文档、删除文档等。

官网地址:https://www.elastic.co/guide/en/elasticsearch/client/java-rest/7.x/java-rest-high-supported-apis.html

5.1.新增

官网地址:https://www.elastic.co/guide/en/elasticsearch/client/java-rest/7.x/java-rest-high-document-index.html

5.1.1.实现思路

根据官网文档,实现的步骤如下:

  • 1)创建IndexRequest对象,并指定索引库名称
  • 2)指定新增的数据的id
  • 3)将新增的文档数据变成JSON格式
  • 4)将JSON数据添加到IndexRequest中
  • 5)发起请求,得到结果

不过,我们的文档数据需要去查询数据库,因此前面会多出一个步骤:从数据库查询文档数据

  • 1)从数据库查询文档数据
  • 2)创建IndexRequest对象,并指定索引库名称
  • 3)指定新增的数据的id
  • 4)将新增的文档数据变成JSON格式
  • 5)将JSON数据添加到IndexRequest中
  • 6)发起请求,得到结果

5.1.2.具体代码

需要在单元测试类中线初始化UserService对象:

1
private UserService userService = new UserService();

新增文档:

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
/**
TODO: 操作ES流程
1.获取客户端对象
2.创建请求语义对象(用于封装本次请求的行为)
3.发送请求给ES,并接收响应结果
4.解析响应结果
*/
/**
* 添加文档数据信息
* @throws IOException
*/
@Test
public void docPost1() throws IOException {
IndexRequest request = new IndexRequest("user");
request.id("2");
String jsonString = "{\n" +
" \"age\" : 18,\n" +
" \"gender\" :\"女\",\n" +
" \"name\" : \"大幂幂\",\n" +
" \"note\" : \"我好美啊.\"\n" +
"}";
request.source(jsonString, XContentType.JSON);
IndexResponse indexResponse = client.index(request, RequestOptions.DEFAULT);
System.out.println(indexResponse.status());
}

@Test
public void docPost2() throws IOException {
User user = service.findById(12L);
System.out.println(user);
IndexRequest request = new IndexRequest("user");
// 设置添加的文档id值
//request.id(user.getId().toString());
request.id("1");
// 设置文档数据信息
String jsonString = JSON.toJSONString(user);
System.out.println(jsonString);
request.source(jsonString, XContentType.JSON);
IndexResponse indexResponse = client.index(request, RequestOptions.DEFAULT);
//System.out.println(indexResponse.status());
System.out.println(indexResponse.getResult());
}

结果:

1
indexResponse = CREATED

5.1.3.新增的ID一致时

我们之前测试过,新增的时候如果ID存在则变成修改,我们试试,再次执行刚才的代码,可以看到结果变了:

1
indexResponse = UPDATED

5.2.查询文档

官网地址:https://www.elastic.co/guide/en/elasticsearch/client/java-rest/7.x/java-rest-high-document-get.html

5.2.1.实现思路

这里的查询是根据id查询,必须知道文档的id才可以。

根据官网文档,实现的步骤如下:

  • 1)创建GetRequest 对象,并指定索引库名称、文档ID
  • 2)发起请求,得到结果
  • 3)从结果中得到source,是json字符串
  • 4)将JSON反序列化为对象

5.2.2.具体代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* 查询文档数据
* @throws IOException
*/
@Test
public void docGet() throws IOException {
// 获取文档对象 语义描述
GetRequest getRequest = new GetRequest("user", "1");
// 发送请求获取文档对象
GetResponse getResponse = client.get(getRequest, RequestOptions.DEFAULT);
// 获取响应结果中的数据信息
String sourceAsString = getResponse.getSourceAsString();
System.out.println(sourceAsString);
// 解析响应结果字符串
User user = JSON.parseObject(sourceAsString, User.class);
System.out.println(user);
}

结果如下:

1
2
{"age":26,"gender":"女","id":12,"name":"唐嫣","note":"唐嫣同学在传智播客学习如何耍酷"}
User(id=12, name=唐嫣, age=26, gender=女, note=唐嫣同学在传智播客学习如何耍酷)

5.3.修改文档

新增时,如果ID一致就会覆盖旧的数据,实现修改。不过,如果我们只修改文档中的某个字段,可以使用另外的API:https://www.elastic.co/guide/en/elasticsearch/client/java-rest/7.x/java-rest-high-document-update.html

5.3.1.思路

根据官网信息,修改时需要指定某个已经存在的文档的id、然后指定要修改的字段及新的值。

基本步骤如下:

  • 1.创建UpdateRequest对象,指定索引库名称、文档ID
  • 2.指定要修改的字段及属性值
  • 3.发起请求

5.3.2.代码实现

1
2
3
4
5
6
7
8
9
@Test
public void docUpdate() throws IOException {
UpdateRequest request = new UpdateRequest("user","1");
// 设置需要修改的字段信息
request.doc("name","唐嫣");
UpdateResponse updateResponse =
client.update(request, RequestOptions.DEFAULT);
System.out.println(updateResponse.getResult());
}

结果如下:

1
updateResponse = UPDATED

如果再次查询,可以发现李磊已经成功变性了:

1
user = User(id=6, name=李磊, age=23, gender=女, note=李磊同学在传智播客学Java)

5.4.删除文档

官网地址:https://www.elastic.co/guide/en/elasticsearch/client/java-rest/7.x/java-rest-high-document-delete.html

实现思路非常简单,直接根据ID删除即可:

  • 1.创建DeleteRequest对象,指定索引库名称、文档ID
  • 2.发起请求

代码实现:

1
2
3
4
5
6
@Test
public void docDelete() throws IOException {
DeleteRequest request = new DeleteRequest("user", "1");
DeleteResponse deleteResponse = client.delete(request, RequestOptions.DEFAULT);
System.out.println(deleteResponse.getResult());
}

结果:

1
deleteResponse = DELETED

5.5.批处理

如果我们需要把数据库中的所有用户信息都导入索引库,可以批量查询出多个用户,但是刚刚的新增文档是一次新增一个文档,这样效率太低了。

因此ElasticSearch提供了批处理的方案:BulkRequest

https://www.elastic.co/guide/en/elasticsearch/client/java-rest/7.x/java-rest-high-document-bulk.html

5.5.1.思路分析

A BulkRequest can be used to execute multiple index, update and/or delete operations using a single request.

一个BulkRequest可以在一次请求中执行多个 新增、更新、删除请求。

所以,BulkRequest就是把多个其它增、删、改请求整合,然后一起发送到ES来执行。

我们拿批量新增来举例,步骤如下:

  • 1.从数据库查询文档数据

  • 2.创建BulkRequest对象

  • 3.创建多个IndexRequest对象,组织文档数据,并添加到BulkRequest中

  • 4.发起请求

5.5.2.具体代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* 批量添加
* @throws IOException
*/
@Test
public void docBulk() throws IOException {
// 查询mysql中的数据信息
List<User> userList = service.findAll();
BulkRequest request = new BulkRequest("user");

for (User user : userList) {
IndexRequest indexRequest = new IndexRequest();
indexRequest.id(user.getId().toString());
String userJson = JSON.toJSONString(user);
indexRequest.source(userJson,XContentType.JSON);

request.add(indexRequest);
}

BulkResponse bulkResponse = client.bulk(request, RequestOptions.DEFAULT);
System.out.println(bulkResponse.status());
}

结果如下:

1
status: OK

可以再Kibana中看到查询的结果:

image-20200105115314222

6.Java实现查询

查询、搜索相关功能主要包括:

  • 基本查询
    • 分词查询
    • 词条查询
    • 范围查询
    • 布尔查询
    • Filter功能
  • 排序
  • 分页
  • 高亮
  • source过滤

官方文档:https://www.elastic.co/guide/en/elasticsearch/client/java-rest/7.x/java-rest-high-search.html

6.1.查询的核心API

先来看下REST风格中查询的语法:

image-20200105153514672

整个请求对象是一个大JSON对象,包含5部分属性:

  • query:查询属性
  • sort:排序属性
  • from和size:分页属性
  • highlight:高亮属性
  • aggs:聚合属性

而Java客户端,其实也是在构建这样的JSON对象。

6.1.1.SearchSourceBuilder

在Java客户端中,SearchSourceBuilder就是用来构建上面提到的大JSON对象,其中包含了5个方法:

  • query(QueryBuilder):查询条件
  • sort(String, SortOrder):排序条件
  • from(int)和size(int):分页条件
  • highlight(HighlightBuilder):高亮条件
  • aggregation(AggregationBuilder):聚合条件

如图:

image-20200105154343366

是不是与REST风格API的JSON对象一致?

接下来,再逐个来看每一个查询子属性。

6.1.2.查询条件QueryBuilders

SearchSourceBuilder的query(QueryBuilder)方法,用来构建查询条件,而查询分为:

  • 分词查询:MatchQuery
  • 词条查询:TermQuery
  • 布尔查询:BooleanQuery
  • 范围查询:RangeQuery
  • 模糊查询:FuzzyQuery

这些查询有一个统一的工具类来提供:QueryBuilders

image-20200105155220039

6.2.搜索结果API

在Kibana中看一下搜索结果:

image-20200105163216526

搜索得到的结果整体是一个JSON对象,包含下列2个属性:

  • hits:查询结果,其中又包含两个属性:
    • total:总命中数量
    • hits:查询到的文档数据,是一个数组,数组中的每个对象就包含一个文档结果,又包含:
      • _source:文档原始信息
      • highlight:高亮结果信息
  • aggregations:聚合结果对象,其中包含多个属性,属性名称由添加聚合时的名称来确定:
    • gender_agg:这个是我们创建聚合时用的聚合名称,其中包含聚合结果
      • buckets:聚合结果数组

Java客户端中的SearchResponse代表整个JSON结果

6.2.1.SearchResponse

Java客户端中的SearchResponse代表整个JSON结果,包含下面的方法:

image-20200105164513323

包含两个方法:

  • getHits():返回的是SearchHits,代表查询结果
  • getAggregations():返回的是Aggregations,代表聚合结果

6.2.2.SearchHits查询结果

SearchHits代表查询结果的JSON对象:

image-20200105171202561

包含下面的方法:

image-20200105165201949

核心方法有3个:

  • getHits():返回SearchHit数组
  • getMaxScore():返回float,文档的最大得分
  • getTotalHists():返回TotalHists,总命中数

6.2.3.SearchHit结果对象

SearchHit封装的就是结果数组中的每一个JSON对象:

image-20200105171414852

包含这样的方法:

image-20200105171625893

  • getSourceAsString():返回的是_source
  • getHighLightFields():返回是高亮结果

6.3.基本查询

6.3.1.思路分析

步骤如下:

  • 1.创建SearchSourceBuilder对象
    • 1.1.添加查询条件QueryBuilders
    • 1.2.添加排序、分页等其它条件
  • 2.创建SearchRequest对象,并制定索引库名称
  • 3.添加SearchSourceBuilder对象到SearchRequest对象中
  • 4.发起请求,得到结果
  • 5.解析结果
    • 5.1.获取总条数
    • 5.2.获取SearchHits数组,并遍历
      • 获取其中的_source,是JSON数据
      • _source反序列化为User对象

6.3.2.查询所有

QueryBuilders可以实现各种查询,比如查询所有: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
@Test
public void testBasicSearch() throws IOException {
// 1.创建SearchSourceBuilder对象
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
// 1.1.添加查询条件QueryBuilders,这里选择match_all,查询所有
sourceBuilder.query(QueryBuilders.matchAllQuery());
// 1.2.添加排序、分页等其它条件

// 2.创建SearchRequest对象,并制定索引库名称
SearchRequest request = new SearchRequest("user");
// 3.添加SearchSourceBuilder对象到SearchRequest对象中
request.source(sourceBuilder);
// 4.发起请求,得到结果
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
// 5.解析结果
SearchHits searchHits = response.getHits();
// 5.1.获取总条数
long total = searchHits.getTotalHits().value;
System.out.println("total = " + total);
// 5.2.获取SearchHits数组,并遍历
SearchHit[] hits = searchHits.getHits();
for (SearchHit hit : hits) {
// - 获取其中的`_source`,是JSON数据
String json = hit.getSourceAsString();
// - 把`_source`反序列化为User对象
User user = JSON.parseObject(json, User.class);
System.out.println("user = " + user);
}
}

结果如下:

1
2
3
4
5
6
7
8
9
10
11
total = 12
user = User(id=5, name=李娜, age=28, gender=女, note=李娜同学在传智播客学Java)
user = User(id=7, name=韩梅梅, age=24, gender=女, note=韩梅梅同学在传智播客学php)
user = User(id=2, name=李四, age=21, gender=男, note=李四同学在传智学Java)
user = User(id=3, name=王五, age=22, gender=男, note=王五同学在学php)
user = User(id=4, name=张伟, age=20, gender=男, note=张伟同学在传智播客学Java)
user = User(id=10, name=范冰冰, age=25, gender=女, note=范冰冰同学在传智播客学表演)
user = User(id=12, name=唐嫣, age=26, gender=女, note=唐嫣同学在传智播客学习如何耍酷)
user = User(id=1, name=张三, age=30, gender=男, note=张三同学在学Java)
user = User(id=6, name=李磊, age=23, gender=男, note=李磊同学在传智播客学Java)
user = User(id=8, name=柳岩, age=21, gender=女, note=柳岩同学在传智播客学表演)

6.3.3.分词查询

MatchQuery就是分词查询,会对搜索的内容分词后查询:

1
sourceBuilder.query(QueryBuilders.matchQuery("note", "唱歌表演"));

完整代码如下:

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
@Test
public void testBasicSearch() throws IOException {
// 1.创建SearchSourceBuilder对象
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
// 1.1.添加查询条件QueryBuilders,这里选择match_all,查询所有
// sourceBuilder.query(QueryBuilders.matchAllQuery());
sourceBuilder.query(QueryBuilders.matchQuery("note", "唱歌表演"));
// 1.2.添加排序、分页等其它条件

// 2.创建SearchRequest对象,并制定索引库名称
SearchRequest request = new SearchRequest("user");
// 3.添加SearchSourceBuilder对象到SearchRequest对象中
request.source(sourceBuilder);
// 4.发起请求,得到结果
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
// 5.解析结果
SearchHits searchHits = response.getHits();
// 5.1.获取总条数
long total = searchHits.getTotalHits().value;
System.out.println("total = " + total);
// 5.2.获取SearchHits数组,并遍历
SearchHit[] hits = searchHits.getHits();
for (SearchHit hit : hits) {
// - 获取其中的`_source`,是JSON数据
String json = hit.getSourceAsString();
// - 把`_source`反序列化为User对象
User user = JSON.parseObject(json, User.class);
System.out.println("user = " + user);
}
}

结果:

1
2
3
4
total = 3
user = User(id=8, name=柳岩, age=21, gender=女, note=柳岩同学在传智播客学表演)
user = User(id=10, name=范冰冰, age=25, gender=女, note=范冰冰同学在传智播客学表演)
user = User(id=9, name=刘亦菲, age=18, gender=女, note=刘亦菲同学在传智播客学唱歌)

6.3.4.布尔查询

BooleanQuery就是布尔查询,需要把其它几个查询用must、must_not组合,另外过滤条件最好使用filter来实现。比如:

1
2
3
4
5
6
7
// 布尔查询
BoolQueryBuilder queryBuilder = QueryBuilders.boolQuery();
// 添加must条件
queryBuilder.must(QueryBuilders.matchQuery("note", "唱歌表演"));
// 添加filter条件,不参与打分
queryBuilder.filter(QueryBuilders.rangeQuery("age").gte(18).lte(24));
sourceBuilder.query(queryBuilder);

完整代码:

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
/**
* bool : 布尔查询
* SearchSourceBuilder: 构建具体的查询条件
* - query(QueryBuilder):查询条件
* - sort(String, SortOrder):排序条件
* - from(int)和size(int):分页条件
* - highlight(HighlightBuilder):高亮条件
* - aggregation(AggregationBuilder):聚合条件
*/
@Test
public void search4() throws IOException {
// TODO:构建最终查询语义对象
SearchRequest searchRequest = new SearchRequest("user");
// =====构建bool查询的具体语义
// 构建检索语义
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
// 布尔检索
BoolQueryBuilder queryBuilder = QueryBuilders.boolQuery();
// 添加must条件
//queryBuilder.must(QueryBuilders.matchQuery("note", "唱歌表演"));
queryBuilder.must(QueryBuilders.matchQuery("gender", "女"));
// 添加filter条件,不影响分值
queryBuilder.filter(QueryBuilders.rangeQuery("age").gte(18).lte(24));
sourceBuilder.query(queryBuilder);
//=======
searchRequest.source(sourceBuilder);

SearchResponse searchResponse = client.search(searchRequest, RequestOptions.DEFAULT);
// 获取所有的返回结果
SearchHits hits = searchResponse.getHits();
TotalHits totalHits = hits.getTotalHits();
long total = totalHits.value;
System.out.println("总条数 : "+total);

SearchHit[] dataHits = hits.getHits();
for (SearchHit data : dataHits) {
User user = JSON.parseObject(data.getSourceAsString(),User.class);
System.out.println(user);
}

}

结果:

1
2
3
total = 2
user = User(id=8, name=柳岩, age=21, gender=女, note=柳岩同学在传智播客学表演)
user = User(id=9, name=刘亦菲, age=18, gender=女, note=刘亦菲同学在传智播客学唱歌)

6.4.source过滤

在原来搜索的基础上,通过SearchSourceBuilder的fetchSource(String[] includes, String[] excludes)方法实现:

  • includes:包含的字段
  • excludes:要排除的字段

代码:

1
2
// 2.source过滤,指定includes,只要id、name、note
sourceBuilder.fetchSource(new String[]{"id", "name", "note"}, new String[0]);

完整代码:

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
@Test
public void testBasicSearch() throws IOException {
// 1.创建SearchSourceBuilder对象
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
// 1.1.添加查询条件QueryBuilders,这里选择match_all,查询所有
sourceBuilder.query(QueryBuilders.matchQuery("note", "唱歌表演"));

// 2.source过滤,指定includes,只要id、name、note
sourceBuilder.fetchSource(new String[]{"id", "name", "note"}, new String[0]);

// 3.创建SearchRequest对象,并制定索引库名称
SearchRequest request = new SearchRequest("user");
// 4.添加SearchSourceBuilder对象到SearchRequest对象中
request.source(sourceBuilder);
// 5.发起请求,得到结果
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
// 6.解析结果
SearchHits searchHits = response.getHits();
// 6.1.获取总条数
long total = searchHits.getTotalHits().value;
System.out.println("total = " + total);
// 6.2.获取SearchHits数组,并遍历
SearchHit[] hits = searchHits.getHits();
for (SearchHit hit : hits) {
// - 获取其中的`_source`,是JSON数据
String json = hit.getSourceAsString();
// - 把`_source`反序列化为User对象
User user = JSON.parseObject(json, User.class);
System.out.println("user = " + user);
}
}

结果:

1
2
3
4
total = 3
user = User(id=8, name=柳岩, age=null, gender=null, note=柳岩同学在传智播客学表演)
user = User(id=10, name=范冰冰, age=null, gender=null, note=范冰冰同学在传智播客学表演)
user = User(id=9, name=刘亦菲, age=null, gender=null, note=刘亦菲同学在传智播客学唱歌)

6.5.排序

6.5.1.API介绍

通过SearchSourceBuilder的sort(String, SortOrder)方法用来实现排序条件的封装:

1
2
3
4
5
6
7
8
9
/**
* Adds a sort against the given field name and the sort ordering.
*
* @param name The name of the field,排序字段名称
* @param order The sort ordering,排序的方式
*/
public SearchSourceBuilder sort(String name, SortOrder order) {
// ...
}

其中的SortOrder是一个枚举,包含ASC和DESC两个枚举项:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public enum SortOrder implements Writeable {
/**
* Ascending order.
*/
ASC {
// ..
},
/**
* Descending order.
*/
DESC {
// ..
};
// ...
}

6.5.2.具体实现

在原由查询的基础上,给SearchSourceBuilder中添加sort即可:

1
2
//  1.2.添加排序条件
sourceBuilder.sort("id", SortOrder.ASC);

完整代码如下:

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
/**
* order
* SearchSourceBuilder: 构建具体的查询条件
* - query(QueryBuilder):查询条件
* - sort(String, SortOrder):排序条件
* - from(int)和size(int):分页条件
* - highlight(HighlightBuilder):高亮条件
* - aggregation(AggregationBuilder):聚合条件
*/
@Test
public void searchOrder() throws IOException {
// 描述查询的语义
SearchRequest searchRequest = new SearchRequest("user");
// 构建查询条件 五大类
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
searchSourceBuilder.query(QueryBuilders.matchAllQuery());
// 排序
//searchSourceBuilder.sort(new ScoreSortBuilder().order(SortOrder.DESC));
searchSourceBuilder.sort(new FieldSortBuilder("id").order(SortOrder.DESC));

// 将查询条件对象赋给查询语义对象
searchRequest.source(searchSourceBuilder);

SearchResponse searchResponse =
client.search(searchRequest, RequestOptions.DEFAULT);

// 获取所有的返回结果
SearchHits hits = searchResponse.getHits();
TotalHits totalHits = hits.getTotalHits();
long total = totalHits.value;
System.out.println("总条数 : "+total);

SearchHit[] dataHits = hits.getHits();
for (SearchHit data : dataHits) {
User user = JSON.parseObject(data.getSourceAsString(),User.class);
System.out.println(user);
}

}

查询结果:

1
2
3
4
5
6
7
8
9
10
11
total = 12
user = User(id=1, name=张三, age=30, gender=男, note=张三同学在学Java)
user = User(id=2, name=李四, age=21, gender=男, note=李四同学在传智学Java)
user = User(id=3, name=王五, age=22, gender=男, note=王五同学在学php)
user = User(id=4, name=张伟, age=20, gender=男, note=张伟同学在传智播客学Java)
user = User(id=5, name=李娜, age=28, gender=女, note=李娜同学在传智播客学Java)
user = User(id=6, name=李磊, age=23, gender=男, note=李磊同学在传智播客学Java)
user = User(id=7, name=韩梅梅, age=24, gender=女, note=韩梅梅同学在传智播客学php)
user = User(id=8, name=柳岩, age=21, gender=女, note=柳岩同学在传智播客学表演)
user = User(id=9, name=刘亦菲, age=18, gender=女, note=刘亦菲同学在传智播客学唱歌)
user = User(id=10, name=范冰冰, age=25, gender=女, note=范冰冰同学在传智播客学表演)

6.5.分页

在原由查询的基础上,给SearchSourceBuilder中添加from和size即可。例如我们的分页信息是:

page = 1,size = 5,代表查询第一页,每页5条,可以计算出: from = (page - 1) * size = 0

所以,代码如下:

1
2
3
4
5
// 1.3.添加分页条件
int page = 1, size = 5;
int from = (page - 1) * size;
sourceBuilder.from(from);
sourceBuilder.size(size);

完整代码:

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
/**
* 分页
* SearchSourceBuilder: 构建具体的查询条件
* - query(QueryBuilder):查询条件
* - sort(String, SortOrder):排序条件
* - from(int)和size(int):分页条件
* - highlight(HighlightBuilder):高亮条件
* - aggregation(AggregationBuilder):聚合条件
*/
@Test
public void searchPage() throws IOException {
// 描述查询的语义
SearchRequest searchRequest = new SearchRequest("user");
// 构建查询条件 五大类
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
searchSourceBuilder.query(QueryBuilders.matchAllQuery());
// 排序
searchSourceBuilder.sort(new FieldSortBuilder("id").order(SortOrder.DESC));

// 起始索引
searchSourceBuilder.from(0);
searchSourceBuilder.size(4);
// 将查询条件对象赋给查询语义对象
searchRequest.source(searchSourceBuilder);

SearchResponse searchResponse =
client.search(searchRequest, RequestOptions.DEFAULT);

// 获取所有的返回结果
SearchHits hits = searchResponse.getHits();
TotalHits totalHits = hits.getTotalHits();
long total = totalHits.value;
System.out.println("总条数 : "+total);

SearchHit[] dataHits = hits.getHits();
for (SearchHit data : dataHits) {
User user = JSON.parseObject(data.getSourceAsString(),User.class);
System.out.println(user);
}

}

结果如下:

1
2
3
4
5
6
total = 12
user = User(id=1, name=张三, age=30, gender=男, note=张三同学在学Java)
user = User(id=2, name=李四, age=21, gender=男, note=李四同学在传智学Java)
user = User(id=3, name=王五, age=22, gender=男, note=王五同学在学php)
user = User(id=4, name=张伟, age=20, gender=男, note=张伟同学在传智播客学Java)
user = User(id=5, name=李娜, age=28, gender=女, note=李娜同学在传智播客学Java)

6.6.高亮

6.6.1.开启高亮

高亮需要在SearchSourceBuilder的highlighter()方法来实现:

1
2
// 2.高亮,指定高亮字段
sourceBuilder.highlighter(new HighlightBuilder().field("note"));

完整代码:

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
@Test
public void testHighlight() throws IOException {
// 1.创建SearchSourceBuilder对象
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
// 1.1.添加查询条件QueryBuilders
sourceBuilder.query(QueryBuilders.matchQuery("note", "唱歌表演"));

// 2.高亮,指定高亮字段
sourceBuilder.highlighter(new HighlightBuilder().field("note"));

// 3.创建SearchRequest对象,并制定索引库名称
SearchRequest request = new SearchRequest("user");
// 4.添加SearchSourceBuilder对象到SearchRequest对象中
request.source(sourceBuilder);
// 5.发起请求,得到结果
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
// 6.解析结果
SearchHits searchHits = response.getHits();
// 6.1.获取总条数
long total = searchHits.getTotalHits().value;
System.out.println("total = " + total);
// 6.2.获取SearchHits数组,并遍历
SearchHit[] hits = searchHits.getHits();
for (SearchHit hit : hits) {
// - 获取其中的`_source`,是JSON数据
String json = hit.getSourceAsString();
// - 把`_source`反序列化为User对象
User user = JSON.parseObject(json, User.class);
System.out.println("user = " + user);
}
}

运行,查看结果:

1
2
3
4
total = 3
user = User(id=8, name=柳岩, age=21, gender=女, note=柳岩同学在传智播客学表演)
user = User(id=10, name=范冰冰, age=25, gender=女, note=范冰冰同学在传智播客学表演)
user = User(id=9, name=刘亦菲, age=18, gender=女, note=刘亦菲同学在传智播客学唱歌)

结果并未高亮,为什么?

这是因为查询结果中,文档数据和高亮数据是分离的:

image-20200105184133923

我们需要自己在搜索结果中解析高亮结果

6.6.2.解析高亮结果

搜索结果SearchHit对象中,包含两个方法:

  • getSourceAsString():返回的是_source
  • getHighLightFields():返回是高亮结果,Map<String, HighlightField>,map的key是高亮字段名称

解析SearchHit并高亮的代码如下:

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
/**
* 高亮
* SearchSourceBuilder: 构建具体的查询条件
* - query(QueryBuilder):查询条件
* - sort(String, SortOrder):排序条件
* - from(int)和size(int):分页条件
* - highlight(HighlightBuilder):高亮条件
* - aggregation(AggregationBuilder):聚合条件
*/
@Test
public void searchHighlight() throws IOException {
// 描述查询的语义
SearchRequest searchRequest = new SearchRequest("user");
// 构建查询条件 五大类
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
searchSourceBuilder.query(QueryBuilders.matchQuery("note","唱歌跳舞"));
// 高亮
searchSourceBuilder.highlighter(new HighlightBuilder().field("note"));

// 将查询条件对象赋给查询语义对象
searchRequest.source(searchSourceBuilder);

SearchResponse searchResponse =
client.search(searchRequest, RequestOptions.DEFAULT);

// 获取所有的返回结果
SearchHits hits = searchResponse.getHits();
TotalHits totalHits = hits.getTotalHits();
long total = totalHits.value;
System.out.println("总条数 : "+total);

SearchHit[] dataHits = hits.getHits();
for (SearchHit data : dataHits) {
User user = JSON.parseObject(data.getSourceAsString(),User.class);

// TODO:获取高亮结果
Map<String, HighlightField> highlightFields = data.getHighlightFields();
// 根据字段名,获取字段值
HighlightField highlightField = highlightFields.get("note");
// 获取高亮结果的片段数组
Text[] fragments = highlightField.getFragments();
// 拼接成字符串
String note = StringUtils.join(fragments);
System.out.println(note);
// 覆盖旧值
user.setNote(note);

System.out.println(user);
}

}

运行结果:

1
2
3
4
total = 3
user = User(id=8, name=柳岩, age=21, gender=女, note=柳岩同学在传智播客学<em>表演</em>)
user = User(id=10, name=范冰冰, age=25, gender=女, note=范冰冰同学在传智播客学<em>表演</em>)
user = User(id=9, name=刘亦菲, age=18, gender=女, note=刘亦菲同学在传智播客学<em>唱歌</em>)

7.Java实现聚合

聚合功能通过SearchSourceBuilder的aggregation(AggregationBuilder aggregation)方法用来构建聚合条件:

1
2
3
4
5
6
/**
* Add an aggregation to perform as part of the search.
*/
public SearchSourceBuilder aggregation(AggregationBuilder aggregation) {
// ..
}

其中要用到的各种聚合如:

  • Term聚合
  • Rang聚合
  • Sum聚合

等都通过AggregationBuilders来提供:

image-20200105160257481

7.1.添加聚合条件

举例,假如对性别字段gender做聚合,代码如下:

1
sourceBuilder.aggregation(AggregationBuilders.terms("gender_agg").field("gender"));
  • terms(String):确定聚合类型是Term类型
  • term(“gender_agg”):给聚合起个名字,要唯一,获取聚合结果以名称获取。
  • field(“gender”):确定要聚合的字段名称,这里是gender

完整请求:

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
/**
* 聚合
* @throws IOException
*/
@Test
public void searchAggs() throws IOException {
// 描述查询的语义
SearchRequest searchRequest = new SearchRequest("user");
// 构建查询条件 五大类
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
searchSourceBuilder.query(QueryBuilders.matchAllQuery());
// 聚合分桶
searchSourceBuilder.aggregation(AggregationBuilders.terms("gender_agg").field("gender"));
// 将查询条件对象赋给查询语义对象
searchRequest.source(searchSourceBuilder);
SearchResponse searchResponse =
client.search(searchRequest, RequestOptions.DEFAULT);

// 获取所有的返回结果
SearchHits hits = searchResponse.getHits();
TotalHits totalHits = hits.getTotalHits();
long total = totalHits.value;
System.out.println("总条数 : "+total);

SearchHit[] dataHits = hits.getHits();
for (SearchHit data : dataHits) {
User user = JSON.parseObject(data.getSourceAsString(),User.class);
System.out.println(user);
}
System.out.println("===============================");
// 解析结果
Aggregations aggregations = searchResponse.getAggregations();
// 根据桶名获取数据信息
Terms terms = aggregations.get("gender_agg");

List<? extends Terms.Bucket> buckets = terms.getBuckets();
for (Terms.Bucket bucket : buckets) {
// 6.4.获取key
String key = bucket.getKeyAsString();
System.out.println("key = " + key);
// 6.5.获取count
long count = bucket.getDocCount();
System.out.println("count = " + count);
}

}

7.2.解析聚合结果

聚合结果是一个JSON对象,如图:

image-20200106115813370

对象的属性是聚合的名称,可以有多个。因此获取聚合要以聚合名称获取,代码如下:

1
2
3
4
5
// 6.解析结果
Aggregations aggregations = response.getAggregations();

// 7.根据聚合名称获取聚合结果
Aggregation aggregation = aggregations.get("gender_agg");

返回值Aggregation是一个接口,包含很多不同实现:

image-20200106150546712

因为我们上面采用的是Term聚合,因此结果应该用Terms这个子接口来接收,然后就可以从中获取到Buckets数组,代码如下:

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
/**
* 聚合
* @throws IOException
*/
@Test
public void searchAggs() throws IOException {
// 描述查询的语义
SearchRequest searchRequest = new SearchRequest("user");
// 构建查询条件 五大类
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
searchSourceBuilder.query(QueryBuilders.matchAllQuery());
// 聚合分桶
searchSourceBuilder.aggregation(AggregationBuilders.terms("gender_agg").field("gender"));
// 将查询条件对象赋给查询语义对象
searchRequest.source(searchSourceBuilder);
SearchResponse searchResponse =
client.search(searchRequest, RequestOptions.DEFAULT);

// 获取所有的返回结果
SearchHits hits = searchResponse.getHits();
TotalHits totalHits = hits.getTotalHits();
long total = totalHits.value;
System.out.println("总条数 : "+total);

SearchHit[] dataHits = hits.getHits();
for (SearchHit data : dataHits) {
User user = JSON.parseObject(data.getSourceAsString(),User.class);
System.out.println(user);
}
System.out.println("===============================");
// 解析结果
Aggregations aggregations = searchResponse.getAggregations();
// 根据桶名获取数据信息
Terms terms = aggregations.get("gender_agg");

List<? extends Terms.Bucket> buckets = terms.getBuckets();
for (Terms.Bucket bucket : buckets) {
// 6.4.获取key
String key = bucket.getKeyAsString();
System.out.println("key = " + key);
// 6.5.获取count
long count = bucket.getDocCount();
System.out.println("count = " + count);
}

}

打印结果:

1
2
3
4
key = 女
count = 7
key = 男
count = 5

总结

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
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
排序: 对查询结果进行排序(默认情况下根据分值排序)
GET /{索引库名称}/_search
{
"query": { ... },
"sort": [
{
"{排序字段}": {
"order": "{asc或desc}"
}
}
]
}
高亮: 给搜索的词条添加一个标记,通过给指定的标记添加样式,实现高亮效果
GET /heima/_search
{
"query": {
"match": {
"title": "手机"
}
},
"highlight": {
"pre_tags": "<em>",
"post_tags": "</em>",
"fields": {
"title": {}
}
}
}
分页: 将查询结果进行逻辑分页(查询到了所有满足条件的数据,只是展示一部分)
GET /heima/_search
{
"query": {
"match_all": {}
},
"sort": [
{
"price": {
"order": "asc"
}
}
],
"from": 0, // 起始索引
"size": 2 // 每页显示的条数
}
Filter过滤: 不会影响词条查询时的分值
GET /heima/_search
{
"query": {
"bool": {
"must": {
"match": {
"title": "小米手机"
}
},
"filter": [
{
"range": {
"price": {
"gt": 2000,
"lt": 3200
}
}
}
]
}
}
}
_source筛选: 展示指定的查询结果字段
GET /heima/_search
{
"_source": {
"includes":["title","price"]
},
"query": {
"term": {
"price": 2699
}
}
}
聚合:
聚合分桶: 类似于mysql中的分组
聚合度量: 类似于mysql中的聚合函数
GET car/_search
{
"size": 0,
"aggs": {
"colors_name": {
"terms": {
"field": "color"
},
"aggs": {
"make_name": {
"terms": {
"field": "make"
},
"aggs": {
"max_name": {
"max": {
"field": "price"
}
}
}
},
"min_name":{
"min": {
"field": "price"
}
}
}
}
}
}
1
2
3
4
5
6
7
8
9
10
11
ES启动后就是一个web服务,直接通过浏览器发送基于http协议Rest格式的路径即可.
Kibana: 客户端,发送请求个ES,从而实现操作ES
JavaAPI:
在java代码中发送基于HTTP协议Rest格式的请求.
API方法较多,使用时查询