大数据开发工程师-全文检索引擎Elasticsearch-2


全文检索引擎Elasticsearch-2

3 Elasticsearch分词详解

ES分词介绍

1
2
3
4
5
6
7
8
ES中在添加数据,也就是创建索引的时候,会先对数据进行分词。
在查询索引数据的时候,也会先根据查询的关键字进行分词。
所以在ES中分词这个过程是非常重要的,涉及到查询的效率和准确度。

假设有一条数据,数据中有一个字段是titile,这个字段的值为LexCorp BFG-9000。
我们想要把这条数据在ES中创建索引,方便后期检索。

创建索引和查询索引的大致流程是这样的:

image-20230611145714539

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
图中左侧是创建索引的过程:
首先对数据进行空白字符分割,将LexCorp BFG-9000切分为LexCorp和BFG-9000。
然后进行单词切割,将LexCorp切分为Lex和Corp,BFG-9000切分为BFG和9000。
最后执行小写转换操作,将英文单词全部转换为小写。

图中右侧是查询索引的过程:
后期想要查询LexCorp BFG-9000这条数据,但是具体的内容记不清了,大致想起来了一些关键词Lex corp bfg9000。
接下来就根据这些关键词进行查询,
首先还是对数据进行空白符分割,将Lex corp bfg9000切分为Lex、corp 和bfg9000。
然后进行单词切割,Lex和corp不变,将bfg9000切分为bfg和9000。
最后执行小写转换操作,将英文单词全部转换为小写。
这样其实在检索的时候就可以忽略英文大小写了,因为前面在创建索引的时候也会对英文进行小写转换。

到这可以发现,使用Lex corp bfg9000是可以查找到LexCorp BFG-9000这条数据的,因为在经过空白符分割、单词切割、小写转换之后,这两条数据是一样的,其实只要能有一个单词是匹配的,就可以把这条数据查找出来。

了解了这个流程之后,我们以后在搜索引擎里面搜索一些内容的时候其实就知道要怎么快速高效的检索内容了,只需要输入一些关键词,中间最好用空格隔开,针对英文字符不用纠结大小写了。

这些数据在ES中分词之后,其实在底层会产生倒排索引,注意了,倒排索引是ES能够提供快速检索能力的核心,下面来看一下这个倒排索引

倒排索引介绍

1
假设有一批数据,数据中有两个字段,文档编号和文档内容。

image-20230611150131888

1
针对这一批数据,在ES中创建索引之后,最终产生的倒排索引内容大致是这样的:

image-20230611150153996

1
2
3
4
5
6
7
8
9
解释:

单词ID:记录每个单词的单词编号。
单词:对应的单词。
文档频率:代表文档集合中有多少个文档包含某个单词。
倒排列表:包含单词ID及其它必要信息。
DocId:单词出现的文档id。
TF:单词在某个文档中出现的次数。
POS:单词在文档中出现的位置。
1
2
以单词 加盟 为例,其单词编号为6,文档频率为3,代表整个文档集合中有3个文档包含这个单词,对应的倒排列表为{(2;1;<4>),(3;1;<7>),(5;1;<5>)},含义是在文档2,3,5中出现过这个单词,在每个文档中都只出现过1次,单词 加盟 在第一个文档的POS(位置)是4,即文档的第四个单词是 加盟 ,其它的类似。
这个倒排索引已经是一个非常完备的索引系统,实际搜索系统的索引结构基本如此。

分词器的作用

1
2
3
4
5
6
7
8
9
前面分析了ES在创建索引和查询索引的时候都需要进行分词,分词需要用到分词器。下面来具体分析一下分词器的作用:

分词器的作用是把一段文本中的词按照一定规则进行切分。

分词器对应的是Analyzer类,这是一个抽象类,切分词的具体规则是由子类实现的。
也就是说不同的分词器分词的规则是不同的!

所以对于不同的语言,要用不同的分词器。
在创建索引时会用到分词器,在搜索时也会用到分词器,这两个地方要使用同一个分词器,否则可能会搜索不出结果。

分词器的工作流程

1
2
3
4
5
6
7
分词器的工作流程一般是这样的:

1.切分关键词,把关键的、核心的单词切出来。
2.去除停用词。
3.对于英文单词,把所有字母转为小写(搜索时不区分大小写)

针对停用词下面来详细分析一下:
停用词
1
2
3
4
5
6
7
8
9
10
11
12
有些词在文本中出现的频率非常高,但是对文本所携带的信息基本不产生影响。
例如:
英文停用词:a、an、the、of等
中文停用词:的、了、着、是、标点符号等

文本经过分词之后,停用词通常被过滤掉,不会被进行索引。
在检索的时候,用户的查询中如果含有停用词,检索系统也会将其过滤掉(因为用户输入的查询字符串也要进行分词处理)。
排除停用词可以加快建立索引的速度,减小索引库文件的大小,并且还可以提高查询的准确度。
如果不去除停用词,可能会存在这个情况:
假设有一批文章数据,基本上每篇文章里面都有的这个词,那我在检索的时候只要输入了的这个词,那么所有文章都认为是满足条件的数据,但是这样是没有意义的。

常见的英文停用词汇总:
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
a
about
above
after
again
against
all
am
an
and
any
are
aren't
as
at
be
because
been
before
being
below
between
both
but
by
can't
cannot
could
couldn't
did
didn't
do
does
doesn't
doing
don't
down
during
each
few
for
from
further
had
hadn't
has
hasn't
have
haven't
having
he
he'd
he'll
he's
her
here
here's
hers
herself
him
himself
his
how
how's
i
i'd
i'll
i'm
i've
if
in
into
is
isn't
it
it's
its
itself
let's
me
more
most
mustn't
my
myself
no
nor
not
of
off
on
once
only
or
other
ought
our
ours
ourselves
out
over
own
same
shan't
she
she'd
she'll
she's
should
shouldn't
so
some
such
than
that
that's
the
their
theirs
them
themselves
then
there
there's
these
they
they'd
they'll
they're
they've
this
those
through
to
too
under
until
up
very
was
wasn't
we
we'd
we'll
we're
we've
were
weren't
what
what's
when
when's
where
where's
which
while
who
who's
whom
why
why's
with
won't
would
wouldn't
you
you'd
you'll
you're
you've
your
yours
yourself
yourselves
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
常见的中文停用词汇总:




































































































使












沿












中文分词方式
1
2
3
4
5
6
7
针对中文而言,在分词的时候有多种分词规则:
常见的有单字分词、二分法分词、词库分词等
单字分词:"我"、"们"、"是"、"中"、"国"、"人"
二分法分词:"我们"、"们是"、"是中"、"中国"、"国人"。
词库分词:按照某种算法构造词,然后去匹配已建好的词库集合,如果匹配到就切分出来成为词语。

从这里面可以看出来,其实最理想的中文分词方式是词库分词。
常见的中文分词器
1
针对前面分析的几种中文分词方式,对应的有一些已经实现好的中分分词器。

image-20230611151020118

1
在词库分词方式领域里面,最经典的就是IK分词器,你懂得!

ES中文分词插件(es-ik)

1
2
3
4
在中文数据检索场景中,为了提供更好的检索效果,需要在ES中集成中文分词器,因为ES默认是按照英文的分词规则进行分词的,基本上可以认为是单字分词,对中文分词效果不理想。
ES之前是没有提供中文分词器的,现在官方也提供了一些,但是在中文分词领域,IK分词器是不可撼动的,所以在这里我们主要讲一下如何在ES中集成IK这个中文分词器。
首先下载es-ik插件,需要到github上下载。
https://github.com/medcl/elasticsearch-analysis-ik

image-20230611151532296

image-20230611151550453

1
2
3
4
最终的下载地址为:
https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v7.13.4/elasticsearch-analysis-ik-7.13.4.zip

注意:在ES中安装IK插件的时候,需要在ES集群的所有节点中都安装。
1
2
3
4
5
6
7
1:将下载好的elasticsearch-analysis-ik-7.13.4.zip上传到bigdata01的/data/soft/ elasticsearch-7.13.4目录中。
[root@bigdata01 elasticsearch-7.13.4]# ll elasticsearch-analysis-ik-7.13.4.zip
-rw-r--r--. 1 root root 4504502 Sep 3 2021 elasticsearch-analysis-ik-7.13.4.zip

2:将elasticsearch-analysis-ik-7.13.4.zip远程拷贝到bigdata02和bigdata03上。
[root@bigdata01 elasticsearch-7.13.4]# scp -rq elasticsearch-analysis-ik-7.13.4.zip bigdata02:/data/soft/elasticsearch-7.13.4
[root@bigdata01 elasticsearch-7.13.4]# scp -rq elasticsearch-analysis-ik-7.13.4.zip bigdata03:/data/soft/elasticsearch-7.13.4
1
2
3:在bigdata01节点离线安装IK插件。
[root@bigdata01 elasticsearch-7.13.4]# bin/elasticsearch-plugin install file:///data/soft/elasticsearch-7.13.4/elasticsearch-analysis-ik-7.13.4.zip
1
2
3
4
5
注意:在安装的过程中会有警告信息提示需要输入y确认继续向下执行。

最后看到如下内容就表示安装成功了。
-> Installed analysis-ik
-> Please restart Elasticsearch to activate any plugins installed
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
config目录下面的analysis-ik里面存储的是ik的配置文件信息。

[root@bigdata01 elasticsearch-7.13.4]# cd config/
[root@bigdata01 config]# ll analysis-ik/
total 8260
-rwxrwxrwx. 1 root root 5225922 Feb 27 20:57 extra_main.dic
-rwxrwxrwx. 1 root root 63188 Feb 27 20:57 extra_single_word.dic
-rwxrwxrwx. 1 root root 63188 Feb 27 20:57 extra_single_word_full.dic
-rwxrwxrwx. 1 root root 10855 Feb 27 20:57 extra_single_word_low_freq.dic
-rwxrwxrwx. 1 root root 156 Feb 27 20:57 extra_stopword.dic
-rwxrwxrwx. 1 root root 625 Feb 27 20:57 IKAnalyzer.cfg.xml
-rwxrwxrwx. 1 root root 3058510 Feb 27 20:57 main.dic
-rwxrwxrwx. 1 root root 123 Feb 27 20:57 preposition.dic
-rwxrwxrwx. 1 root root 1824 Feb 27 20:57 quantifier.dic
-rwxrwxrwx. 1 root root 164 Feb 27 20:57 stopword.dic
-rwxrwxrwx. 1 root root 192 Feb 27 20:57 suffix.dic
-rwxrwxrwx. 1 root root 752 Feb 27 20:57 surname.dic
1
2
3
4
5
6
7
8
9
10
11
12
plugins目录下面的analysis-ik里面存储的是ik的核心jar包。

[root@bigdata01 elasticsearch-7.13.4]# cd plugins/
[root@bigdata01 plugins]# ll analysis-ik/
total 1428
-rwxrwxrwx. 1 root root 263965 Feb 27 20:56 commons-codec-1.9.jar
-rwxrwxrwx. 1 root root 61829 Feb 27 20:56 commons-logging-1.2.jar
-rwxrwxrwx. 1 root root 54626 Feb 27 20:56 elasticsearch-analysis-ik-7.13.4.jar
-rwxrwxrwx. 1 root root 736658 Feb 27 20:56 httpclient-4.5.2.jar
-rwxrwxrwx. 1 root root 326724 Feb 27 20:56 httpcore-4.4.4.jar
-rwxrwxrwx. 1 root root 1807 Feb 27 20:56 plugin-descriptor.properties
-rwxrwxrwx. 1 root root 125 Feb 27 20:56 plugin-security.policy
1
2
3
4:在bigdata02节点离线安装IK插件。

5:在bigdata03节点离线安装IK插件。
1
2
3
4
5
6
7
8
9
10
6:如果集群正在运行,则需要停止集群。
在bigdata01上停止。
[root@bigdata01 elasticsearch-7.13.4]# jps
1680 Elasticsearch
2047 Jps
[root@bigdata01 elasticsearch-7.13.4]# kill 1680

在bigdata02上停止。

在bigdata03上停止。
1
2
3
4
5
6
7
8
9
7:修改elasticsearch-7.13.4的plugins目录下analysis-ik子目录的权限。
直接修改elasticsearch-7.13.4目录的权限即可。
在bigdata01上执行。

[root@bigdata01 elasticsearch-7.13.4]# cd ..
[root@bigdata01 soft]# chmod -R 777 elasticsearch-7.13.4

在bigdata02上执行。
在bigdata03上执行。
1
2
3
4
5
6
7
8
8:重新启动ES集群。
在bigdata01上执行。
[root@bigdata01 soft]# su es
[es@bigdata01 soft]$ cd /data/soft/elasticsearch-7.13.4
[es@bigdata01 elasticsearch-7.13.4]$ bin/elasticsearch -d

在bigdata02上执行。
在bigdata03上执行。
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
9:验证IK的分词效果。
首先使用默认分词器测试中文分词效果。

