ElasticSearch 入门
- 新增数据
- 查询数据
- 更新数据
- 删除数据
- 批量操作
ElasticSearch 高级使用
- 聚合操作
- 映射操作
- 分词操作
Spring Boot 整合 ElasticSearch
- …
本篇文章的内容均以 Docker 环境为基础。
首先拉取镜像:
1 | docker pull elasticsearch:7.4.2 |
然后下载kibana,这是一个可视化检索数据的工具:
1 | docker pull kibana:7.4.2 |
创建两个文件夹用作数据卷:
1 | mkdir -p /mydata/elasticsearch/config |
创建一个配置文件并写入配置,使得外部机器能够访问 elasticsearch:
1 | echo "http.host: 0.0.0.0" >> /mydata/elasticsearch/config/elasticsearch.yml |
这样就可以启动 elasticsearch 了:
1 | docker run --name elasticsearch -p 9200:9200 -p 9300:9300 \ |
其中开放的 9200
端口为向elasticsearch发送请求的端口,而 9300
为集群环境下elasticsearch之间互相通信的端口;"discovery.type=single-node"
表示以单节点运行elasticsearch;ES_JAVA_OPTS="-Xms64m -Xmx128m"
用于指定elasticsearch的内存占用,而且必须指定,否则elasticsearch将占用系统的全部内存;最后设置elasticsearch的挂载点。
若是启动报错:
1 | "Caused by: java.nio.file.AccessDeniedException: /usr/share/elasticsearch/data/nodes", |
这是因为我们的挂载点权限不足导致的,此时修改挂载点权限重新启动elasticsearch即可:
1 | chmod 777 /mydata/elasticsearch/data/ |
然后启动 Kibana:
1 | docker run --name kibana -e ELASTICSEARCH_HOSTS=http://www.ithui.top:9200 -p 5601:5601 -d kibana:7.4.2 |
启动完成后访问 http://www.ithui.top:5601/:
ElasticSearch 入门
ElasticSearch 通过接收请求的方式来对数据进行处理,接下来对elasticsearch进行一个简单的入门。首先是 _cat
请求,通过该请求能够查询elasticsearch的一些基本信息,具体如下:
- GET /_cat/nodes:查看所有节点
- GET /_cat/health:查看elasticsearch的健康状况
- GET /_cat/master:查看主节点
- GET /_cat/indices:查看所有索引
比如查看 elasticsearch 的所有节点,则需要发送 http://www.ithui.top/_cat/nodes 请求,结果如下:
若是想查看 elasticsearch 的健康状态,则发送 http://www.ithui.top:9200/_cat/health 请求,结果如下:
新增数据
elasticsearch通过接收PUT和POST请求来新增数据,然而在新增数据之前,我们需要来了解elasticsearch中的几个概念:
- 索引
- 类型
- 文档
- 属性
我们可以类比一下mysql中的概念来更形象地理解它们。在mysql中,若是想保存一条数据,我们首先需要创建数据库,然后在数据库中创建数据表,最后将数据作为一条记录插入数据表;而elasticsearch中的索引就相当于mysql中的数据库,类型就相当于数据表,文档就相当于一条记录。
所以,若是想在elasticsearch中新增一条数据,我们就需要指定这条数据放在哪个索引的哪个类型下,该条数据也被称为一个文档,而且这些数据是json格式的,json中的键被称为属性。
在elasticsearch中新增一条数据我们有更加专业的说法,称其为 索引一个文档
,接下来就可以发送一个请求 http://www.ithui.top:9200/customer/external/1 ,该请求表示向customer索引下的external文档存放一个标识为1的数据,数据可以存放在请求体中携带过去:
我们来分析一下该请求的返回结果:
1 | { |
其中以 _
开头的属性称为元数据,它表示的是elasticsearch中的基本信息,比如 _index
表示当前索引;_type
表示当前类型;_id
表示当前数据的标识;_version
表示版本;result
表示当前操作的状态,这里是新建状态,若是索引的文档已经存在,则状态为更新状态。
PUT请求方式同样也能够新增数据,然而它与POST有些许不同,POST能够不携带id进行数据的保存,比如:
此时elasticsearch会自动为我们分配一个唯一的id,所以若是不携带id,则每次请求都将是一次新增数据的操作。然而PUT请求方式是无法实现这样的效果的,也就是说,若是使用PUT方式发送请求,则必须携带id,否则就会报错:
查询数据
elasticsearch接收GET请求用于查询数据,比如发送 http://www.ithui.top:9200/customer/external/1 :
其中数据存放在 _source
中, _seq_no
属性和 _primary_term
属性是用来做并发控制的,数据在每次更新之后都会加1,是乐观锁机制。
它的原理是这样的,假设此时有两个请求同时来到并且均想要修改id为1的数据,那么可以让这两个请求去判断一下当前的数据是否是最新的,怎么知道数据是最新的呢?
就是判断 seq_no
和 _primary_term
属性,比如这个请求:http://www.ithui.top:9200/customer/external/1?if_seq_no=0&if_primary_term=1 。
它在更新数据前会去判断 seq_no
是否等于0, _primary_term
是否等于1,若成立,则证明数据是最新的,所以它能够修改成功:
此时需要注意了,当数据更新成功后, _seq_no
属性便会自动向上递增,此时第二个请求就无法更新了:
原因就是当前的版本与期望的版本不一致,此时若想继续更新,就需要重新查询数据,得到并发控制属性值,再进行更新。
更新数据
在新增数据中我们已然接触了更新数据,当要新增的数据已经存在时,新增操作就会变为更新操作,当然了,elasticsearch还是为我们提供了另外一种更新方式。比如 http://www.ithui.top:9200/customer/external/1/_update :
需要注意的是,若是以这样的方式进行数据更新,则请求体数据必须用 doc
属性进行封装,它与不携带 _update
进行数据更新的方式有什么区别呢?
若是不携带 _update
更新数据,则每次发送请求elasticsearch都认为是一次更新,而携带 _update
更新数据,则elasticsearch会检查当前数据是否与原数据一致,若一致,则不会进行任何的操作,包括版本号、并发控制属性等都不会发生变化。
这里的PUT方式与POST方式效果一样,没有任何区别。
删除数据
elasticsearch通过接收DELETE请求来完成数据的删除操作。比如 http://www.ithui.top:9200/customer/external/1 ,通过该请求能够删除id为1的数据;elasticsearch还支持直接删除索引,比如 http://www.ithui.top:9200/customer ,该请求将删除customer索引;但是我们无法直接删除类型,elasticsearch是不支持直接删除类型的。
批量操作
elasticsearch还支持批量操作,不过批量操作我们就需要在kibana中进行测试了,我们在前面已经启动了kibana的镜像,只需访问 http://www.ithui.top:5601/ 即可来到kibana的界面:
点击左侧导航栏的 Dev Tools
进入到开发工具:
编写好请求后点击运行图标即可发送请求,批量操作的数据格式非常有讲究:
1 | {"index":{"_id":"1"}} |
这里的 "index"
表示新增操作,并且指定了数据的id为1,而具体需要新增的数据值是在第二行存放着:{"name":"zhangsan"}
;第三行也是如此,仍然是新增操作,指定数据id为2,数据值为:{"name":"lisi"}
,这样运行之后elasticsearch会执行两个新增操作,将这两个数据保存起来。
再比如这样一段复杂的批量操作:
1 | POST /_bulk |
首先是第一行, _bulk
指定了这次操作为批量操作,因为没有设置其它的任何信息,所以接下来的所有操作都需要指定索引、类型等信息;
第二行 delete
表示一个删除操作,其后指定了索引、类型以及要删除的数据id,因为删除操作不需要携带请求体数据,所以我们可以直接在下一行编写第二个操作;
第三行 create
表示一个新增操作,并指定了索引、类型、数据id,第四行就是需要新增的数据值了;第五行与第六行也是一个新增操作;第七行和第八行是一个更新操作,而且因为是update更新,所以请求体数据需要用 doc
属性包装。
运行该操作,得到结果:
第一个删除操作,因为不存在这样的一个数据,所以删除失败了,状态为404,但是后面的操作却成功了,从这里可以说明,elasticsearch批量操作中的每个操作都是相互独立的,互相不会造成任何影响。
ElasticSearch高级使用
elasticsearch有两种检索方式,一种是前面说过的,发送REST请求,将数据以请求体的方式携带,还有一种方式就是直接将数据拼接在url路径上,比如:
1 | GET bank/_search?q=*&sort=account_number:asc |
它表示这是一个GET请求,要操作的索引是bank, _search
表示这是一次检索, q=*
表示查询所有, sort=account_number:asc
表示以 account_number
的值进行升序排序。
我们还可以这样进行检索:
1 | GET bank/_search |
两种检索方式得到的结果是一样的,来介绍一下这种检索方式的语法。首先是 query
,它用于指定查询条件, match_all
表示匹配所有,大括号后面可以编写匹配的规则;其次是 sort
,它用于指定排序条件, account_number:asc
则表示以该属性的值进行升序排序,若是想指定多个规则,可以继续在 sort
属性中进行编写:
1 | "sort": [ |
此时则表示先按 account_number
进行升序,再按 balance
进行降序排序。
通过请求体数据进行检索的方式被称为 Query DSL
,即:查询领域对象语言,在elasticsearch中我们将会大量地编写这种语言。查询领域对象语言的基本语法是:
1 | { |
首先整个语句需要被一对大括号包含,在大括号内需要编写对应的操作,比如查询,就编写:
1 | { |
在 query
中又需要指定查询的条件,比如匹配部分、匹配所有,以及匹配的规则等等,若是想要排序,则编写:
1 | { |
sort
是一个数组,表示可以指定多个排序的规则。在其中还可以指定分页,只需要设置 from
和size
属性值即可:
1 | GET bank/_search |
这里表示从第一条数据开始,每页显示5条数据。我们还能通过指定 _source
属性来决定 elasticsearch 检索出的数据中包含哪些属性值,以剔除不必要的属性:
1 | GET bank/_search |
此时表示只返回 balance
和 firstname
属性值。匹配规则中除了可以指定匹配全部外,还可以匹配指定的属性值,比如:
1 | GET bank/_search |
此时表示检索 account_number
为20的数据,这是一个精确检索的操作。它当然还支持模糊检索,比如:
1 | GET bank/_search |
此时表示检索 address
中包含mill和lane的数据,我们来看看elasticsearch返回的结果:
来观察一下这两条数据,第一条数据有一个 _score
的属性,它表示的是当前数据的得分情况,因为该数据中包含了Mill lane字符串,所以能够最大程度地匹配上我们的匹配规则,故它的得分最高;再看第二条数据,因为该数据中只包含了Mill而没有Lane,所以它的匹配度更低一些,故而得分低一些。
由此可知,elasticsearch会将匹配规则中包含的所有字符串都去与待检索的数据进行匹配,实际上,elasticsearch底层采用的是分词策略,具体是如何进行分词的我们暂且先不做了解。我们还可以根据得分情况去获知哪些数据与我们想要的数据匹配度更接近。
虽然elasticsearch可以进行分词模糊匹配,但我们若是就想查询哪些数据中含有mill road呢?这个时候,我们可以采用 match_phrase
来实现短语匹配,比如:
1 | GET bank/_search |
我们还可以进行多字段匹配,比如:
1 | GET bank/_search |
此时表示在 address
和 city
属性中匹配mill字符串,倘若有一个满足,都符合我们的匹配规则。
当我们需要同时指定多个检索规则的时候,我们可以使用 bool
属性完成复合检索:
1 | GET bank/_search |
在这段语句中,首先是 bool
属性复合了四个检索条件,然后是 must
属性,它表示必须满足检索条件,条件为 gender
等于F, address
包含mill;而 must_not
表示必须排除检索条件,条件为 age
等于28;最后是 should
,它表示应该满足检索条件,检索条件为lastname
等于Wallace。
它的关键在于即使不满足该条件,这条数据也可以被查询出来,但如果某条数据满足了 should
指定的条件,它就会得到相应的得分,可以认为这是一个加分项。
我们还可以通过 filter
过滤器实现检索,比如:
1 | GET bank/_search |
这段语句表示检索年龄在18~30之间的数据,但需要注意的是,使用 filter
属性并不会影响数据的得分。
使用 match
能够实现精确检索,但elasticsearch推荐我们使用 match
属性进行模糊检索,而使用 term
进行精确检索,比如:
1 | GET /bank/_search |
聚合
elasticsearch 除了能够检索数据,它还能够对数据进行分析,elasticsearch 中的聚合就提供了从数据中分组和提取数据的能力。
比如这样的一个需求,检索地址中包含mill的所有人的年龄分布以及平均年龄,该如何编写语句呢?
1 | GET /bank/_search |
首先通过 query
属性检索地址中包含mill的人,然后通过 aggs
指定聚合操作,第一个聚合为 ageAgg
,这是聚合操作的名字,可以取任意值,其中 terms
表示分布情况;第二个聚合为 ageAvg
,其中 avg
表示平均值,来看检索得到的结果:
其中 buckers
中的数据是每种年龄的分布情况,比如38岁的有2人,28岁的有1人;ageAvg
中的就是年龄的平均值了,为34.0。
又比如这个需求,按照年龄聚合,并求这些年龄段的人的平均薪资,语句就该这么写:
1 | GET /bank/_search |
这是一个嵌套的聚合操作,首先通过 terms
按照年龄聚合, size
表示分布有多少种情况,这里假设有100种,然后在该聚合的基础上进行平均聚合,这里需要注意平均聚合语句的位置是在年龄聚合的里面的。
映射
映射是用来定义一个文档,以及它所包含的属性是如何存储和索引的,但我们发现,在使用elasticsearch的过程中,我们并没有对数据进行类型的指定,这是因为elasticsearch会自动猜测映射类型,我们可以通过请求查看映射信息:
1 | GET bank/_mapping |
得到结果:
1 | { |
每个属性的 type
中都显示了它的类型。然而有些属性的类型并不是我们想要的,这个时候我们可以修改指定属性的类型:
1 | PUT /my_index |
按照我们获取到的映射规则对其进行设置即可。此时我们获取my_index索引的映射信息进行查看:
1 | { |
说明设置是成功的。但这种方式仅限于设置不存在的索引,若是需要设置的索引已经存在,则我们无法通过这种方式来改变属性的类型。我们需要采用另外一种方式:
1 | PUT /my_index/_mapping |
然而这种方式只能用于添加新的映射规则, 而不能修改之前属性的类型。所以若是想要修改已存在的属性类型,只能再创建一个新的索引,并指定好映射规则,再将之前的数据迁移到新的索引中来:
1 | # 创建新的映射规则索引 |
分词
在前面有说到elasticsearch是通过分词来进行模糊检索的,而分词在elasticsearch中是通过tokenizer分词器实现的,分词器通过接收一个字符流,将其分割为独立的tokens词元。
elasticsearch中默认有一些分词器,但它们都只支持英文的分词,对于中文,我们需要额外安装一个分词器,这里以 ik
分词器为例,首先下载ik分词器,下载地址:https://github.com/medcl/elasticsearch-analysis-ik
同样下载 7.4.2 版本的 ik 分词器:
在该位置复制下载地址,然后在服务器上进行安装,因为在启动 elasticsearch 镜像的时候我们做了数据卷的映射,所以只需将其下载好存放在 /mydata/elasticsearch/plugins
目录下即可:
1 | cd /mydata/elasticsearch/plugins |
下载完成后解压一下:
1 | unzip elasticsearch-analysis-ik-7.4.2.zip |
解压完成后重启elasticsearch,这样我们就安装好了ik分词器,可以来测试一下:
1 | POST _analyze |
分词结果:
1 | { |
我们来看看elasticsearch默认使用的分词器的分词效果:
1 | POST _analyze |
分词结果:
1 | { |
很显然,默认的分词器只是把每个字都当成了一个词而已,在中文的处理上与ik分词器相比就要逊色很多了。
但有时候ik分词器仍然达不到我们的需求,比如最近才流行起来的网络热词,ik分词器肯定是没有办法对其进行分词的,所以我们需要对其定制扩展词库,找到nginx映射目录下的html文件夹,在该文件夹下创建es文件夹用于存放ik分词器需要使用到的一些资源( 关于nginx的运行与配置请看文章最后一节
):
1 | cd cd /mydata/nginx/html/ |
然后在es目录下新建一个文本信息:
1 | [root@izrcf5u3j3q8xaz es]# vim fenci.txt |
将需要定制的词放在该文本中即可,此时我们测试一下访问
http://www.ithui.top/es/fenci.txt :
没有问题,我们继续下一步,来到ik分词器的配置目录:
1 | cd /mydata/elasticsearch/plugins/ik/config/ |
修改它的配置文件:
1 | vim IKAnalyzer.cfg.xml |
修改内容如下,将定制的文本地址配置进去即可:
配置完成后一定要重启elasticsearch,此时我们来测试一下:
1 | POST _analyze |
分词结果:
1 | { |
Spring Boot 整合 elasticsearch
前面介绍了elasticsearch的一些基础知识,接下来我们来看看如何在SpringBoot应用中使用elasticsearch。
想要通过Java程序操作elasticsearch,我们也需要发送请求给elasticsearch进行操作,这里我们使用ElasticSearch-Rest-Client对elasticsearch进行操作。
首先引入依赖:
1 | <dependency> |
因为SpringBoot管理了elasticsearch的依赖版本,所以我们需要指定一下elasticsearch的版本与其一致:
1 | <properties> |
然后编写一个配置类,向容器中注册一个操作elasticsearch的组件:
1 | @Configuration |
接下来我们就可以通过它操作 elasticsearch 了:
1 | @Autowired |
RestHighLevelClient提供了非常多的方式用于保存数据,但比较常用的是通过json数据直接保存,首先需要指定索引, IndexRequest indexRequest = new IndexRequest("users");
指定了users索引,然后指定数据id,接着指定数据值,最后使用client执行保存操作,然后可以拿到响应数据。
elasticsearch的其它简单操作,诸如:更新、删除等,都只需要转换一下调用方法即可,如更新操作,就需要使用client调用update方法,接下来我们看看Java程序该如何实现较为复杂的检索操作。
比如现在想聚合出年龄的分布情况,并求出每个年龄分布人群的平均薪资,就应该这样进行编写:
1 | @Test |
这些语法我们在最开始接触elasticsearch的时候都已经了解过了,只不过这里是一个链式的方法调用而已。
Nginx
首先拉取 nginx 的镜像:
1 | docker pull nginx:1.10 |
然后随意地启动一个 nginx 实例:
1 | docker run -p 80:80 --name nginx -d nginx:1.10 |
启动该 nginx 实例的目的是将 nginx 中的配置文件复制出来:
1 | docker container cp nginx:/etc/nginx . |
这样当前目录下就会产生一个nginx文件夹,将其先重命名为conf,然后再创建一个nginx文件夹,并将conf文件夹移动进去:
1 | mv nginx conf |
然后正式启动一个新的 nginx 实例:
1 | docker run -p 80:80 --name nginx \ |
将刚才准备好的 nginx 文件夹与 nginx 容器内的文件夹作一个一一映射。
索引=数据库 类型=数据表 属性=表字段 文档=一条条的数据