[root@bigdata01 soft]# curl -H "Content-Type: application/json" -XPOST 'http://bigdata01:9200/emp/_analyze?pretty' -d '{"text":"我们是中国人"}'
{
"tokens" : [
{
"token" : "我",
"start_offset" : 0,
"end_offset" : 1,
"type" : "<IDEOGRAPHIC>",
"position" : 0
},
{
"token" : "们",
"start_offset" : 1,
"end_offset" : 2,
"type" : "<IDEOGRAPHIC>",
"position" : 1
},
{
"token" : "是",
"start_offset" : 2,
"end_offset" : 3,
"type" : "<IDEOGRAPHIC>",
"position" : 2
},
{
"token" : "中",
"start_offset" : 3,
"end_offset" : 4,
"type" : "<IDEOGRAPHIC>",
"position" : 3
},
{
"token" : "国",
"start_offset" : 4,
"end_offset" : 5,
"type" : "<IDEOGRAPHIC>",
"position" : 4
},
{
"token" : "人",
"start_offset" : 5,
"end_offset" : 6,
"type" : "<IDEOGRAPHIC>",
"position" : 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
然后使用IK分词器测试中文分词效果。

[root@bigdata01 soft]# curl -H "Content-Type: application/json" -XPOST 'http://bigdata01:9200/emp/_analyze?pretty' -d '{"text":"我们是中国人","tokenizer":"ik_max_word"}'
{
"tokens" : [
{
"token" : "我们",
"start_offset" : 0,
"end_offset" : 2,
"type" : "CN_WORD",
"position" : 0
},
{
"token" : "是",
"start_offset" : 2,
"end_offset" : 3,
"type" : "CN_CHAR",
"position" : 1
},
{
"token" : "中国人",
"start_offset" : 3,
"end_offset" : 6,
"type" : "CN_WORD",
"position" : 2
},
{
"token" : "中国",
"start_offset" : 3,
"end_offset" : 5,
"type" : "CN_WORD",
"position" : 3
},
{
"token" : "国人",
"start_offset" : 4,
"end_offset" : 6,
"type" : "CN_WORD",
"position" : 4
}
]
}
1
2
3
4
在这里我们发现分出来的单词里面有一个是,这个单词其实可以认为是一个停用词,在分词的时候是不需要切分出来的。
在这被切分出来了,那也就意味着在进行停用词过滤的时候没有过滤掉。

针对ik这个词库而言,它的停用词词库里面都有哪些单词呢?
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
[root@bigdata01 elasticsearch-7.13.4]# cd config/analysis-ik/
[root@bigdata01 analysis-ik]# ll
total 8260
-rwxrwxrwx. 1 root root 5225922 Feb 27 20:57 extra_main.dic
-rwxrwxrwx. 1 root root 63188 Feb 27 20:57 extra_single_word.dic
-rwxrwxrwx. 1 root root 63188 Feb 27 20:57 extra_single_word_full.dic
-rwxrwxrwx. 1 root root 10855 Feb 27 20:57 extra_single_word_low_freq.dic
-rwxrwxrwx. 1 root root 156 Feb 27 20:57 extra_stopword.dic
-rwxrwxrwx. 1 root root 625 Feb 27 20:57 IKAnalyzer.cfg.xml
-rwxrwxrwx. 1 root root 3058510 Feb 27 20:57 main.dic
-rwxrwxrwx. 1 root root 123 Feb 27 20:57 preposition.dic
-rwxrwxrwx. 1 root root 1824 Feb 27 20:57 quantifier.dic
-rwxrwxrwx. 1 root root 164 Feb 27 20:57 stopword.dic
-rwxrwxrwx. 1 root root 192 Feb 27 20:57 suffix.dic
-rwxrwxrwx. 1 root root 752 Feb 27 20:57 surname.dic
[root@bigdata01 analysis-ik]# more stopword.dic
a
an
and
are
as
at
be
but
by
for
if
in
into
is
it
no
not
of
on
or
1
2
ik的停用词词库是stopword.dic这个文件,这个文件里面目前都是一些英文停用词。
我们可以手工在这个文件中把中文停用词添加进去,先添加 是 这个停用词。
1
2
3
[root@bigdata01 analysis-ik]# vi stopword.dic 
.....

1
2
3
4
然后把这个文件的改动同步到集群中的所有节点上。

[root@bigdata01 analysis-ik]# scp -rq stopword.dic bigdata02:/data/soft/elasticsearch-7.13.4/config/analysis-ik/
[root@bigdata01 analysis-ik]# scp -rq stopword.dic bigdata03:/data/soft/elasticsearch-7.13.4/config/analysis-ik/
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
重启集群让配置生效。

再使用IK分词器测试一下中文分词效果。

[root@bigdata01 analysis-ik]# curl -H "Content-Type: application/json" -XPOST 'http://bigdata01:9200/test/_analyze?pretty' -d '{"text":"我们是中国人","tokenizer":"ik_max_word"}'
{
"tokens" : [
{
"token" : "我们",
"start_offset" : 0,
"end_offset" : 2,
"type" : "CN_WORD",
"position" : 0
},
{
"token" : "中国人",
"start_offset" : 3,
"end_offset" : 6,
"type" : "CN_WORD",
"position" : 1
},
{
"token" : "中国",
"start_offset" : 3,
"end_offset" : 5,
"type" : "CN_WORD",
"position" : 2
},
{
"token" : "国人",
"start_offset" : 4,
"end_offset" : 6,
"type" : "CN_WORD",
"position" : 3
}
]
}
1
此时再查看会发现没有"是" 这个单词了,相当于在过滤停用词的时候把它过滤掉了。

es-ik添加自定义词库

自定义词库

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
针对一些特殊的词语在分词的时候也需要能够识别。
例如:公司产品的名称或者网络上新流行的词语
假设我们公司开发了一款新产品,命名为:数据大脑,我们希望ES在分词的时候能够把这个产品名称直接识别成一个词语。
现在使用ik分词器测试一下分词效果:

[root@bigdata01 ~]$ curl -H "Content-Type: application/json" -XPOST 'http://bigdata01:9200/test/_analyze?pretty' -d '{"text":"数据大脑","tokenizer":"ik_max_word"}'
{
"tokens" : [
{
"token" : "数据",
"start_offset" : 0,
"end_offset" : 2,
"type" : "CN_WORD",
"position" : 0
},
{
"token" : "大脑",
"start_offset" : 2,
"end_offset" : 4,
"type" : "CN_WORD",
"position" : 1
}
]
}
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
结果发现ik分词器会把数据大脑分为 数据 和 大脑这两个单词。
因为这个词语是我们自己造出来的,并不是通用的词语,所以ik分词器识别不出来也属于正常。
想要让IK分词器识别出来,就需要自定义词库了,也就是把我们自己造的词语添加到词库里面,这样在分词的时候就可以识别到了。
下面演示一下如何在IK中自定义词库:
1:首先在ik插件对应的配置文件目录下创建一个自定义词库文件my.dic
首先在bigdata01节点上操作。
切换到es用户,进入到ik插件对应的配置文件目录

[root@bigdata01 ~]# su es
[es@bigdata01 root]$ cd /data/soft/elasticsearch-7.13.4
[es@bigdata01 elasticsearch-7.13.4]$ cd config
[es@bigdata01 config]$ cd analysis-ik
[es@bigdata01 analysis-ik]$ ll
total 8260
-rwxrwxrwx. 1 root root 5225922 Feb 27 20:57 extra_main.dic
-rwxrwxrwx. 1 root root 63188 Feb 27 20:57 extra_single_word.dic
-rwxrwxrwx. 1 root root 63188 Feb 27 20:57 extra_single_word_full.dic
-rwxrwxrwx. 1 root root 10855 Feb 27 20:57 extra_single_word_low_freq.dic
-rwxrwxrwx. 1 root root 156 Feb 27 20:57 extra_stopword.dic
-rwxrwxrwx. 1 root root 625 Feb 27 20:57 IKAnalyzer.cfg.xml
-rwxrwxrwx. 1 root root 3058510 Feb 27 20:57 main.dic
-rwxrwxrwx. 1 root root 123 Feb 27 20:57 preposition.dic
-rwxrwxrwx. 1 root root 1824 Feb 27 20:57 quantifier.dic
-rwxrwxrwx. 1 root root 171 Feb 27 21:42 stopword.dic
-rwxrwxrwx. 1 root root 192 Feb 27 20:57 suffix.dic
-rwxrwxrwx. 1 root root 752 Feb 27 20:57 surname.dic
1
2
3
4
5
创建自定义词库文件my.dic
直接在文件中添加词语即可,每一个词语一行。

[es@bigdata01 analysis-ik]$ vi my.dic
数据大脑
1
2
3
4
5
6
注意:这个my.dic词库文件可以在Linux中直接使用vi命令创建,或者在Windows中创建之后上传到这里。

-如果是在Linux中直接使用vi命令创建,可以直接使用。
-如果是在Windows中创建的,需要注意文件的编码必须是UTF-8 without BOM 格式【UTF-8 无 BOM格式】

以Notepad++为例:新版本的Notepad++里面的文件编码有这么几种,需要选择【使用UTF-8编码】,这个就是UTF-8 without BOM 格式。

image-20230611153519003

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
2:修改ik的IKAnalyzer.cfg.xml配置文件
进入到ik插件对应的配置文件目录中,修改IKAnalyzer.cfg.xml配置文件

[es@bigdata01 analysis-ik]$ vi IKAnalyzer.cfg.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">
<properties>
<comment>IK Analyzer 扩展配置</comment>
<!--用户可以在这里配置自己的扩展字典 -->
<entry key="ext_dict">my.dic</entry>
<!--用户可以在这里配置自己的扩展停止词字典-->
<entry key="ext_stopwords"></entry>
<!--用户可以在这里配置远程扩展字典 -->
<!-- <entry key="remote_ext_dict">words_location</entry> -->
<!--用户可以在这里配置远程扩展停止词字典-->
<!-- <entry key="remote_ext_stopwords">words_location</entry>-->
</properties>
1
2
3
4
注意:需要把my.dic词库文件添加到key="ext_dict"这个entry中,切记不要随意新增entry,随意新增的entry是不被IK识别的,并且entry的名称也不能乱改,否则也不会识别。

如果需要指定多个自定义词库文件的话需要使用分号;隔开。
例如:<entry key="ext_dict">my.dic;your.dic</entry>
1
2
3
3:将修改好的IK配置文件复制到集群中的所有节点中

注意:如果是多个节点的ES集群,一定要把配置远程拷贝到其他节点。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
先从bigdata01上将my.dic拷贝到bigdata02和bigdata03

[es@bigdata01 analysis-ik]$ scp -rq my.dic bigdata02:/data/soft/elasticsearch-7.13.4/config/analysis-ik/
The authenticity of host 'bigdata02 (192.168.182.101)' can't be established.
ECDSA key fingerprint is SHA256:SnzVynyweeRcPIorakoDQRxFhugZp6PNIPV3agX/bZM.
ECDSA key fingerprint is MD5:f6:1a:48:78:64:77:89:52:c4:ad:63:82:a5:d5:57:92.
Are you sure you want to continue connecting (yes/no)? yes
es@bigdata02's password:
[es@bigdata01 analysis-ik]$ scp -rq my.dic bigdata03:/data/soft/elasticsearch-7.13.4/config/analysis-ik/
The authenticity of host 'bigdata03 (192.168.182.102)' can't be established.
ECDSA key fingerprint is SHA256:SnzVynyweeRcPIorakoDQRxFhugZp6PNIPV3agX/bZM.
ECDSA key fingerprint is MD5:f6:1a:48:78:64:77:89:52:c4:ad:63:82:a5:d5:57:92.
Are you sure you want to continue connecting (yes/no)? yes
es@bigdata03's password:
1
注意:因为现在使用的是普通用户es,所以在使用scp的时候需要指定目标机器的用户名(如果是root可以省略不写),并且还需要手工输入密码,因为之前是基于root用户做的免密码登录。
1
再从bigdata01上将IKAnalyzer.cfg.xml拷贝到bigdata02和bigdata03
1
注意:如果后期想增加自定义停用词库,也需要按照这个思路进行添加,只不过停用词库需要配置到 key="ext_stopwords"这个entry中。
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
4:重启ES验证一下自定义词库的分词效果

[es@bigdata01 elasticsearch-7.13.4]$ curl -H "Content-Type: application/json" -XPOST 'http://bigdata01:9200/test/_analyze?pretty' -d '{"text":"数据大脑","tokenizer":"ik_max_word"}'
{
"tokens" : [
{
"token" : "数据大脑",
"start_offset" : 0,
"end_offset" : 4,
"type" : "CN_WORD",
"position" : 0
},
{
"token" : "数据",
"start_offset" : 0,
"end_offset" : 2,
"type" : "CN_WORD",
"position" : 1
},
{
"token" : "大脑",
"start_offset" : 2,
"end_offset" : 4,
"type" : "CN_WORD",
"position" : 2
}
]
}
1
现在发现数据大脑这个词语可以被识别出来了,说明自定义词库生效了。

热更新词库

1
2
3
4
5
6
7
8
针对前面分析的自定义词库,后期只要词库内容发生了变动,就需要重启ES才能生效,在实际工作中,频繁重启ES集群不是一个好办法
所以ES提供了热更新词库的解决方案,在不重启ES集群的情况下识别新增的词语,这样就很方便了,也不会对线上业务产生影响。
下面来演示一下热更新词库的使用:
1:在bigdata04上部署HTTP服务
在这使用tomcat作为Web容器,先下载一个tomcat 8.x版本。
tomcat 8.0.52版本下载地址:
https://archive.apache.org/dist/tomcat/tomcat-8/v8.0.52/bin/apache-tomcat-8.0.52.tar.gz
上传到bigdata04上的/data/soft目录里面,并且解压
1
2
3
[root@bigdata04 soft]# ll apache-tomcat-8.0.52.tar.gz 
-rw-r--r--. 1 root root 9435483 Sep 22 2021 apache-tomcat-8.0.52.tar.gz
[root@bigdata04 soft]# tar -zxvf apache-tomcat-8.0.52.tar.gz
1
2
3
4
5
tomcat的ROOT项目中创建一个自定义词库文件hot.dic,在文件中输入一行内容:测试
[root@bigdata04 soft]# cd apache-tomcat-8.0.52
[root@bigdata04 apache-tomcat-8.0.52]# cd webapps/ROOT/
[root@bigdata04 ROOT]# vi hot.dic
测试
1
2
3
4
5
6
7
8
9
10
启动Tomcat

[root@bigdata04 ROOT]# cd /data/soft/apache-tomcat-8.0.52
[root@bigdata04 apache-tomcat-8.0.52]# bin/startup.sh
Using CATALINA_BASE: /data/soft/apache-tomcat-8.0.52
Using CATALINA_HOME: /data/soft/apache-tomcat-8.0.52
Using CATALINA_TMPDIR: /data/soft/apache-tomcat-8.0.52/temp
Using JRE_HOME: /data/soft/jdk1.8
Using CLASSPATH: /data/soft/apache-tomcat-8.0.52/bin/bootstrap.jar:/data/soft/apache-tomcat-8.0.52/bin/tomcat-juli.jar
Tomcat started.
1
验证一下hot.dic文件是否可以通过浏览器访问:
image-20230611155315334
1
2
3
4
5
6
7
8
注意:页面会显示乱码,这是正常的,不用处理即可。

2:修改ES集群中ik插件的IKAnalyzer.cfg.xml配置文件
在bigdata01上修改。
在key="remote_ext_dict"这个entry中添加hot.dic的远程访问链接
http://bigdata04:8080/hot.dic

注意:一定要记得去掉key="remote_ext_dict"这个entry外面的注释,否则添加的内容是不生效的。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
[es@bigdata01 analysis-ik]$ vi IKAnalyzer.cfg.xml 
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">
<properties>
<comment>IK Analyzer 扩展配置</comment>
<!--用户可以在这里配置自己的扩展字典 -->
<entry key="ext_dict">my.dic</entry>
<!--用户可以在这里配置自己的扩展停止词字典-->
<entry key="ext_stopwords"></entry>
<!--用户可以在这里配置远程扩展字典 -->
<entry key="remote_ext_dict">http://bigdata04:8080/hot.dic</entry>
<!--用户可以在这里配置远程扩展停止词字典-->
<!-- <entry key="remote_ext_stopwords">words_location</entry>-->
</properties>
1
2
3
4
3:将修改好的IK配置文件复制到集群中的所有节点中

4:重启ES集群验证效果。
因为修改了配置,所以需要重启集群。
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
验证:
对北京雾霾这个词语进行分词

[es@bigdata01 elasticsearch-7.13.4]$ curl -H "Content-Type: application/json" -XPOST 'http://bigdata01:9200/test/_analyze?pretty' -d '{"text":"北京雾霾","tokenizer":"ik_max_word"}'
{
"tokens" : [
{
"token" : "北京",
"start_offset" : 0,
"end_offset" : 2,
"type" : "CN_WORD",
"position" : 0
},
{
"token" : "雾",
"start_offset" : 2,
"end_offset" : 3,
"type" : "CN_CHAR",
"position" : 1
},
{
"token" : "霾",
"start_offset" : 3,
"end_offset" : 4,
"type" : "CN_CHAR",
"position" : 2
}
]
}
1
2
3
4
5
6
7
8
正常情况下 北京雾霾 会被分被拆分为多个词语,但是在这我希望ES能够把 北京雾霾 认为是一个完整的词语,又不希望重启ES。
这样就可以修改前面配置的hot.dic文件,在里面增加一个词语:北京雾霾
在bigdata04里面操作,此时可以在Linux中直接编辑文件。

[root@bigdata04 apache-tomcat-8.0.52]# cd webapps/ROOT/
[root@bigdata04 ROOT]# vi hot.dic
测试
北京雾霾
1
2
3
4
5
6
7
8
文件保存之后,在bigdata01上查看ES的日志会看到如下日志信息:
[2027-03-09T18:43:12,700][INFO ][o.w.a.d.Dictionary ] [bigdata01] start to reload ik dict.
[2027-03-09T18:43:12,701][INFO ][o.w.a.d.Dictionary ] [bigdata01] try load config from /data/soft/elasticsearch-7.13.4/config/analysis-ik/IKAnalyzer.cfg.xml
[2027-03-09T18:43:12,929][INFO ][o.w.a.d.Dictionary ] [bigdata01] [Dict Loading] /data/soft/elasticsearch-7.13.4/config/analysis-ik/my.dic
[2027-03-09T18:43:12,929][INFO ][o.w.a.d.Dictionary ] [bigdata01] [Dict Loading] http://bigdata04:8080/hot.dic
[2027-03-09T18:43:12,934][INFO ][o.w.a.d.Dictionary ] [bigdata01] 测试
[2027-03-09T18:43:12,935][INFO ][o.w.a.d.Dictionary ] [bigdata01] 北京雾霾
[2027-03-09T18:43:12,935][INFO ][o.w.a.d.Dictionary ] [bigdata01] reload ik dict finished.
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
再对北京雾霾这个词语进行分词

[es@bigdata01 elasticsearch-7.13.4]$ curl -H "Content-Type: application/json" -XPOST 'http://bigdata01:9200/test/_analyze?pretty' -d '{"text":"北京雾霾","tokenizer":"ik_max_word"}'
{
"tokens" : [
{
"token" : "北京雾霾",
"start_offset" : 0,
"end_offset" : 4,
"type" : "CN_WORD",
"position" : 0
},
{
"token" : "北京",
"start_offset" : 0,
"end_offset" : 2,
"type" : "CN_WORD",
"position" : 1
},
{
"token" : "雾",
"start_offset" : 2,
"end_offset" : 3,
"type" : "CN_CHAR",
"position" : 2
},
{
"token" : "霾",
"start_offset" : 3,
"end_offset" : 4,
"type" : "CN_CHAR",
"position" : 3
}
]
}
1
2
3
4
5
此时,发现北京雾霾这个词语就可以完整被切分出来了,到这为止,我们就成功实现了热更新自定义词库的功能。

注意:默认情况下,最多一分钟之内就可以识别到新增的词语。
通过查看es-ik插件的源码可以发现
https://github.com/medcl/elasticsearch-analysis-ik/blob/master/src/main/java/org/wltea/analyzer/dic/Monitor.java

image-20230611155917849

4 Elasticsearch查询详解

ES Search查询

1
2
在ES中查询单条数据可以使用Get,想要查询一批满足条件的数据的话,就需要使用Search了。
下面来看一个案例,查询索引库中的所有数据,代码如下:
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
package com.imooc.es;

import org.apache.http.HttpHost;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestClient;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.SearchHits;

/**
* Search详解
* Created by xuwei
*/
public class EsSearchOp {
public static void main(String[] args) throws Exception{
//获取RestClient连接
RestHighLevelClient client = new RestHighLevelClient(
RestClient.builder(
new HttpHost("bigdata01", 9200, "http"),
new HttpHost("bigdata02", 9200, "http"),
new HttpHost("bigdata03", 9200, "http")));


SearchRequest searchRequest = new SearchRequest();
//指定索引库,支持指定一个或者多个,也支持通配符,例如:user*
searchRequest.indices("user");
//
//过滤条件
//
//执行查询操作
SearchResponse searchResponse = client.search(searchRequest, RequestOptions.DEFAULT);

//获取查询返回的结果
SearchHits hits = searchResponse.getHits();
//获取数据总量
long numHits = hits.getTotalHits().value;
System.out.println("数据总数:"+numHits);
//获取具体内容
SearchHit[] searchHits = hits.getHits();
//迭代解析具体内容
for (SearchHit hit : searchHits) {
String sourceAsString = hit.getSourceAsString();
System.out.println(sourceAsString);
}

//关闭连接
client.close();

}
}
1
2
3
4
5
6
7
8
9
10
11
12
在执行代码之前先初始化数据:
[root@bigdata01 ~]$ curl -H "Content-Type: application/json" -XPOST 'http://bigdata01:9200/user/_doc/1' -d '{"name":"tom","age":20}'
[root@bigdata01 ~]$ curl -H "Content-Type: application/json" -XPOST 'http://bigdata01:9200/user/_doc/2' -d '{"name":"tom","age":15}'
[root@bigdata01 ~]$ curl -H "Content-Type: application/json" -XPOST 'http://bigdata01:9200/user/_doc/3' -d '{"name":"jack","age":17}'
[root@bigdata01 ~]$ curl -H "Content-Type: application/json" -XPOST 'http://bigdata01:9200/user/_doc/4' -d '{"name":"jess","age":19}'
[root@bigdata01 ~]$ curl -H "Content-Type: application/json" -XPOST 'http://bigdata01:9200/user/_doc/5' -d '{"name":"mick","age":23}'
[root@bigdata01 ~]$ curl -H "Content-Type: application/json" -XPOST 'http://bigdata01:9200/user/_doc/6' -d '{"name":"lili","age":12}'
[root@bigdata01 ~]$ curl -H "Content-Type: application/json" -XPOST 'http://bigdata01:9200/user/_doc/7' -d '{"name":"john","age":28}'
[root@bigdata01 ~]$ curl -H "Content-Type: application/json" -XPOST 'http://bigdata01:9200/user/_doc/8' -d '{"name":"jojo","age":30}'
[root@bigdata01 ~]$ curl -H "Content-Type: application/json" -XPOST 'http://bigdata01:9200/user/_doc/9' -d '{"name":"bubu","age":16}'
[root@bigdata01 ~]$ curl -H "Content-Type: application/json" -XPOST 'http://bigdata01:9200/user/_doc/10' -d '{"name":"pig","age":21}'
[root@bigdata01 ~]$ curl -H "Content-Type: application/json" -XPOST 'http://bigdata01:9200/user/_doc/11' -d '{"name":"mary","age":19}'
1
2
3
4
5
6
7
8
9
10
11
12
13
14
在IDEA中执行代码,可以看到下面结果:
数据总数:11
{"name":"tom","age":20}
{"name":"tom","age":15}
{"name":"jack","age":17}
{"name":"jess","age":19}
{"name":"mick","age":23}
{"name":"lili","age":12}
{"name":"john","age":28}
{"name":"jojo","age":30}
{"name":"bubu","age":16}
{"name":"pig","age":21}

显示数据总数有11条,但是下面的明细内容只有10条,这是因为ES默认只会返回10条数据,如果默认返回所有满足条件的数据,对ES的压力就比较大了。

searchType详解

1
2
3
4
5
6
ES在查询数据的时候可以指定searchType,也就是搜索类型

//指定searchType
searchRequest.searchType(SearchType.QUERY_THEN_FETCH);

searchType之前是可以指定为下面这4种:

image-20230611160600236

1
其中QUERY AND FETCH和DFS QUERY AND FETCH这两种searchType现在已经不支持了。
1
2
3
4
5
6
7
这4种搜索类型到底有什么区别,下面我们来详细分析一下:

在具体分析这4种搜索类型的区别之前,我们先分析一下分布式搜索的背景:
ES天生就是为分布式而生的,但分布式有分布式的缺点,比如要搜索某个单词,但是数据却分别在5个分片(Shard)上面,这5个分片可能在5台主机上面。因为全文搜索天生就要排序(按照匹配度进行排名),但数据却在5个分片上,如何得到最后正确的排序呢?ES是这样做的,大概分两步。
第1步:ES客户端将会同时向5个分片发起搜索请求。
第2步:这5个分片基于本分片的内容独立完成搜索,然后将符合条件的结果全部返回。
大致流程如下图所示:
image-20230611161344874 image-20230611161408321
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
然而这其中有两个问题。
第一:数量问题。比如,用户需要搜索"衣服",要求返回符合条件的前10条。但在5个分片中,可能都存储着衣服相关的数据。所以ES会向这5个分片都发出查询请求,并且要求每个分片都返回符合条件的10条记录。这种情况,ES中5个分片最多会收到10*5=50条记录,这样返回给用户的结果数量会多于用户请求的数量。
第二:排名问题。上面说的搜索,每个分片计算符合条件的前10条数据都是基于自己分片的数据进行打分计算的。计算分值使用的词频和文档频率等信息都是基于自己分片的数据进行的,而ES进行整体排名是基于每个分片计算后的分值进行排序的(相当于打分依据就不一样,最终对这些数据统一排名的时候就不准确了),这就可能会导致排名不准确的问题。如果我们想更精确的控制排序,应该先将计算排序和排名相关的信息(词频和文档频率等打分依据)从5个分片收集上来,进行统一计算,然后使用整体的词频和文档频率为每个分片中的数据进行打分,这样打分依据就一样了。

再举个例子解释一下【排名问题】:
假设某学校有一班和二班两个班级。
期末考试之后,学校要给全校前十名学员发奖金。
但是一班和二班考试的时候使用的不是一套试卷。
一班:使用的是A卷【A卷偏容易】
二班:使用的是B卷【B卷偏难】
结果就是一班的最高分是100分,最低分是80分。
二班的最高分是70分,最低分是30分。

这样全校前十名就都是一班的学员了。这显然是不合理的。
因为一班和二班的试卷难易程度不一样,也就是打分依据不一样,所以不能放在一块排名,这个就解释了刚才的排名问题。
如果想要保证排名准确的话,需要保证一班和二班使用的试卷内容一样。
可以这样做,把A卷和B卷的内容组合到一块,作为C卷。
一班和二班考试都使用C卷,这样他们的打分依据就一样了,最终再根据所有学员的成绩排名求前十名就准确合理了。
1
这两个问题,ES也没有什么较好的解决方法,最终把选择的权利交给用户,方法就是在搜索的时候指定searchType。
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
1.QUERY AND FETCH(淘汰)
向索引的所有分片都发出查询请求,各分片返回的时候把元素文档(document)和计算后的排名信息一起返回。
这种搜索方式是最快的。因为相比下面的几种搜索方式,这种查询方法只需要去分片查询一次。但是各个分片返回的结果的数量之和可能是用户要求的数据量的N倍。
优点:
只需要查询一次
缺点:
返回的数据量不准确,可能返回(N*分片数量)的数据
并且数据排名也不准确

2.QUERY THEN FETCH(ES默认的搜索方式)
如果你搜索时,没有指定搜索方式,就是使用的这种搜索方式。这种搜索方式,大概分两个步骤,
第一步,先向所有的分片发出请求,各分片只返回文档id(注意,不包括文档document)和排名相关的信息(也就是文档对应的分值),然后按照各分片返回的文档的分数进行重新排序和排名,取前size个文档。
第二步,根据文档id去相关的分片取文档。这种方式返回的文档数量与用户要求的数量是相等的。
优点:
返回的数据量是准确的
缺点:
性能一般,
并且数据排名不准确

3.DFS QUERY AND FETCH(淘汰)
这种方式比第一种方式多了一个DFS步骤,有这一步,可以更精确控制搜索打分和排名。
也就是在进行查询之前,先对所有分片发送请求,把所有分片中的词频和文档频率等打分依据全部汇总到一块,再执行后面的操作、
优点:
数据排名准确
缺点:
性能一般
返回的数据量不准确,可能返回(N*分片数量)的数据

4.DFS QUERY THEN FETCH
比第2种方式多了一个DFS步骤。
也就是在进行查询之前,先对所有分片发送请求,把所有分片中的词频和文档频率等打分依据全部汇总到一块,再执行后面的操作、
优点:
返回的数据量是准确的
数据排名准确
缺点:
性能最差【这个最差只是表示在这四种查询方式中性能最慢,也不至于不能忍受,如果对查询性能要求不是非常高,而对查询准确度要求比较高的时候可以考虑这个】

DFS是一个什么样的过程?

1
2
3
4
5
DFS其实就是在进行真正的查询之前,先把各个分片的词频率和文档频率收集一下,然后进行词搜索的时候,各分片依据全局的词频率和文档频率进行搜索和排名。显然如果使用DFS_QUERY_THEN_FETCH这种查询方式,效率是最低的,因为一个搜索,可能要请求3次分片。但使用DFS方法,搜索精度是最高的。

总结一下,从性能考虑QUERY_AND_FETCH是最快的,DFS_QUERY_THEN_FETCH是最慢的。从搜索的准确度来说,DFS要比非DFS的准确度更高。

目前官方舍弃了QUERY AND FETCH和DFS QUERY AND FETCH这两种类型,保留了QUERY THEN FETCH和DFS QUERY THEN FETCH,这两种都是可以保证数据量是准确的。如果对查询的精确度要求没那么高,就使用QUERY THEN FETCH,如果对查询数据的精确度要求非常高,就使用DFS QUERY THEN FETCH。

ES查询扩展

1
在查询数据的时候可以在searchRequest中指定一些参数,实现过滤、分页、排序、高亮等功能

EsSearchOp

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
package com.imooc.es;

import org.apache.http.HttpHost;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestClient;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.common.text.Text;
import org.elasticsearch.index.query.Operator;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.SearchHits;
import org.elasticsearch.search.builder.SearchSourceBuilder;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightBuilder;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightField;
import org.elasticsearch.search.sort.SortOrder;

import java.util.Map;

/**
* Search详解
* Created by xuwei
*/
public class EsSearchOp {
public static void main(String[] args) throws Exception{
//获取RestClient连接
RestHighLevelClient client = new RestHighLevelClient(
RestClient.builder(
new HttpHost("bigdata01",9200,"http"),
new HttpHost("bigdata02",9200,"http"),
new HttpHost("bigdata03",9200,"http")));
SearchRequest searchRequest = new SearchRequest();
//指定索引库,支持指定一个或者多个,也支持通配符,例如:user*
searchRequest.indices("user");

//指定查询条件
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
//查询所有,可以不指定,默认就是查询索引库中的所有数据
//searchSourceBuilder.query(QueryBuilders.matchAllQuery());
//对指定字段的值进行过滤,注意:在查询数据的时候会对数据进行分词
//如果指定多个query,后面的query会覆盖前面的query
//针对字符串类型内容的查询,不支持通配符
//searchSourceBuilder.query(QueryBuilders.matchQuery("name","tom"));
//searchSourceBuilder.query(QueryBuilders.matchQuery("age","17"));//针对age的值,这里可以指定字符串或者数字都可以
//针对字符串类型内容的查询,支持通配符,但是性能较差,可以认为是全表扫描
//searchSourceBuilder.query(QueryBuilders.wildcardQuery("name","t*"));
//区间查询,主要针对数据类型,可以使用from+to 或者gt,gte+lt,lte
//searchSourceBuilder.query(QueryBuilders.rangeQuery("age").from(0).to(20));
//searchSourceBuilder.query(QueryBuilders.rangeQuery("age").gte(0).lte(20));
//不限制边界,指定为null即可
//searchSourceBuilder.query(QueryBuilders.rangeQuery("age").from(0).to(null));
//同时指定多个条件,条件之间的关系支持and(must)、or(should)
//searchSourceBuilder.query(QueryBuilders.boolQuery().should(QueryBuilders.matchQuery("name","tom")).should(QueryBuilders.matchQuery("age",19)));
//多条件组合查询的时候,可以设置条件的权重值,将满足高权重值条件的数据排到结果列表的前面
//searchSourceBuilder.query(QueryBuilders.boolQuery().should(QueryBuilders.matchQuery("name","tom").boost(1.0f)).should(QueryBuilders.matchQuery("age",19).boost(5.0f)));
//对多个指定字段的值进行过滤,注意:多个字段的数据类型必须一致,否则会报错,如果查询的字段不存在不会报错
//searchSourceBuilder.query(QueryBuilders.multiMatchQuery("tom","name","tag"));
//这里通过queryStringQuery可以支持Lucene的原生查询语法,更加灵活,注意:AND、OR、TO之类的关键字必须大写
//searchSourceBuilder.query(QueryBuilders.queryStringQuery("name:tom AND age:[15 TO 30]"));
//searchSourceBuilder.query(QueryBuilders.boolQuery().must(QueryBuilders.matchQuery("name","tom")).must(QueryBuilders.rangeQuery("age").from(15).to(30)));
//queryStringQuery支持通配符,但是性能也是比较差
//searchSourceBuilder.query(QueryBuilders.queryStringQuery("name:t*"));
//精确查询,查询的时候不分词,针对人名、手机号、主机名、邮箱号码等字段的查询时一般不需要分词
//初始化一条测试数据name=刘德华,默认情况下在建立索引的时候刘德华 会被切分为刘、德、华这三个词
//所以这里精确查询是查不出来的,使用matchQuery是可以查出来的
//searchSourceBuilder.query(QueryBuilders.matchQuery("name","刘德华"));
//searchSourceBuilder.query(QueryBuilders.termQuery("name","刘德华"));
//正常情况下想要使用termQuery实现精确查询的字段不能进行分词
//但是有时候会遇到某个字段已经分词建立索引了,后期还想要实现精确查询
//重新建立索引也不现实,怎么办呢?
//searchSourceBuilder.query(QueryBuilders.queryStringQuery("name:\"刘德华\""));
//matchQuery默认会根据分词的结果进行 or 操作,满足任意一个词语的数据都会查询出来
//searchSourceBuilder.query(QueryBuilders.matchQuery("name","刘德华"));
//如果想要对matchQuery的分词结果实现and操作,可以通过operator进行设置
//这种方式也可以解决某个字段已经分词建立索引了,后期还想要实现精确查询的问题(间接实现,其实是查询了满足刘、德、华这三个词语的内容)
//searchSourceBuilder.query(QueryBuilders.matchQuery("name","刘德华").operator(Operator.AND));

//高亮查询name字段
//searchSourceBuilder.query(QueryBuilders.matchQuery("name","tom"));
//searchSourceBuilder.query(QueryBuilders.matchQuery("name","刘德华"));

//分页
//设置每页的起始位置,默认是0
//searchSourceBuilder.from(0);
//设置每页的数据量,默认是10
//searchSourceBuilder.size(10);

//排序
//searchSourceBuilder.sort("age", SortOrder.DESC);
//注意:age字段是数字类型,不需要分词,name字段是字符串类型(Text),默认会被分词,所以不支持排序和聚合操作
//如果想要根据这些会被分词的字段进行排序或者聚合,需要指定使用他们的keyword类型,这个类型表示不会对数据分词
//searchSourceBuilder.sort("name.keyword",SortOrder.DESC);
//keyword类型的特性其实也适用于精确查询的场景,可以在matchQuery中指定字段的keyword类型实现精确查询,不管在建立索引的时候有没有被分词都不影响使用
//searchSourceBuilder.query(QueryBuilders.matchQuery("name.keyword","刘德华"));

//高亮
//设置高亮字段
HighlightBuilder highlightBuilder = new HighlightBuilder()
.field("name");//支持多个高亮字段,使用多个field方法指定即可
//设置高亮字段的前缀和后缀内容
highlightBuilder.preTags("<font color='red'>");
highlightBuilder.postTags("</font>");
searchSourceBuilder.highlighter(highlightBuilder);


searchRequest.source(searchSourceBuilder);

//执行查询操作
SearchResponse searchResponse = client.search(searchRequest, RequestOptions.DEFAULT);

//获取查询返回的结果
SearchHits hits = searchResponse.getHits();
//获取数据总量
long numHits = hits.getTotalHits().value;
System.out.println("数据总数:"+numHits);
//获取具体内容
SearchHit[] searchHits = hits.getHits();
//迭代解析具体内容
for (SearchHit hit : searchHits) {
/*String sourceAsString = hit.getSourceAsString();
System.out.println(sourceAsString);*/
Map<String, Object> sourceAsMap = hit.getSourceAsMap();
String name = sourceAsMap.get("name").toString();
int age = Integer.parseInt(sourceAsMap.get("age").toString());
//获取高亮字段内容
Map<String, HighlightField> highlightFields = hit.getHighlightFields();
//获取name字段的高亮内容
HighlightField highlightField = highlightFields.get("name");
if(highlightField!=null){
Text[] fragments = highlightField.getFragments();
name = "";
for (Text text: fragments) {
name += text;
}
}

//获取最终的结果数据
System.out.println(name+"---"+age);
}
//关闭连接
client.close();
}
}

过滤

1
2
首先看一下如何在查询的时候指定过滤条件
核心代码如下:
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
//指定查询条件
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
//查询所有,可以不指定,默认就是查询索引库中的所有数据
//searchSourceBuilder.query(QueryBuilders.matchAllQuery());

//对指定字段的值进行过滤,注意:在查询数据的时候会对数据进行分词
//如果指定多个query,后面的query会覆盖前面的query
//针对字符串(数字)类型内容的查询,不支持通配符
//searchSourceBuilder.query(QueryBuilders.matchQuery("name","tom"));
//searchSourceBuilder.query(QueryBuilders.matchQuery("age","17"));//针对age的值,这里可以指定字符串或者数字都可以

//针对字符串类型内容的查询,支持通配符,但是性能较差,可以认为是全表扫描
//searchSourceBuilder.query(QueryBuilders.wildcardQuery("name","t*"));

//区间查询,主要针对数据类型,可以使用from+to 或者gt,gte+lt,lte
//searchSourceBuilder.query(QueryBuilders.rangeQuery("age").from(0).to(20));
//searchSourceBuilder.query(QueryBuilders.rangeQuery("age").gte(0).lte(20));

//不限制边界,指定为null即可
//searchSourceBuilder.query(QueryBuilders.rangeQuery("age").from(0).to(null));

//同时指定多个条件,条件之间的关系支持and(must)、or(should)
//searchSourceBuilder.query(QueryBuilders.boolQuery().should(QueryBuilders.matchQuery("name","tom")).should(QueryBuilders.matchQuery("age",19)));

//多条件组合查询的时候,可以设置条件的权重值,将满足高权重值条件的数据排到结果列表的前面
//searchSourceBuilder.query(QueryBuilders.boolQuery().should(QueryBuilders.matchQuery("name","tom").boost(1.0f)).should(QueryBuilders.matchQuery("age",19).boost(5.0f)));

//对多个指定字段的值进行过滤,注意:多个字段的数据类型必须一致,否则会报错,如果查询的字段不存在不会报错
//searchSourceBuilder.query(QueryBuilders.multiMatchQuery("tom","name","tag")); // tag字段不存在

//这里通过queryStringQuery可以支持Lucene的原生查询语法,更加灵活,注意:AND、OR、TO之类的关键字必须大写
//searchSourceBuilder.query(QueryBuilders.queryStringQuery("name:tom AND age:[15 TO 30]"));
//searchSourceBuilder.query(QueryBuilders.boolQuery().must(QueryBuilders.matchQuery("name","tom")).must(QueryBuilders.rangeQuery("age").from(15).to(30)));

//queryStringQuery支持通配符,但是性能也是比较差
//searchSourceBuilder.query(QueryBuilders.queryStringQuery("name:t*"));

//精确查询(查询时不分词),查询的时候不分词,针对人名、手机号、主机名、邮箱号码等字段的查询时一般不需要分词
//初始化一条测试数据name=刘德华,默认情况下在建立索引的时候(这是建立索引,还没字段的分词器,还是默认的)刘德华 会被切分为刘、德、华这三个词
//所以这里精确查询是查不出来的,使用matchQuery是可以查出来的
//searchSourceBuilder.query(QueryBuilders.matchQuery("name","刘德华"));
//searchSourceBuilder.query(QueryBuilders.termQuery("name","刘德华"));

//正常情况下想要使用termQuery实现精确查询的字段不能进行分词
//但是有时候会遇到某个字段已经分词建立索引了,后期还想要实现精确查询
//重新建立索引也不现实,怎么办呢?
//searchSourceBuilder.query(QueryBuilders.queryStringQuery("name:\"刘德华\"")); //这里里面是转义字符

//matchQuery默认会根据分词的结果进行 or 操作,满足任意一个词语的数据都会查询出来
//searchSourceBuilder.query(QueryBuilders.matchQuery("name","刘德华"));

//如果想要对matchQuery的分词结果实现and操作,可以通过operator进行设置
//这种方式也可以解决某个字段已经分词建立索引了,后期还想要实现精确查询的问题(间接实现,其实是查询了满足刘、德、华这三个词语的内容)
//searchSourceBuilder.query(QueryBuilders.matchQuery("name","刘德华").operator(Operator.AND));

searchRequest.source(searchSourceBuilder);
1
带权重

image-20230613234458201

1
使用match,还是模糊查询

image-20230613235809923

1
termQuery,查不到结果,因为他直接拿“刘德华”去查询,索引库里没有

image-20230614000114276

1
利用queryStringQuery实现精准查询

image-20230614000332868

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
默认情况下ES会对刘德华这个词语进行分词,效果如下(使用的默认分词器):
[root@bigdata01 ~]$ curl -H "Content-Type: application/json" -XPOST 'http://bigdata01:9200/emp/_analyze?pretty' -d '{"text":"刘德华"}'
{
"tokens" : [
{
"token" : "刘",
"start_offset" : 0,
"end_offset" : 1,
"type" : "<IDEOGRAPHIC>",
"position" : 0
},
{
"token" : "德",
"start_offset" : 1,
"end_offset" : 2,
"type" : "<IDEOGRAPHIC>",
"position" : 1
},
{
"token" : "华",
"start_offset" : 2,
"end_offset" : 3,
"type" : "<IDEOGRAPHIC>",
"position" : 2
}
]
}
1
2
3
初始化数据:
[root@bigdata01 ~]$ curl -H "Content-Type: application/json" -XPOST 'http://bigdata01:9200/user/_doc/12' -d '{"name":"刘德华","age":60}'
[root@bigdata01 ~]$ curl -H "Content-Type: application/json" -XPOST 'http://bigdata01:9200/user/_doc/13' -d '{"name":"刘老二","age":20}'

分页

1
2
3
4
5
6
7
8
ES每次返回的数据默认最多是10条,可以认为是一页的数据,这个数据量是可以控制的
核心代码如下:

//分页
//设置每页的起始位置,默认是0
//searchSourceBuilder.from(0);
//设置每页的数据量,默认是10
//searchSourceBuilder.size(10);

排序

1
2
3
4
5
6
7
8
9
10
11
12
在返回满足条件的结果之前,可以按照指定的要求对数据进行排序,默认是按照搜索条件的匹配度返回数据的。(默认情况下,查询关键字的匹配度越高越靠前)
核心代码如下:

//排序
//按照age字段,倒序排序
//searchSourceBuilder.sort("age", SortOrder.DESC);
//注意:age字段是数字类型,不需要分词,name字段是字符串类型(Text),默认会被分词,所以不支持排序和聚合操作
//如果想要根据这些会被分词的字段进行排序或者聚合,需要指定使用他们的keyword类型,这个类型表示不会对数据分词(ES默认会对字符串类型数据,建立两份索引,一份分词,一份不分词)
//searchSourceBuilder.sort("name.keyword", SortOrder.DESC);

//keyword类型的特性其实也适用于精确查询的场景,可以在matchQuery中指定字段的keyword类型实现精确查询,不管在建立索引的时候有没有被分词都不影响使用
//searchSourceBuilder.query(QueryBuilders.matchQuery("name.keyword", "刘德华"));
1
match实现精确查询

image-20230614002640823

高亮

1
2
3
4
5
6
7
8
9
10
11
针对用户搜索时的关键词,如果匹配到了,最终在页面展现的时候可以标红高亮显示,看起来比较清晰。
设置高亮的核心代码如下:

//高亮
//设置高亮字段
HighlightBuilder highlightBuilder = new HighlightBuilder()
.field("name");//支持多个高亮字段,使用多个field方法指定即可
//设置高亮字段的前缀和后缀内容
highlightBuilder.preTags("<font color='red'>");
highlightBuilder.postTags("</font>");
searchSourceBuilder.highlighter(highlightBuilder);
1
别忘了前面完整代码中,要对高亮字段查询,才会有效果

image-20230614004401423

image-20230614004343078

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
解析高亮内容的核心代码如下:

//迭代解析具体内容
for (SearchHit hit : searchHits) {
/*String sourceAsString = hit.getSourceAsString();
System.out.println(sourceAsString);*/
Map<String, Object> sourceAsMap = hit.getSourceAsMap();
String name = sourceAsMap.get("name").toString();
int age = Integer.parseInt(sourceAsMap.get("age").toString());
//获取高亮字段内容
Map<String, HighlightField> highlightFields = hit.getHighlightFields();
//获取name字段的高亮内容
HighlightField highlightField = highlightFields.get("name");
if(highlightField!=null){
Text[] fragments = highlightField.getFragments();
name = "";
for (Text text : fragments) {
name += text;
}
}
//获取最终的结果数据
System.out.println(name+"---"+age);
}
1
2
3
4
5
注意:必须要设置查询的字段,否则无法实现高亮。

//高亮查询name字段
searchSourceBuilder.query(QueryBuilders.matchQuery("name","tom"));
searchSourceBuilder.query(QueryBuilders.matchQuery("name","刘德华"));

评分依据(了解)

1
2
3
4
5
6
ES在返回满足条件的数据的时候,按照搜索条件的匹配度返回数据的,匹配度最高的数据排在最前面,这个匹配度其实就是ES中返回结果中的score字段的值。

//获取数据的匹配度分值,值越大说明和搜索的关键字匹配度越高
float score = hit.getScore();
//获取最终的结果数据
System.out.println(name+"---"+age+"---"+score);
1
2
3
4
5
6
7
此时,我们搜索name=刘华 的数据
searchSourceBuilder.query(QueryBuilders.matchQuery("name", "刘华"));

结果如下:
数据总数:2
<font color='red'>刘</font>德<font color='red'>华</font>---60---2.591636
<font color='red'>刘</font>老二---20---1.0036464
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
可以看到第一条数据的score分值为2.59
第二条数据的score分值为1.00

score分值具体是如何计算出来的呢?可以通过开启评分依据进行查看详细信息:
首先开启评分依据:
//评分依据,true:开启,false:关闭
searchSourceBuilder.explain(true);


获取评分依据信息:
//获取Score的评分依据
Explanation explanation = hit.getExplanation();
//打印评分依据
if(explanation!=null){
System.out.println(explanation.toString());
}
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
再执行程序,就可以看到具体的评分依据信息了:
数据总数:2
<font color='red'>刘</font>德<font color='red'>华</font>---60---2.591636
2.591636 = sum of:
1.0036464 = weight(name:刘 in 1) [PerFieldSimilarity], result of:
1.0036464 = score(freq=1.0), computed as boost * idf * tf from:
2.2 = boost
1.4552872 = idf, computed as log(1 + (N - n + 0.5) / (n + 0.5)) from:
3.0 = n, number of documents containing term
14.0 = N, total number of documents with field
0.3134796 = tf, computed as freq / (freq + k1 * (1 - b + b * dl / avgdl)) from:
1.0 = freq, occurrences of term within document
1.2 = k1, term saturation parameter
0.75 = b, length normalization parameter
3.0 = dl, length of field
1.4285715 = avgdl, average length of field
1.5879896 = weight(name:华 in 1) [PerFieldSimilarity], result of:
1.5879896 = score(freq=1.0), computed as boost * idf * tf from:
2.2 = boost
2.3025851 = idf, computed as log(1 + (N - n + 0.5) / (n + 0.5)) from:
1.0 = n, number of documents containing term
14.0 = N, total number of documents with field
0.3134796 = tf, computed as freq / (freq + k1 * (1 - b + b * dl / avgdl)) from:
1.0 = freq, occurrences of term within document
1.2 = k1, term saturation parameter
0.75 = b, length normalization parameter
3.0 = dl, length of field
1.4285715 = avgdl, average length of field

<font color='red'>刘</font>老二---20---1.0036464
1.0036464 = sum of:
1.0036464 = weight(name:刘 in 2) [PerFieldSimilarity], result of:
1.0036464 = score(freq=1.0), computed as boost * idf * tf from:
2.2 = boost
1.4552872 = idf, computed as log(1 + (N - n + 0.5) / (n + 0.5)) from:
3.0 = n, number of documents containing term
14.0 = N, total number of documents with field
0.3134796 = tf, computed as freq / (freq + k1 * (1 - b + b * dl / avgdl)) from:
1.0 = freq, occurrences of term within document
1.2 = k1, term saturation parameter
0.75 = b, length normalization parameter
3.0 = dl, length of field
1.4285715 = avgdl, average length of field

ES中分页的性能问题

1
2
3
4
5
6
7
8
在使用ES实现分页查询的时候,不要一次请求过多或者页码过大的结果,这样会对服务器造成很大的压力,因为它们会在返回前排序。
ES是分布式搜索,所以ES客户端的一个查询请求会发送到索引对应的多个分片中,每个分片都会生成自己的排序结果,最后再进行集中排序,以确保最终结果的正确性。

我们假设在搜索一个拥有5个主分片的索引,当我们请求第一页数据的时候,每个分片产生自己前10名,然后将它们返回给请求节点,然后这个请求节点会将收到的50条结果重新排序以产生最终的前10名。

现在想象一下我们如果要获得第1,000页的数据,也就是第10,001到第10,010条数据,每一个分片都会先产生自己的前10,010名,然后请求节点统一处理这50,050条数据,最后再丢弃掉其中的50,040条!
现在我们就明白了,在分布式系统中,大页码请求所消耗的系统资源是呈指数式增长的。这也是为什么网络搜索引擎一般不会提供超过1,000条搜索结果的原因。
例如:百度上的效果。

image-20230611165023568

1
当然还有一点原因是后面的搜索结果基本上也不是我们想要的数据了,我们在使用搜索引擎的时候,一般只会看第1页和第2页的数据。

aggregations聚合统计

1
2
3
4
ES中可以实现基于字段进行分组聚合的统计
聚合操作支持count()、sum()、avg()、max()、min()等

下面来看两个案例

统计相同年龄的学员个数

1
2
需求:统计相同年龄的学员个数
数据如下所示:

image-20230611165752222

1
2
3
4
5
6
7
8
首先在ES中初始化这份数据:

[root@bigdata01 ~]# curl -H "Content-Type: application/json" -XPOST 'http://bigdata01:9200/stu/_doc/1' -d'{"name":"tom","age":18}'
[root@bigdata01 ~]# curl -H "Content-Type: application/json" -XPOST 'http://bigdata01:9200/stu/_doc/2' -d'{"name":"jack","age":29}'
[root@bigdata01 ~]# curl -H "Content-Type: application/json" -XPOST 'http://bigdata01:9200/stu/_doc/3' -d'{"name":"jessica","age":18}'
[root@bigdata01 ~]# curl -H "Content-Type: application/json" -XPOST 'http://bigdata01:9200/stu/_doc/4' -d'{"name":"dave","age":19}'
[root@bigdata01 ~]# curl -H "Content-Type: application/json" -XPOST 'http://bigdata01:9200/stu/_doc/5' -d'{"name":"lilei","age":18}'
[root@bigdata01 ~]# curl -H "Content-Type: application/json" -XPOST 'http://bigdata01:9200/stu/_doc/6' -d'{"name":"lili","age":29}'
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
package com.imooc.es;

import org.apache.http.HttpHost;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestClient;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.search.aggregations.AggregationBuilders;
import org.elasticsearch.search.aggregations.bucket.terms.Terms;
import org.elasticsearch.search.aggregations.bucket.terms.TermsAggregationBuilder;
import org.elasticsearch.search.builder.SearchSourceBuilder;

import java.util.List;

/**
* 聚合统计:统计相同年龄的学员个数
* Created by xuwei
*/
public class EsAggOp01 {
public static void main(String[] args) throws Exception{
//获取RestClient连接
RestHighLevelClient client = new RestHighLevelClient(
RestClient.builder(
new HttpHost("bigdata01", 9200, "http"),
new HttpHost("bigdata02", 9200, "http"),
new HttpHost("bigdata03", 9200, "http")));
SearchRequest searchRequest = new SearchRequest();
searchRequest.indices("stu");

//指定查询条件
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
//指定分组信息,默认是执行count聚合
TermsAggregationBuilder aggregation = AggregationBuilders.terms("age_term")
.field("age");
searchSourceBuilder.aggregation(aggregation);

searchRequest.source(searchSourceBuilder);

//执行查询操作
SearchResponse searchResponse = client.search(searchRequest, RequestOptions.DEFAULT);

//获取分组信息
Terms terms = searchResponse.getAggregations().get("age_term");
List<? extends Terms.Bucket> buckets = terms.getBuckets();
for (Terms.Bucket bucket: buckets) {
System.out.println(bucket.getKey()+"---"+bucket.getDocCount());
}

//关闭连接
client.close();
}

}

统计每个学员的总成绩

1
2
需求:统计每个学员的总成绩
数据如下所示:

image-20230611165858836

1
2
3
4
5
6
[root@bigdata01 ~]# curl -H "Content-Type: application/json" -XPOST 'http://bigdata01:9200/score/_doc/1' -d'{"name":"tom","subject":"chinese","score":59}'
[root@bigdata01 ~]# curl -H "Content-Type: application/json" -XPOST 'http://bigdata01:9200/score/_doc/2' -d'{"name":"tom","subject":"math","score":89}'
[root@bigdata01 ~]# curl -H "Content-Type: application/json" -XPOST 'http://bigdata01:9200/score/_doc/3' -d'{"name":"jack","subject":"chinese","score":78}'
[root@bigdata01 ~]# curl -H "Content-Type: application/json" -XPOST 'http://bigdata01:9200/score/_doc/4' -d'{"name":"jack","subject":"math","score":85}'
[root@bigdata01 ~]# curl -H "Content-Type: application/json" -XPOST 'http://bigdata01:9200/score/_doc/5' -d'{"name":"jessica","subject":"chinese","score":97}'
[root@bigdata01 ~]# curl -H "Content-Type: application/json" -XPOST 'http://bigdata01:9200/score/_doc/6' -d'{"name":"jessica","subject":"math","score":68}'
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
package com.imooc.es;

import org.apache.http.HttpHost;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestClient;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.search.aggregations.Aggregation;
import org.elasticsearch.search.aggregations.AggregationBuilders;
import org.elasticsearch.search.aggregations.bucket.terms.Terms;
import org.elasticsearch.search.aggregations.bucket.terms.TermsAggregationBuilder;
import org.elasticsearch.search.aggregations.metrics.Sum;
import org.elasticsearch.search.builder.SearchSourceBuilder;

import java.util.List;

/**
* 聚合统计:统计每个学员的总成绩
* Created by xuwei
*/
public class EsAggOp02 {
public static void main(String[] args) throws Exception{
//获取RestClient连接
RestHighLevelClient client = new RestHighLevelClient(
RestClient.builder(
new HttpHost("bigdata01", 9200, "http"),
new HttpHost("bigdata02", 9200, "http"),
new HttpHost("bigdata03", 9200, "http")));
SearchRequest searchRequest = new SearchRequest();
searchRequest.indices("score");

//指定查询条件
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
//指定分组和求sum
TermsAggregationBuilder aggregation = AggregationBuilders.terms("name_term")
.field("name.keyword")//指定分组字段,如果是字符串(Text)类型,则需要指定使用keyword类型
.subAggregation(AggregationBuilders.sum("sum_score").field("score"));//指定求sum,也支持avg、min、max等操作
searchSourceBuilder.aggregation(aggregation);

searchRequest.source(searchSourceBuilder);

//执行查询操作
SearchResponse searchResponse = client.search(searchRequest, RequestOptions.DEFAULT);

//获取分组信息
Terms terms = searchResponse.getAggregations().get("name_term");
List<? extends Terms.Bucket> buckets = terms.getBuckets();
for (Terms.Bucket bucket: buckets) {
//获取sum聚合的结果
Sum sum = bucket.getAggregations().get("sum_score");
System.out.println(bucket.getKey()+"---"+sum.getValue());
}

//关闭连接
client.close();
}

}

aggregations获取所有分组数据

1
2
3
4
5
6
7
8
9
10
11
12
13
默认情况下,ES只会返回10个分组的数据,如果分组之后的结果超过了10组,如何解决?

可以通过在聚合操作中使用size方法进行设置,获取指定个数的数据组或者获取所有的数据组。
在案例1的基础上再初始化一批测试数据:
[root@bigdata01 ~]# curl -H "Content-Type: application/json" -XPOST 'http://bigdata01:9200/stu/_doc/61' -d'{"name":"s1","age":31}'
[root@bigdata01 ~]# curl -H "Content-Type: application/json" -XPOST 'http://bigdata01:9200/stu/_doc/62' -d'{"name":"s2","age":32}'
[root@bigdata01 ~]# curl -H "Content-Type: application/json" -XPOST 'http://bigdata01:9200/stu/_doc/63' -d'{"name":"s3","age":33}'
[root@bigdata01 ~]# curl -H "Content-Type: application/json" -XPOST 'http://bigdata01:9200/stu/_doc/64' -d'{"name":"s4","age":34}'
[root@bigdata01 ~]# curl -H "Content-Type: application/json" -XPOST 'http://bigdata01:9200/stu/_doc/65' -d'{"name":"s5","age":35}'
[root@bigdata01 ~]# curl -H "Content-Type: application/json" -XPOST 'http://bigdata01:9200/stu/_doc/66' -d'{"name":"s6","age":36}'
[root@bigdata01 ~]# curl -H "Content-Type: application/json" -XPOST 'http://bigdata01:9200/stu/_doc/67' -d'{"name":"s7","age":37}'
[root@bigdata01 ~]# curl -H "Content-Type: application/json" -XPOST 'http://bigdata01:9200/stu/_doc/68' -d'{"name":"s8","age":38}'
[root@bigdata01 ~]# curl -H "Content-Type: application/json" -XPOST 'http://bigdata01:9200/stu/_doc/69' -d'{"name":"s9","age":39}'
1
2
3
4
5
6
7
8
9
10
11
支持案例1的代码,查看返回的分组个数:
18---3
29---2
19---1
31---1
32---1
33---1
34---1
35---1
36---1
37---1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
发现结果中返回的分组个数是10个,没有全部都显示出来,这个其实和分页也没关系,尝试增加分页的代码发现也是无效的:
//增加分页参数,注意:分页参数针对分组数据是无效的。
searchSourceBuilder.from(0).size(20);

执行案例1的代码,结果发现还是10条数据。
18---3
29---2
19---1
31---1
32---1
33---1
34---1
35---1
36---1
37---1
1
2
3
4
通过在聚合操作上使用size方法进行设置:
TermsAggregationBuilder aggregation = AggregationBuilders.terms("age_term")
.field("age")
.size(20);//获取指定分组个数的数据
1
2
3
4
5
6
7
8
9
10
11
12
13
执行案例1的代码:
18---3
29---2
19---1
31---1
32---1
33---1
34---1
35---1
36---1
37---1
38---1
39---1
1
2
3
此时可以获取到所有分组的数据,因为结果一共有12个分组,在代码中通过size设置最多可以获取到20个分组的数据。

如果前期不确定到底有多少个分组的数据,还想获取到所有分组的数据,此时可以在size中设置一个Integer的最大值,这样基本上就没什么问题了,但是注意:如果最后的分组个数太多,会给ES造成比较大的压力,所以官方在这做了限制,让用户手工指定获取多少分组的数据。
1
2
3
TermsAggregationBuilder aggregation = AggregationBuilders.terms("age_term")
.field("age")
.size(Integer.MAX_VALUE);//获取指定分组个数的数据
1
注意:在ES7.x版本之前,想要获取所有的分组数据,只需要在size中指定参数为0即可。现在ES7.x版本不支持这个数值了。

5 Elasticsearch的高级特性

ES中的settings

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
ES中的settings可以设置索引库的一些配置信息,主要是针对分片数量和副本数量
其中分片数量只能在一开始创建索引库的时候指定,后期不能修改。
副本数量可以随时修改。

首先查看一下ES中目前已有的索引库的默认settings信息。

[root@bigdata01 ~]# curl -XGET 'http://bigdata01:9200/emp/_settings?pretty'
{
"emp" : {
"settings" : {
"index" : {
"routing" : {
"allocation" : {
"include" : {
"_tier_preference" : "data_content"
}
}
},
"number_of_shards" : "1",
"provided_name" : "emp",
"creation_date" : "1803648122805",
"number_of_replicas" : "1",
"uuid" : "kBpwz6kAQ2eS0uCISVcaew",
"version" : {
"created" : "7130499"
}
}
}
}
}
1
2
3
4
此时分片和副本数量默认都是1。
尝试手工指定分片和副本数量。
针对不存在的索引,在创建的时候可以同时指定分片(5)和副本(1)数量:
[root@bigdata01 ~]# curl -H "Content-Type: application/json" -XPUT 'http://bigdata01:9200/test1/' -d'{"settings":{"number_of_shards":5,"number_of_replicas":1}}'
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
查看这个索引库的settings信息:
[root@bigdata01 ~]# curl -XGET 'http://bigdata01:9200/test1/_settings?pretty'
{
"test1" : {
"settings" : {
"index" : {
"routing" : {
"allocation" : {
"include" : {
"_tier_preference" : "data_content"
}
}
},
"number_of_shards" : "5",
"provided_name" : "test1",
"creation_date" : "1804844538706",
"number_of_replicas" : "1",
"uuid" : "WEUwvKVoRzWfna-KFntdqQ",
"version" : {
"created" : "7130499"
}
}
}
}
}
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
针对已存在的索引,只能通过settings指定副本信息。
将刚才创建的索引的副本数量修改为0。

[root@bigdata01 ~]# curl -H "Content-Type: application/json" -XPUT 'http://bigdata01:9200/test1/_settings' -d'{"index":{"number_of_replicas":0}}'

查看这个索引库目前的settings信息:
[root@bigdata01 ~]# curl -XGET 'http://bigdata01:9200/test1/_settings?pretty'
{
"test1" : {
"settings" : {
"index" : {
"routing" : {
"allocation" : {
"include" : {
"_tier_preference" : "data_content"
}
}
},
"number_of_shards" : "5",
"provided_name" : "test1",
"creation_date" : "1804844538706",
"number_of_replicas" : "0",
"uuid" : "WEUwvKVoRzWfna-KFntdqQ",
"version" : {
"created" : "7130499"
}
}
}
}
}

ES中的mapping

1
2
3
4
5
6
7
mapping表示索引库中数据的字段类型信息,类似于MySQL中的表结构信息。

一般不需要手工指定mapping,因为ES会自动根据数据格式识别它的类型

如果你需要对某些字段添加特殊属性(例如:指定分词器),就必须手工指定字段的mapping。

下面先来看一下ES中的常用数据类型:

ES中的常用数据类型

ES中的常用数据类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
字符串:支持text和keyword类型
text类型支持分词,支持模糊、精确查询,但是不支持聚合和排序操作,text类型不限制存储的内容长度,适合大字段存储。
https://www.elastic.co/guide/en/elasticsearch/reference/7.13/text.html

keyword类型不支持分词,会直接对数据建立索引,支持模糊、精确查询,支持聚合和排序操作。keyword类型最大支持存储内容长度为32766个UTF-8类型的字符,可以通过设置ignore_above参数指定某个字段最大支持的字符长度,超过给定长度后的数据将不被索引,此时就无法通过termQuery精确查询返回结果了。keyword类型适合存储手机号、姓名等不需要分词的数据。
https://www.elastic.co/guide/en/elasticsearch/reference/7.13/keyword.html

数字:最常用的就是long和double了,整数使用long、小数使用double。当然也支持integer、short、byte、float这些数据类型。
https://www.elastic.co/guide/en/elasticsearch/reference/7.13/number.html

日期:最常用的就是date类型了,date类型可以支持到毫秒,如果特殊情况下需要精确到纳秒需要使用date_nanos这个类型。
其中date日期类型可以自定义日期格式,通过format参数指定:
{“type”:“date”,“format”:“yyyy-MM-dd”}
https://www.elastic.co/guide/en/elasticsearch/reference/7.13/date.html

布尔型:支持true或者false。

二进制:该字段存储编码为Base64字符串的二进制值。如果想要存储图片,可以存储图片地址,或者图片本身,存储图片本身的话就需要获取图片的二进制值进行存储了。
https://www.elastic.co/guide/en/elasticsearch/reference/7.13/binary.html

查看mapping

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
ES中还支持一些其他数据类型,感兴趣的话可以到文档里面看一下:
https://www.elastic.co/guide/en/elasticsearch/reference/7.13/mapping-types.html

下面查询一下目前已有的索引库score的mapping信息:
[root@bigdata01 ~]# curl -XGET 'http://bigdata01:9200/score/_mapping?pretty'
{
"score" : {
"mappings" : {
"properties" : {
"name" : {
"type" : "text",
"fields" : {
"keyword" : {
"type" : "keyword",
"ignore_above" : 256
}
}
},
"score" : {
"type" : "long"
},
"subject" : {
"type" : "text",
"fields" : {
"keyword" : {
"type" : "keyword",
"ignore_above" : 256
}
}
}
}
}
}
}
1
2
3
4
5
通过返回的mapping信息可以看到score这个索引库里面有3个字段,name、score和subject。
1:name是text类型,其中还通过fields属性指定了一个keyword类型,表示name字段会按照text类型和keyword类型存储2份。

针对fields属性的解释官网里面有详细介绍,见下图:
大致意思是说,一个字段可以设置多种数据类型,这样ES会按照多种数据类型的特性对这个字段进行存储和建立索引。

image-20230611222209691

1
2
3
4
5
6
7
8
9
"ignore_above" : 256,表示keyword类型最大支持的字符串长度是256。
ES默认会把字符串类型的数据同时指定text类型和keyword类型。
想要实现分词检索的时候需要使用text类型,在代码层面直接指定这个name字段就表示使用text类型。
想要实现精确查询的时候需要使用keyword类型,在代码层面指定name.keyword表示使用name的keyword类型。

其实前面我们在讲排序的时候也用到了这个keyword类型的特性,因为直接指定name字段会使用text类型,text类型不支持排序和聚合,所以使用的是name.keyword。

2:score是long类型,整数默认会被识别为long类型。
3:subject也是text类型,和name是一样的。
1
注意:ES 7.x版本之前字符串默认只会被识别为text类型,不会附加一个keyword类型。

手工创建mapping

1
2
3
4
5
下面我们首先操作一个不存在的索引库的mapping信息:
指定name为text类型(手工指定字符串类型时,就不会再支持keyword类型了),并且使用ik分词器。
age为integer类型。

[root@bigdata01 ~]# curl -H "Content-Type: application/json" -XPUT 'http://bigdata01:9200/test2' -d'{"mappings":{"properties":{"name":{"type":"text","analyzer": "ik_max_word"},"age":{"type":"integer"}}}}'
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
查看这个索引库的mapping信息。

[root@bigdata01 ~]# curl -XGET 'http://bigdata01:9200/test2/_mapping?pretty'
{
"test2" : {
"mappings" : {
"properties" : {
"age" : {
"type" : "integer"
},
"name" : {
"type" : "text",
"analyzer" : "ik_max_word"
}
}
}
}
}

更新mapping

1
2
3
在这个已存在的索引库中增加mapping信息。

注意:只能新增字段,不能修改已有字段的类型,否则会报错。
1
2
[root@bigdata01 ~]# curl -H "Content-Type: application/json" -XPOST 'http://bigdata01:9200/test2/_mapping' -d'{"properties":{"age":{"type":"long"}}}'
{"error":{"root_cause":[{"type":"illegal_argument_exception","reason":"mapper [age] cannot be changed from type [integer] to [long]"}],"type":"illegal_argument_exception","reason":"mapper [age] cannot be changed from type [integer] to [long]"},"status":400}
1
2
3
4
可以假设一下,假设支持修改已有字段的类型,之前name是text类型,如果我修改为long类型,这样就会出现矛盾了,所以ES不支持修改已有字段的类型。
在已存在的索引库中增加一个flag字段,类型为boolean类型

[root@bigdata01 ~]# curl -H "Content-Type: application/json" -XPOST 'http://bigdata01:9200/test2/_mapping' -d'{"properties":{"flag":{"type":"boolean"}}}'
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
查看索引库的最新mapping信息。

[root@bigdata01 ~]# curl -XGET 'http://bigdata01:9200/test2/_mapping?pretty'
{
"test2" : {
"mappings" : {
"properties" : {
"age" : {
"type" : "integer"
},
"flag" : {
"type" : "boolean"
},
"name" : {
"type" : "text",
"analyzer" : "ik_max_word"
}
}
}
}
}

ES的偏好查询(分片查询方式)

1
2
3
4
5
ES中的索引数据最终都是存储在分片里面的,分片有多个,并且分片还分为主分片和副本分片。

ES在查询索引库中的数据时,具体是到哪些分片里面查询数据呢?

在具体分析这个之前,我们先分析一下ES的分布式查询过程:

image-20230611222857840

1
2
3
4
5
6
7
8
这个表示是一个3个节点的ES集群,集群内有一个索引库,索引库里面有P0和P1两个主分片,这两个主分片分别都有2个副本分片,R0和R1。

查询过程主要包含三个步骤:

1.客户端发送一个查询请求到Node 3,Node 3会创建一个空优先队列,主要为了存储结果数据。
2.Node 3将查询请求转发到索引的主分片或副本分片中。每个分片在本地执行查询并将查询到的结果添加到本地的有序优先队列中。
具体这里面Node 3将查询请求转发到索引的哪个分片中,可以是随机的,也可以由我们程序员来控制。默认是randomize across shards:表示随机从分片中取数据。
3.每个分片返回各自优先队列中所有文档的ID和排序值给到Node 3,Node 3合并这些值到自己的优先队列中来产生一个全局排序后的结果列表。
1
2
注意:当客户端的一个搜索请求被发送到某个节点时,这个节点就变成了协调节点。 这个节点的任务是广播查询请求到所有相关分片,并将它们查询到的结果整合成全局排序后的结果集合,这个结果集合会返回给客户端。
这里面的Node3节点其实就是协调节点。
1
2
3
4
5
接下来我们来具体分析一下如何控制查询请求到分片之间的分发规则:
先创建一个具有5个分片,0个副本的索引库
分片太少不好验证效果。

[root@bigdata01 ~]# curl -H "Content-Type: application/json" -XPUT 'http://bigdata01:9200/pre/' -d'{"settings":{"number_of_shards":5,"number_of_replicas":0}}'

image-20230611223918180

1
2
3
4
5
6
7
在索引库中初始化一批测试数据:
[root@bigdata01 ~]# curl -H "Content-Type: application/json" -XPOST 'http://bigdata01:9200/pre/_doc/1' -d'{"name":"tom","age":18}'
[root@bigdata01 ~]# curl -H "Content-Type: application/json" -XPOST 'http://bigdata01:9200/pre/_doc/2' -d'{"name":"jack","age":29}'
[root@bigdata01 ~]# curl -H "Content-Type: application/json" -XPOST 'http://bigdata01:9200/pre/_doc/3' -d'{"name":"jessica","age":18}'
[root@bigdata01 ~]# curl -H "Content-Type: application/json" -XPOST 'http://bigdata01:9200/pre/_doc/4' -d'{"name":"dave","age":19}'
[root@bigdata01 ~]# curl -H "Content-Type: application/json" -XPOST 'http://bigdata01:9200/pre/_doc/5' -d'{"name":"lilei","age":18}'
[root@bigdata01 ~]# curl -H "Content-Type: application/json" -XPOST 'http://bigdata01:9200/pre/_doc/6' -d'{"name":"lili","age":29}'
1
这些测试数据在索引库的分片中的分布情况是这样的:

image-20230611224210088

image-20230611224227978

image-20230611224242367

1
2
3
在代码层面通过preference(...)来设置具体的分片查询方式:
//指定分片查询方式
searchRequest.preference();//默认随机

_local

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
表示查询操作会优先在本地节点(协调节点)的分片中查询,没有的话再到其它节点中查询。
这种方式可以提高查询性能,假设一个索引库有5个分片,这5个分片都在Node3节点里面,客户端的查询请求正好也分配到了Node3节点上,这样在查询这5个分片的数据就都在Node3节点上进行查询了,每个分片返回结果数据的时候就不需要跨网络传输数据了,可以节省网络传输的时间。
但是这种方式也会有弊端,如果这个节点在某一时刻接收到的查询请求比较多的时候,会对当前节点造成比较大的压力,因为这些查询请求都会优先查询这个节点上的分片数据。

searchRequest.preference("_local");


此时是可以查到所有数据的。
数据总数:6
{"name":"jessica","age":18}
{"name":"lilei","age":18}
{"name":"dave","age":19}
{"name":"jack","age":29}
{"name":"lili","age":29}
{"name":"tom","age":18}

_only_local

1
2
3
4
5
6
7
8
9
10
表示查询只会在本地节点的分片中查询。
这种方式只会在查询请求所在的节点上进行查询,查询速度比较快,但是数据可能不完整,因为我们无法保证索引库的分片正好都在这一个节点上。

searchRequest.preference("_only_local");

数据总数:2
{"name":"dave","age":19}
{"name":"tom","age":18}

注意:大家在自己本地试验的时候,结果和我的可能不一样,因为请求不一定会分到哪一个节点上面。

_only_nodes

1
2
3
4
5
表示只在指定的节点中查询。
可以控制只在指定的节点中查询某一个索引库的分片信息。
但是注意:这里指定的节点列表里面,必须包含指定索引库的所有分片,如果从这些节点列表中获取到的索引库的分片个数不完整,程序会报错。
这种情况适用于在某种特殊情况下,集群中的个别节点压力比较大,短时间内又无法恢复,那么我们在查询的时候可以规避掉这些节点,只选择一些正常的节点进行查询。
前提是索引库的分片有副本,如果没有副本,只有一个主分片,就算主分片的节点压力比较大,那也只能查询这个节点了。
1
2
3
4
5
在这里面需要指定节点ID,节点ID应该如何获取呢?

通过ES中针对节点信息的RestAPI可以快速获取:
http://bigdata01:9200/_nodes?pretty
返回的信息有点多,建议在浏览器中访问。

image-20230611225100064

1
2
3
4
5
最终可以获取到三个节点的ID:

注意:这个节点ID是集群随机生成的一个字符串,所以每个人的集群节点ID都是不一样的。

目前这个索引库的分片分布在3个节点上

image-20230611225316854

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
在这里可以指定一个或者多个节点ID,多个节点ID之间使用逗号分割即可
首先指定bigdata01节点的ID

searchRequest.preference("_only_nodes:KzjZauWGRt6hJRKTgZ7BcA");

注意:执行这个查询会报错,错误提示找不到pre索引库的2号分片的数据,2号分片是在bigdata03节点上
{"error":{"root_cause":[{"type":"illegal_argument_exception","reason":"no data nodes with criteria [l7H4B3pdRm6ckBWd7ODS5Q] found for shard: [pre][2]"}],"type":"illegal_argument_exception","reason":"no data nodes with criteria [l7H4B3pdRm6ckBWd7ODS5Q] found for shard: [pre][2]"},"status":400}

再把bigdata03的节点ID添加到里面
searchRequest.preference("_only_nodes:l7H4B3pdRm6ckBWd7ODS5Q,KzjZauWGRt6hJRKTgZ7BcA");
执行发现还是会报错,提示找不到pre索引库的3号分片,3号分片是在bigdata02节点上的
{"error":{"root_cause":[{"type":"illegal_argument_exception","reason":"no data nodes with criterion [l7H4B3pdRm6ckBWd7ODS5Q,KzjZauWGRt6hJRKTgZ7BcA] found for shard: [pre][3]"}],"type":"illegal_argument_exception","reason":"no data nodes with criterion [l7H4B3pdRm6ckBWd7ODS5Q,KzjZauWGRt6hJRKTgZ7BcA] found for shard: [pre][3]"},"status":400}

再把bigdata02的节点ID添加到里面
searchRequest.preference("_only_nodes:l7H4B3pdRm6ckBWd7ODS5Q,KzjZauWGRt6hJRKTgZ7BcA,nS_RptvTQDuRYTplia24WA");
1
2
3
4
5
6
7
8
此时执行就可以正常执行了:
数据总数:6
{"name":"jessica","age":18}
{"name":"lilei","age":18}
{"name":"dave","age":19}
{"name":"jack","age":29}
{"name":"lili","age":29}
{"name":"tom","age":18}
1
注意:在ES7.x版本之前,_only_nodes后面可以只指定某一个索引库部分分片所在的节点信息,如果不完整,不会报错,只是返回的数据是不完整的。

_prefer_nodes

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
表示优先在指定的节点上查询。
优先在指定的节点上查询索引库分片中的数据
如果某个节点比较空闲,尽可能的多在这个节点上查询,减轻集群中其他节点的压力,尽可能实现负载均衡。
这里可以指定一个或者多个节点

searchRequest.preference("_prefer_nodes:l7H4B3pdRm6ckBWd7ODS5Q");

最终可以查询到完整的结果:
数据总数:6
{"name":"jessica","age":18}
{"name":"lilei","age":18}
{"name":"dave","age":19}
{"name":"jack","age":29}
{"name":"lili","age":29}
{"name":"tom","age":18}

_shards(常用)

1
2
3
4
5
6
7
8
9
10
11
12
表示只查询索引库中指定分片的数据。
在查询的时候可以指定只查询索引库中指定分片中的数据,其实有点类似于Hive中的分区表的特性。
如果我们提前已经知道需要查询的数据都在这个索引库的哪些分片里面,在这里提前指定对应分片编号,这样查询请求就只会到这些分片里面进行查询,这样可以提高查询效率,减轻集群压力。
可以指定一个或者多个分片编号,分片编号是从0开始的。

searchRequest.preference("_shards:0,1");

最终可以看到这两个分区里面的数据:
数据总数:3
{"name":"jessica","age":18}
{"name":"lilei","age":18}
{"name":"dave","age":19}
1
2
3
那我们如何控制将某一类型的数据添加到指定分片呢?

不要着急,一会就会讲到。

custom-string

1
2
3
4
自定义一个参数,不能以下划线(_)开头。
有时候我们希望多次查询使用索引库中相同的分片,因为分片会有副本,正常情况下如果不做控制,那么两次查询的时候使用的分片可能会不一样,第一次查询可能使用的是主分片,第二次查询可能使用的是副本分片。

大家可能会有疑问,不管是主分片,还是副本分片,这些分片里面的数据是完全一样的,就算两次查询使用的不是相同分片又会有什么问题吗?
1
2
3
4
5
6
会有问题的!如果searchType使用的是QUERY_THEN_FETCH,此时分片里面的数据在计算打分依据的时候是根据当前节点里面的词频和文档频率,两次查询使用的分片不是同一个,这样就会导致在计算打分依据的时候使用的样本不一致,最终导致两次相同的查询条件返回的结果不一样。
当然了,如果你使用的是DFS_QUERY_THEN_FETCH就不会有这个问题了,但是DFS_QUERY_THEN_FETCH对性能损耗会大一些,所以并不是所有情况下都使用这种searchType。

通过自定义参数的设置,只要两次查询使用的自定义参数是同一个,这样就可以保证这两次查询使用的分片是一样的,那么这两次查询的结果肯定是一样的。

注意:自定义参数不能以_开头。
1
2
3
4
5
6
7
8
9
10
searchRequest.preference("abc");//自定义参数

查询到的结果还是完整的。
数据总数:6
{"name":"jessica","age":18}
{"name":"lilei","age":18}
{"name":"dave","age":19}
{"name":"jack","age":29}
{"name":"lili","age":29}
{"name":"tom","age":18}

ES中的routing路由功能

1
2
3
4
5
6
7
ES在添加数据时,会根据id或者routing参数进行hash,得到hash值再与该索引库的分片数量取模,得到的值即为存入的分片编号

如果多条数据使用相同的routing,那么最终计算出来的分片编号都是一样的,那么这些数据就可以存储到相同的分片里面了。

后期查询的只需要到指定分片中查询即可,可以显著提高查询性能。

如果在面试的时候面试官问你如何在ES中实现极速查询,其实就是问这个routing路由功能的。
1
2
3
4
5
6
7
8
9
10
11
下面来演示一下:
创建一个新的索引库,指定5个分片,0个副本。
[root@bigdata01 ~]# curl -H "Content-Type: application/json" -XPUT 'http://bigdata01:9200/rout/' -d'{"settings":{"number_of_shards":5,"number_of_replicas":0}}'

初始化数据:
[root@bigdata01 ~]# curl -H "Content-Type: application/json" -XPOST 'http://bigdata01:9200/rout/_doc/1?routing=class1' -d'{"name":"tom","age":18}'
[root@bigdata01 ~]# curl -H "Content-Type: application/json" -XPOST 'http://bigdata01:9200/rout/_doc/2?routing=class1' -d'{"name":"jack","age":29}'
[root@bigdata01 ~]# curl -H "Content-Type: application/json" -XPOST 'http://bigdata01:9200/rout/_doc/3?routing=class1' -d'{"name":"jessica","age":18}'
[root@bigdata01 ~]# curl -H "Content-Type: application/json" -XPOST 'http://bigdata01:9200/rout/_doc/4?routing=class1' -d'{"name":"dave","age":19}'
[root@bigdata01 ~]# curl -H "Content-Type: application/json" -XPOST 'http://bigdata01:9200/rout/_doc/5?routing=class1' -d'{"name":"lilei","age":18}'
[root@bigdata01 ~]# curl -H "Content-Type: application/json" -XPOST 'http://bigdata01:9200/rout/_doc/6?routing=class1' -d'{"name":"lili","age":29}'
1
2
3
4
5
6
7
8
9
10
11
12
13
14
如果是使用的JavaAPI,那么需要通过使用routing函数指定。

private static void addIndexByJson(RestHighLevelClient client) throws IOException {
IndexRequest request = new IndexRequest("emp");
request.id("10");
String jsonString = "{" +
"\"name\":\"jessic\"," +
"\"age\":20" +
"}";
request.source(jsonString, XContentType.JSON);
request.routing("class1");
//执行
client.index(request, RequestOptions.DEFAULT);
}
1
查看数据在分片中的分布情况,发现所有数据都在0号分片里面,说明routing参数生效了。

image-20230611232004430

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
通过代码查询的时候,可以通过偏好查询指定只查询0号分片里面的数据。

SearchRequest searchRequest = new SearchRequest();
searchRequest.indices("rout");

//指定分片查询方式
searchRequest.preference("_shards:0");

这样就可以查看所有的数据:
数据总数:6
{"name":"tom","age":18}
{"name":"jack","age":29}
{"name":"jessica","age":18}
{"name":"dave","age":19}
{"name":"lilei","age":18}
{"name":"lili","age":29}
1
2
3
4
5
6
7
8
9
10
11
12
13
通过偏好查询中的_shard手工指定分片编号在使用的时候不太友好,需要我们单独维护一份数据和分片之间的关系,比较麻烦。
还有一种比较简单常用的方式是在查询的时候设置相同的路由参数,这样就可以快速查询到使用这个路由参数添加的数据了。
底层其实是会计算这个路由参数对应的分片编号,最终到指定的分片中查询数据。

SearchRequest searchRequest = new SearchRequest();
//指定索引库,支持指定一个或者多个,也支持通配符,例如:user*
searchRequest.indices("rout");

//指定分片查询方式
//searchRequest.preference("_shards:0");

//指定路由参数
searchRequest.routing("class1");
1
2
3
4
5
6
7
8
结果如下
数据总数:6
{"name":"tom","age":18}
{"name":"jack","age":29}
{"name":"jessica","age":18}
{"name":"dave","age":19}
{"name":"lilei","age":18}
{"name":"lili","age":29}
1
2
3
4
5
6
我们把routing参数修改一下,改为class2
//指定路由参数
searchRequest.routing("class2");

此时结果如下:
数据总数:0
1
2
3
从这可以看出来,这个routing参数确实生效了。

注意:routing机制使用不好可能会导致数据倾斜,就是有的分片里面数据很多,有的分片里面数据很少。

ES的索引库模板(了解)

1
2
3
在实际工作中针对一批大量数据存储的时候需要使用多个索引库,如果手工指定每个索引库的配置信息的话就很麻烦了。
配置信息其实主要就是settings和mapping。
可以通过提前创建一个索引库模板,后期在创建索引库的时候,只要索引库的命名符合一定的要求就可以直接套用模板中的配置。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
下面看一个案例:
首先创建两个索引库模板:
第一个索引库模板:
[root@bigdata01 ~]#curl -H "Content-Type: application/json" -XPUT 'http://bigdata01:9200/_template/t_1' -d '
{
"template" : "*",
"order" : 0,
"settings" : {
"number_of_shards" : 2
},
"mappings" : {
"properties":{
"name":{"type":"text"},
"age":{"type":"integer"}
}
}
}
'
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
第二个索引库模板:
[root@bigdata01 ~]#curl -H "Content-Type: application/json" -XPUT 'http://bigdata01:9200/_template/t_2' -d '
{
"template" : "te*",
"order" : 1,
"settings" : {
"number_of_shards" : 3
},
"mappings" : {
"properties":{
"name":{"type":"text"},
"age":{"type":"long"}
}
}
}
'
1
2
3
4
注意:order值大的模板内容会覆盖order值小的。

第一个索引库模板默认会匹配所有的索引库,第二个索引库模板只会匹配索引库名称以te开头的索引库,通过template属性配置的。
如果我们创建的索引库名称满足第二个就会使用第二个模板,不满足的话才会使用第一个模板。
1
2
3
下面创建一个索引库,索引库名称为:test10

[root@bigdata01 ~]# curl -XPUT 'http://localhost:9200/test10'
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
查看索引库test10的setting和mapping信息。

[root@bigdata01 ~]# curl -XGET 'http://bigdata01:9200/test10/_settings?pretty'
{
"test10" : {
"settings" : {
"index" : {
"routing" : {
"allocation" : {
"include" : {
"_tier_preference" : "data_content"
}
}
},
"number_of_shards" : "3",
"provided_name" : "test10",
"creation_date" : "1804935156129",
"number_of_replicas" : "1",
"uuid" : "iJLdIRwQSpagzEtIu0LDEw",
"version" : {
"created" : "7130499"
}
}
}
}
}
[root@bigdata01 ~]# curl -XGET 'http://bigdata01:9200/test10/_mapping?pretty'
{
"test10" : {
"mappings" : {
"properties" : {
"age" : {
"type" : "long"
},
"name" : {
"type" : "text"
}
}
}
}
}
1
2
3
接下来再创建一个索引库,索引库名称为hello

[root@bigdata01 ~]# curl -XPUT 'http://bigdata01:9200/hello'
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
[root@bigdata01 ~]# curl -XGET 'http://bigdata01:9200/hello/_settings?pretty' 
{
"hello" : {
"settings" : {
"index" : {
"routing" : {
"allocation" : {
"include" : {
"_tier_preference" : "data_content"
}
}
},
"number_of_shards" : "2",
"provided_name" : "hello",
"creation_date" : "1804935301339",
"number_of_replicas" : "1",
"uuid" : "xkg-XXSQSHKcJ_5nxyTPTQ",
"version" : {
"created" : "7130499"
}
}
}
}
}
[root@bigdata01 ~]# curl -XGET 'http://bigdata01:9200/hello/_mapping?pretty'
{
"hello" : {
"mappings" : {
"properties" : {
"age" : {
"type" : "integer"
},
"name" : {
"type" : "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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
通过结果可以看出来hello这个索引库使用到了第一个索引库模板。

后期想要查看索引库模板内容可以这样查看:
[root@bigdata01 ~]# curl -XGET 'http://bigdata01:9200/_template/t_*?pretty'
{
"t_1" : {
"order" : 0,
"index_patterns" : [
"*"
],
"settings" : {
"index" : {
"number_of_shards" : "2"
}
},
"mappings" : {
"properties" : {
"name" : {
"type" : "text"
},
"age" : {
"type" : "integer"
}
}
},
"aliases" : { }
},
"t_2" : {
"order" : 1,
"index_patterns" : [
"te*"
],
"settings" : {
"index" : {
"number_of_shards" : "3"
}
},
"mappings" : {
"properties" : {
"name" : {
"type" : "text"
},
"age" : {
"type" : "long"
}
}
},
"aliases" : { }
}
}
1
2
想要删除索引库模板可以这样做:
[root@bigdata01 ~]# curl -XDELETE 'http://bigdata01:9200/_template/t_2'

ES的索引库别名(了解)

1
2
3
4
5
6
7
在工作中使用ES收集应用的运行日志,每个星期创建一个索引库,这样时间长了就会创建很多的索引库,操作和管理的时候很不方便。
由于新增索引数据只会操作当前这个星期的索引库,所以为了使用方便,我们就创建了两个索引库别名:curr_week和last_3_month。

-curr_week:这个别名指向当前这个星期的索引库,新增数据使用这个索引库别名。
-last_3_month:这个别名指向最近三个月的所有索引库,因为我们的需求是需要查询最近三个月的日志信息。

后期只需要修改这两个别名和索引库之间的指向关系即可,应用层代码不需要任何改动。
1
2
3
4
5
6
7
8
9
10
11
12
下面来演示一下这个案例:
假设ES已经收集了一段时间的日志数据,每一星期都会创建一个索引库,所以目前创建了4个索引库:
[root@bigdata01 ~]# curl -H "Content-Type: application/json" -XPUT 'http://bigdata01:9200/log_20260301/'
[root@bigdata01 ~]# curl -H "Content-Type: application/json" -XPUT 'http://bigdata01:9200/log_20260308/'
[root@bigdata01 ~]# curl -H "Content-Type: application/json" -XPUT 'http://bigdata01:9200/log_20260315/'
[root@bigdata01 ~]# curl -H "Content-Type: application/json" -XPUT 'http://bigdata01:9200/log_20260322/'

分别向每个索引库里面初始化1条测试数据:
[root@bigdata01 ~]# curl -H "Content-Type: application/json" -XPOST 'http://bigdata01:9200/log_20260301/_doc/1' -d'{"log":"info->20260301"}'
[root@bigdata01 ~]# curl -H "Content-Type: application/json" -XPOST 'http://bigdata01:9200/log_20260308/_doc/1' -d'{"log":"info->20260308"}'
[root@bigdata01 ~]# curl -H "Content-Type: application/json" -XPOST 'http://bigdata01:9200/log_20260315/_doc/1' -d'{"log":"info->20260315"}'
[root@bigdata01 ~]# curl -H "Content-Type: application/json" -XPOST 'http://bigdata01:9200/log_20260322/_doc/1' -d'{"log":"info->20260322"}'
1
2
3
4
5
6
7
8
9
为了使用方便,我们创建了两个索引库别名:curr_week和last_3_month。
curr_week指向最新的索引库:log_20260322

[root@bigdata01 ~]# curl -H "Content-Type: application/json" -XPOST 'http://bigdata01:9200/_aliases' -d '
{
"actions" : [
{ "add" : { "index" : "log_20260322", "alias" : "curr_week" } }
]
}'
1
2
3
4
5
6
7
8
9
10
11
last_3_month指向之前3个月内的索引库:
可以同时增加多个索引别名。

[root@bigdata01 ~]# curl -H "Content-Type: application/json" -XPOST 'http://bigdata01:9200/_aliases' -d '
{
"actions" : [
{ "add" : { "index" : "log_20260301", "alias" : "last_3_month" } },
{ "add" : { "index" : "log_20260308", "alias" : "last_3_month" } },
{ "add" : { "index" : "log_20260315", "alias" : "last_3_month" } }
]
}'
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
以后使用的时候,想要操作当前星期内的数据就使用curr_week这个索引库别名就行了。
查询一下curr_week里面的数据:
这里面就1条数据,和使用索引库log_20260322查询的结果是一样的。
[root@bigdata01 ~]# curl -XGET 'http://bigdata01:9200/curr_week/_search?pretty'
{
"took" : 987,
"timed_out" : false,
"_shards" : {
"total" : 2,
"successful" : 2,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 1,
"relation" : "eq"
},
"max_score" : 1.0,
"hits" : [
{
"_index" : "log_20260322",
"_type" : "_doc",
"_id" : "1",
"_score" : 1.0,
"_source" : {
"log" : "info->20260322"
}
}
]
}
}
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
再使用last_3_month查询一下数据:
这里面返回了log_20260301、log_20260308和log_20260315这3个索引库里面的数据。

[root@bigdata01 ~]# curl -XGET 'http://bigdata01:9200/last_3_month/_search?pretty'
{
"took" : 73,
"timed_out" : false,
"_shards" : {
"total" : 6,
"successful" : 6,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 3,
"relation" : "eq"
},
"max_score" : 1.0,
"hits" : [
{
"_index" : "log_20260301",
"_type" : "_doc",
"_id" : "1",
"_score" : 1.0,
"_source" : {
"log" : "info->20260301"
}
},
{
"_index" : "log_20260308",
"_type" : "_doc",
"_id" : "1",
"_score" : 1.0,
"_source" : {
"log" : "info->20260308"
}
},
{
"_index" : "log_20260315",
"_type" : "_doc",
"_id" : "1",
"_score" : 1.0,
"_source" : {
"log" : "info->20260315"
}
}
]
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
过了一个星期之后,又多了一个新的索引库:log_20260329
创建这个索引库并初始化一条数据
[root@bigdata01 ~]# curl -H "Content-Type: application/json" -XPUT 'http://bigdata01:9200/log_20260329/'
[root@bigdata01 ~]# curl -H "Content-Type: application/json" -XPOST 'http://bigdata01:9200/log_20260329/_doc/1' -d'{"log":"info->20260329"}'

此时就需要修改curr_week别名指向的索引库了,需要先删除之前的关联关系,再增加新的。
删除curr_week和log_20260322之间的关联关系。
[root@bigdata01 ~]# curl -H "Content-Type: application/json" -XPOST 'http://bigdata01:9200/_aliases' -d '
{
"actions" : [
{ "remove" : { "index" : "log_20260322", "alias" : "curr_week" } }
]
}'

新增curr_week和log_20260329之间的关联关系
[root@bigdata01 ~]# curl -H "Content-Type: application/json" -XPOST 'http://bigdata01:9200/_aliases' -d '
{
"actions" : [
{ "add" : { "index" : "log_20260329", "alias" : "curr_week" } }
]
}'

此时再查询curr_week中的数据其实就是查询索引库log_20260329里面的数据了。
1
2
3
4
5
6
7
然后再把索引库log_20260322添加到last_3_month别名中:
[root@bigdata01 ~]# curl -H "Content-Type: application/json" -XPOST 'http://bigdata01:9200/_aliases' -d '
{
"actions" : [
{ "add" : { "index" : "log_20260322", "alias" : "last_3_month" } }
]
}'
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
这些关联别名映射关系和移除别名映射关系的操作需要写个脚本定时执行,这样就可以实现别名自动关联到指定索引库了。

假设时间长了,我们如果忘记了这个别名下对应的都有哪些索引库,可以使用下面的方法查看一下:
[root@bigdata01 ~]# curl -XGET 'http://bigdata01:9200/_alias/curr_week?pretty'
{
"log_20260329" : {
"aliases" : {
"curr_week" : { }
}
}
}
[root@bigdata01 ~]# curl -XGET 'http://bigdata01:9200/_alias/last_3_month?pretty'
{
"log_20260315" : {
"aliases" : {
"last_3_month" : { }
}
},
"log_20260308" : {
"aliases" : {
"last_3_month" : { }
}
},
"log_20260301" : {
"aliases" : {
"last_3_month" : { }
}
},
"log_20260322" : {
"aliases" : {
"last_3_month" : { }
}
}
}
1
2
3
4
5
6
7
8
9
如果想知道哪些别名指向了这个索引,可以这样查看:
[root@bigdata01 ~]# curl -XGET 'http://bigdata01:9200/log_20260301/_alias/*?pretty'
{
"log_20260301" : {
"aliases" : {
"last_3_month" : { }
}
}
}
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
注意:针对3个月以前的索引基本上就很少再使用了,为了减少对ES服务器的性能损耗(主要是内存的损耗),建议把这些长时间不使用的索引库close掉,close之后这个索引库里面的索引数据就不支持读写操作了,close并不会删除索引库里面的数据,后期想要重新读写这个索引库里面的数据的话,可以通过open把索引库打开。

将log_20260301索引库close掉:
[root@bigdata01 ~]# curl -XPOST 'http://bigdata01:9200/log_20260301/_close'

此时再查看这个索引库的数据就查询不到了:
会提示这个索引库已经被close掉了。
[root@bigdata01 ~]# curl -XGET 'http://bigdata01:9200/log_20260301/_search?pretty'
{
"error" : {
"root_cause" : [
{
"type" : "index_closed_exception",
"reason" : "closed",
"index_uuid" : "VGfSvKVJRjy5h3aCcsveKQ",
"index" : "log_20260301"
}
],
"type" : "index_closed_exception",
"reason" : "closed",
"index_uuid" : "VGfSvKVJRjy5h3aCcsveKQ",
"index" : "log_20260301"
},
"status" : 400
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
注意:这些close之后的索引库需要从索引库别名中移除掉,否则会导致无法使用从索引库别名查询数据,因为这个索引库别名中映射的有已经close掉的索引库。
[root@bigdata01 ~]# curl -XGET 'http://bigdata01:9200/last_3_month/_search?pretty'
{
"error" : {
"root_cause" : [
{
"type" : "index_closed_exception",
"reason" : "closed",
"index_uuid" : "VGfSvKVJRjy5h3aCcsveKQ",
"index" : "log_20260301"
}
],
"type" : "index_closed_exception",
"reason" : "closed",
"index_uuid" : "VGfSvKVJRjy5h3aCcsveKQ",
"index" : "log_20260301"
},
"status" : 400
}
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
接下来将log_20260301索引库重新open(打开)。
[root@bigdata01 ~]# curl -XPOST 'http://bigdata01:9200/log_20260301/_open'

索引库open之后就可以查询了
[root@bigdata01 ~]# curl -XGET 'http://bigdata01:9200/log_20260301/_search?pretty'
{
"took" : 5,
"timed_out" : false,
"_shards" : {
"total" : 2,
"successful" : 2,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 1,
"relation" : "eq"
},
"max_score" : 1.0,
"hits" : [
{
"_index" : "log_20260301",
"_type" : "_doc",
"_id" : "1",
"_score" : 1.0,
"_source" : {
"log" : "info->20260301"
}
}
]
}
}

索引库别名也可以正常使用了
1
索引库close掉之后,虽然对ES服务器没有性能损耗了,但是对ES集群的磁盘占用还是存在的,所以可以根据需求,将一年以前的索引库彻底删除掉。

ES SQL

1
2
3
4
5
6
7
8
针对ES中的结构化数据,使用SQL实现聚合统计会很方便,可以减少很多工作量。

ES SQL支持常见的SQL语法,包括分组、排序、函数等,但是目前不支持JOIN。

在使用的时候可以使用SQL命令行、RestAPI、JDBC、ODBC等方式操作。
本地测试的时候使用SQL命令行更加方便。
想要实现跨语言调用使用RestAPI更加方便。
Java程序员使用JDBC方式更方便。

ES 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
[es@bigdata01 elasticsearch-7.13.4]$ bin/elasticsearch-sql-cli http://bigdata01:9200
sql> select * from user;
age | name
---------------+---------------
20 |tom
15 |tom
17 |jack
19 |jess
23 |mick
12 |lili
28 |john
30 |jojo
16 |bubu
21 |pig
19 |mary
60 |刘德华
20 |刘老二
sql> select * from user where age > 20;
age | name
---------------+---------------
23 |mick
28 |john
30 |jojo
21 |pig
60 |刘德华
1
2
3
4
5
6
7
8
9
如果想要实现模糊查询,使用sql中的like是否可行?
sql> select * from user where name like '刘华';
age | name
---------------+---------------
sql> select * from user where name like '刘%';
age | name
---------------+---------------
60 |刘德华
20 |刘老二
1
2
3
4
5
6
7
8
like这种方式其实就是普通的查询了,无法实现分词查询。
想要实现分词查询,需要使用match。

sql> select * from user where match(name,'刘华');
age | name
---------------+---------------
60 |刘德华
20 |刘老二
1
退出ES SQL命令行,需要输入exit;

RestAPI下ES SQL的使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
查询user索引库中的数据,根据age倒序排序,获取前5条数据。
[root@bigdata01 ~]# curl -H "Content-Type: application/json" -XPOST 'http://bigdata01:9200/_sql?format=txt' -d'
{
"query":"select * from user order by age desc limit 5"
}
'
age | name
---------------+---------------
60 |刘德华
30 |jojo
28 |john
23 |mick
21 |pig

JDBC操作ES SQL

1
2
3
4
5
6
首先添加ES sql-jdbc的依赖。
<dependency>
<groupId>org.elasticsearch.plugin</groupId>
<artifactId>x-pack-sql-jdbc</artifactId>
<version>7.13.4</version>
</dependency>
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
package com.imooc.es;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.Statement;
import java.util.Properties;

/**
* JDBC操作ES SQL
* Created by xuwei
*/
public class EsJdbcOp {
public static void main(String[] args) throws Exception{
//指定jdbcUrl
String jdbcUrl = "jdbc:es://http://bigdata01:9200/?timezone=UTC+8";
Properties properties = new Properties();
//获取JDBC连接
Connection conn = DriverManager.getConnection(jdbcUrl, properties);
Statement stmt = conn.createStatement();
ResultSet results = stmt.executeQuery("select name,age from user order by age desc limit 5");
while (results.next()){
String name = results.getString(1);
int age = results.getInt(2);
System.out.println(name+"--"+age);
}

//关闭连接
stmt.close();
conn.close();
}
}
1
2
3
注意:jdbc这种方式目前无法免费使用,需要购买授权。

Exception in thread "main" java.sql.SQLInvalidAuthorizationSpecException: current license is non-compliant for [jdbc]
1
所以在工作中常用的是RestAPI这种方式。

ES优化策略

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
1.ES中Too many open files的问题。

ES中的索引数据都是存储在磁盘文件中的,每一条数据在底层都会产生一份索引片段文件
这些索引数据默认的存储目录是在ES安装目录下的data目录里面。

[es@bigdata01 index]$ pwd
/data/soft/elasticsearch-7.13.4/data/nodes/0/indices/28q_rMHAR4GJBImzb40woA/0/index
[es@bigdata01 index]$ ll
total 52
-rw-rw-r--. 1 es es 479 Mar 12 15:21 _0.cfe
-rw-rw-r--. 1 es es 3302 Mar 12 15:21 _0.cfs
-rw-rw-r--. 1 es es 363 Mar 12 15:21 _0.si
-rw-rw-r--. 1 es es 479 Mar 12 15:21 _1.cfe
-rw-rw-r--. 1 es es 2996 Mar 12 15:21 _1.cfs
-rw-rw-r--. 1 es es 363 Mar 12 15:21 _1.si
-rw-rw-r--. 1 es es 923 Mar 12 16:28 _2_1.fnm
-rw-rw-r--. 1 es es 103 Mar 12 16:28 _2_1_Lucene80_0.dvd
-rw-rw-r--. 1 es es 160 Mar 12 16:28 _2_1_Lucene80_0.dvm
-rw-rw-r--. 1 es es 479 Mar 12 16:28 _2.cfe
-rw-rw-r--. 1 es es 3718 Mar 12 16:28 _2.cfs
-rw-rw-r--. 1 es es 363 Mar 12 16:28 _2.si
-rw-rw-r--. 1 es es 533 Mar 12 16:33 segments_4
-rw-rw-r--. 1 es es 0 Mar 12 15:21 write.lock

注意:路径中的28q_rMHAR4GJBImzb40woA表示是索引库的UUID。

image-20230611234857593

1
2
3
4
5
6
7
8
9
10
11
12
ES在查询索引库里面数据的时候需要读取所有的索引片段,如果索引库中数据量比较多,那么ES在查询的时候就需要读取很多索引片段文件,此时可能就会达到Linux系统的极限,因为Linux会限制系统内最大文件打开数。
这个最大文件打开数的的配置在安装ES集群的时候我们已经修改过了:
主要就是这些参数:
[root@bigdata01 soft]# vi /etc/security/limits.conf
* soft nofile 65536
* hard nofile 131072
* soft nproc 2048
* hard nproc 4096

理论上来说,不管我们将最大文件打开数修改为多大,在使用的时候都有可能会出问题,因为ES中的数据是越来越多的,那如何解决?
其实也不用过于担心,因为ES中默认会有一个自动的索引片段合并机制,这样可以保证ES中不会产生过多的索引片段。
只要是单个索引片段文件小于5G的,在自动索引片段合并机制触发的时候都会进行合并。
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
2.索引合并优化,清除标记为删除状态的索引数据。

咱们前面分析过,ES中的删除并不是真正的删除,只是会给数据标记一个删除状态,索引片段在合并的时候,是会把索引片段中标记为删除的数据真正删掉,这样也是可以提高性能的,因为标记为删除状态的数据是会参与查询的,只不过会被过滤掉。

索引片段合并除了可以避免产生Too many open files这个问题,其实它也是可以显著提升查询性能的,因为我们读取一个中等大小的文件肯定是比读取很多个小文件效率更高的

除了等待自动的索引片段合并,也可以手工执行索引片段合并操作,但是要注意:索引片段合并操作是比较消耗系统IO资源的,不要在业务高峰期执行,也没必要频繁调用,可以每天凌晨执行一次。

[root@bigdata01 ~]# curl -XPOST 'http://bigdata01:9200/stu/_forcemerge'
合并之后会的索引片段就变成了这样,这些文件其实属于一个索引片段,都是以_3开头的:
[es@bigdata01 index]$ ll
total 72
-rw-rw-r--. 1 es es 158 Mar 14 15:47 _3.fdm
-rw-rw-r--. 1 es es 527 Mar 14 15:47 _3.fdt
-rw-rw-r--. 1 es es 64 Mar 14 15:47 _3.fdx
-rw-rw-r--. 1 es es 922 Mar 14 15:47 _3.fnm
-rw-rw-r--. 1 es es 202 Mar 14 15:47 _3.kdd
-rw-rw-r--. 1 es es 69 Mar 14 15:47 _3.kdi
-rw-rw-r--. 1 es es 200 Mar 14 15:47 _3.kdm
-rw-rw-r--. 1 es es 159 Mar 14 15:47 _3_Lucene80_0.dvd
-rw-rw-r--. 1 es es 855 Mar 14 15:47 _3_Lucene80_0.dvm
-rw-rw-r--. 1 es es 78 Mar 14 15:47 _3_Lucene84_0.doc
-rw-rw-r--. 1 es es 92 Mar 14 15:47 _3_Lucene84_0.pos
-rw-rw-r--. 1 es es 305 Mar 14 15:47 _3_Lucene84_0.tim
-rw-rw-r--. 1 es es 74 Mar 14 15:47 _3_Lucene84_0.tip
-rw-rw-r--. 1 es es 261 Mar 14 15:47 _3_Lucene84_0.tmd
-rw-rw-r--. 1 es es 59 Mar 14 15:47 _3.nvd
-rw-rw-r--. 1 es es 103 Mar 14 15:47 _3.nvm
-rw-rw-r--. 1 es es 575 Mar 14 15:47 _3.si
-rw-rw-r--. 1 es es 316 Mar 14 15:47 segments_5
-rw-rw-r--. 1 es es 0 Mar 12 15:21 write.lock

如果一个索引库中的数据已经非常多了,手工执行索引片段合并操作可能会产生一些非常大的索引片段(超过5G的),如果继续向这个索引库里面写入新的数据,那么ES的自动索引片段合并机制就不会再考虑这些非常大的索引片段了(超过5G的),这样会导致索引库中保留了非常大的索引片段,从而降低搜索性能。

这种问题该如何解决呢?往下面继续看!
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
3.分片和副本个数调整

分片多的话,可以提升建立索引的能力,单个索引库,建议使用5-20个分片比较合适。
分片数过少或过多,都会降低检索效率。
分片数过多会导致检索时打开比较多的文件,另外也会导致多台服务器之间的通讯。
而分片数过少会导致单个分片索引过大,所以检索速度也会慢。
建议单个分片存储20G左右的索引数据【最高也不要超过50G,否则性能会很差】
所以,大致有一个公式:分片数量=数据总量/20G

当数据规模超过单个索引库最大存储能力的时候,只需要新建一个索引库即可,所以ES中的海量数据存储能力是需要依靠多个索引库的,这样就可以解决前面所说的索引库中单个索引片段过大的问题。

副本多的话,理论上来说可以提升检索的能力,但是如果设置很多副本的话也会对服务器造成额外的压力,因为主分片需要给所有副本分片同步数据,所以建议最多设置1-2个副本即可。

注意:从ES7.x版本开始,集群中每个节点默认支持最多1000个了片,这块主要是考虑到单个节点的性能问题,如果集群内每个节点的性能都比较强,当然也是支持修改的。

先查看一下现在集群默认的参数配置:
现在里面的参数都是空的。
[root@bigdata01 ~]# curl -XGET 'http://bigdata01:9200/_cluster/settings?pretty'
{
"persistent" : { },
"transient" : { }
}

修改节点支持的最大分片数量
[root@bigdata01 ~]# curl -H "Content-Type: application/json" -XPUT 'http://bigdata01:9200/_cluster/settings' -d '{ "persistent": { "cluster.max_shards_per_node": "10000" } }'

重新查询集群最新的参数配置:
[root@bigdata01 ~]# curl -XGET 'http://bigdata01:9200/_cluster/settings?pretty'
{
"persistent" : {
"cluster" : {
"max_shards_per_node" : "10000"
}
},
"transient" : { }
}
1
2
3
4
4.初始化数据时,建议将副本数设置为0。

如果是在项目初期,ES集群刚安装好,需要向里面批量初始化大量数据,此时建议将副本数设置为0,这样是可以显著提高入库效率的。
如果有副本的话,在批量初始化数据的同时,索引库的主分片还需要负责向副本分片同步数据,这样会影响数据的入库性能。
1
2
5. 针对不使用的index,建议close,减少性能损耗。
具体的操作方式在前面讲索引库别名的时候已经讲过了。
1
2
3
4
5
6
7
6.调整ES的JVM内存大小,单个ES实例最大不超过32G。

单个ES实例官方建议最大使用32G内存,如果超过这个内存ES也使用不了,这样会造成资源浪费。
所以在前期申请ES集群机器的时候,建议单机内存在32G左右即可。
如果由于历史遗留问题导致每台机器的内存都很大,假设是128G的,如果在这台机器上只部署一个ES实例,会造成内存资源浪费,此时有一种取巧的方式,在同一台机器上部署多个ES实例,只需要让这台机器中的每个ES实例监听不同的端口就行了。
这样这个128G内存的机器理论上至少可以部署4个ES实例。
但是这样会存在一个弊端,如果后期这台机器出现了故障,那么ES集群会同时丢失4个节点,可能会丢数据,所以还是尽量避免这种情况。

本文标题:大数据开发工程师-全文检索引擎Elasticsearch-2

文章作者:TTYONG

发布时间:2023年06月02日 - 18:06

最后更新:2023年06月19日 - 18:06

原始链接:http://tianyong.fun/%E5%A4%A7%E6%95%B0%E6%8D%AE%E5%BC%80%E5%8F%91%E5%B7%A5%E7%A8%8B%E5%B8%88-%E5%85%A8%E6%96%87%E6%A3%80%E7%B4%A2%E5%BC%95%E6%93%8EElasticsearch-2.html

许可协议: 转载请保留原文链接及作者。

多少都是爱
0%