elasticsearch的分片(ElasticSearch分布式搜索引擎——从入门到精通)
ES分布式搜索引擎
注意: 在没有创建库的时候搜索 ,ES会创建一个库并自动创建该字段并且设置为String类型也就是text
什么是elasticsearch?
一个开源的分布式搜索引擎 ,可以用来实现搜索 、日志统计 、分析 、系统监控等功能什么是elastic stack(ELK)?
是以elasticsearch为核心的技术栈 ,包括beats 、Logstash 、kibana 、elasticsearch什么是Lucene?
是Apache的开源搜索引擎类库 ,提供了搜索引擎的核心APIelasticsearch是一款非常强大的开源搜索引擎 ,具备非常多强大功能 ,可以帮助我们从海量数据中快速找到需要的内容
ELK技术栈
本文只使用了elasticsearch ,以及kibana做可视化界面
elasticsearch结合kibana 、Logstash 、Beats ,也就是elastic stack(ELK) 。被广泛应用在日志数据分析 、实时监控等领域:
而elasticsearch是elastic stack的核心 ,负责存储 、搜索 、分析数据 。
初识elasticsearch
1. elasticsearch背景介绍
elasticsearch底层是基于lucene来实现的 。
Lucene是一个Java语言的搜索引擎类库 ,是Apache公司的顶级项目 ,由DougCutting于1999年研发 。官网地址:https://lucene.apache.org/ 。
elasticsearch的发展历史:
2004年Shay Banon基于Lucene开发了Compass 2010年Shay Banon 重写了Compass ,取名为Elasticsearch 。2. 倒排索引
倒排索引的概念是基于MySQL这样的正向索引而言的 。
2.1 正向索引设置了索引的话挺快的,但要是模糊查询则就很慢!
那么什么是正向索引呢?例如给下表(tb_goods)中的id创建索引:
如果是根据id查询 ,那么直接走索引 ,查询速度非常快 。
但如果是基于title做模糊查询,只能是逐行扫描数据 ,流程如下:
1)用户搜索数据 ,条件是title符合"%手机%"
2)逐行获取数据 ,比如id为1的数据
3)判断数据中的title是否符合用户搜索条件
4)如果符合则放入结果集 ,不符合则丢弃 。回到步骤1
逐行扫描 ,也就是全表扫描 ,随着数据量增加 ,其查询效率也会越来越低 。当数据量达到数百万时 ,就是一场灾难 。
2.2 倒排索引倒排索引中有两个非常重要的概念:
文档(Document):用来搜索的数据 ,其中的每一条数据就是一个文档 。例如一个网页 、一个商品信息 词条(Term):对文档数据或用户搜索数据 ,利用某种算法分词 ,得到的具备含义的词语就是词条。例如:我是中国人 ,就可以分为:我、是 、中国人 、中国、国人这样的几个词条创建倒排索引是对正向索引的一种特殊处理,流程如下:
将每一个文档的数据利用算法分词 ,得到一个个词条 创建表 ,每行数据包括词条 、词条所在文档id 、位置等信息 因为词条唯一性,可以给词条创建索引 ,例如hash表结构索引如图:
倒排索引的搜索流程如下(以搜索"华为手机"为例):
1)用户输入条件"华为手机"进行搜索 。
2)对用户输入内容分词 ,得到词条:华为 、手机 。
3)拿着词条在倒排索引中查找 ,可以得到包含词条的文档id:1 、2 、3。
4)拿着文档id到正向索引中查找具体文档 。
如图:
虽然要先查询倒排索引 ,再查询倒排索引 ,但是无论是词条 、还是文档id都建立了索引 ,查询速度非常快!无需全表扫描 。
2.3 正向和倒排对比概念区别:
正向索引是最传统的 ,根据id索引的方式 。但根据词条查询时 ,必须先逐条获取每个文档 ,然后判断文档中是否包含所需要的词条 ,是根据文档找词条的过程 。
而倒排索引则相反 ,是先找到用户要搜索的词条 ,根据词条得到保护词条的文档的id,然后根据id获取文档 。是根据词条找文档的过程 。
优缺点:
正向索引:
优点: 可以给多个字段创建索引 根据索引字段搜索 、排序速度非常快 缺点: 根据非索引字段 ,或者索引字段中的部分词条查找时 ,只能全表扫描 。倒排索引:
优点: 根据词条搜索 、模糊搜索时,速度非常快 缺点: 只能给词条创建索引 ,而不是字段 无法根据字段做排序3. ES数据库基本概念
elasticsearch中有很多独有的概念 ,与mysql中略有差别 ,但也有相似之处 。
3.1.文档和字段一个文档就像数据库里的一条数据 ,字段就像数据库里的列
elasticsearch是面向文档(Document)存储的 ,可以是数据库中的一条商品数据 ,一个订单信息 。文档数据会被序列化为json格式后存储在elasticsearch中:
而Json文档中往往包含很多的字段(Field) ,类似于mysql数据库中的列 。
3.2.索引和映射索引就像数据库里的表 ,映射就像数据库中定义的表结构
索引(Index) ,就是相同类型的文档的集合【类似mysql中的表】
例如:
所有用户文档 ,就可以组织在一起 ,称为用户的索引; 所有商品的文档 ,可以组织在一起,称为商品的索引; 所有订单的文档 ,可以组织在一起 ,称为订单的索引;因此,我们可以把索引当做是数据库中的表 。
数据库的表会有约束信息 ,用来定义表的结构 、字段的名称 、类型等信息 。因此 ,索引库中就有映射(mapping) ,是索引中文档的字段约束信息 ,类似表的结构约束。
3.3.mysql与elasticsearch各自长处:
Mysql:擅长事务类型操作 ,可以确保数据的安全和一致性
Elasticsearch:擅长海量数据的搜索 、分析 、计算
我们统一的把mysql与elasticsearch的概念做一下对比:
MySQL Elasticsearch 说明 Table Index 索引(index) ,就是文档的集合 ,类似数据库的表(table) Row Document 文档(Document) ,就是一条条的数据 ,类似数据库中的行(Row) ,文档都是JSON格式 Column Field 字段(Field) ,就是JSON文档中的字段 ,类似数据库中的列(Column) Schema Mapping Mapping(映射)是索引中文档的约束,例如字段类型约束 。类似数据库的表结构(Schema) SQL DSL DSL是elasticsearch提供的JSON风格的请求语句 ,用来操作elasticsearch ,实现CRUD在企业中,往往是两者结合使用:
对安全性要求较高的写操作 ,使用mysql实现 对查询性能要求较高的搜索需求 ,使用elasticsearch实现 两者再基于某种方式 ,实现数据的同步 ,保证一致性4. 安装es、kibana 、分词器
分词器的作用是什么?
创建倒排索引时对文档分词 用户搜索时 ,对输入的内容分词IK分词器有几种模式?
ik_smart:智能切分 ,粗粒度 ik_max_word:最细切分 ,细粒度IK分词器如何拓展词条?如何停用词条?
利用config目录的IkAnalyzer.cfg.xml文件添加拓展词典和停用词典 在词典中添加拓展词条或者停用词条 4.1 部署单点es 4.1.1.创建网络因为我们还需要部署kibana容器 ,因此需要让es和kibana容器互联 。这里先创建一个网络:
docker network create es-net 4.1.2.加载镜像这里我们采用elasticsearch的7.12.1版本的镜像 ,这个镜像体积非常大 ,接近1G。不建议大家自己pull 。
课前资料提供了镜像的tar包:
大家将其上传到虚拟机中 ,然后运行命令加载即可:
# 导入数据 docker load -i es.tar注意:同理还有kibana的tar包也需要这样做 。
4.1.3.运行运行docker命令 ,部署单点es:
docker run -d \ --name es \ -e "ES_JAVA_OPTS=-Xms512m -Xmx512m" \ -e "discovery.type=single-node" \ -v es-data:/usr/share/elasticsearch/data \ -v es-plugins:/usr/share/elasticsearch/plugins \ --privileged \ --network es-net \ -p 9200:9200 \ -p 9300:9300 \ elasticsearch:7.12.1命令解释:
-e "cluster.name=es-docker-cluster":设置集群名称 -e "http.host=0.0.0.0":监听的地址,可以外网访问 -e "ES_JAVA_OPTS=-Xms512m -Xmx512m":内存大小 -e "discovery.type=single-node":非集群模式 -v es-data:/usr/share/elasticsearch/data:挂载逻辑卷 ,绑定es的数据目录 -v es-logs:/usr/share/elasticsearch/logs:挂载逻辑卷 ,绑定es的日志目录 -v es-plugins:/usr/share/elasticsearch/plugins:挂载逻辑卷,绑定es的插件目录 --privileged:授予逻辑卷访问权 --network es-net :加入一个名为es-net的网络中 -p 9200:9200:端口映射配置在浏览器中输入:http://192.168.194.131/:9200 即可看到elasticsearch的响应结果:
4.2.部署kibanakibana可以给我们提供一个elasticsearch的可视化界面 ,便于我们学习 。
4.2.1.部署创建网络后 ,导入kibana压缩包 ,然后创建并启动相应容器 。【和前面部署单点es一样做法】
再运行docker命令 ,部署kibana
docker run -d \ --name kibana \ -e ELASTICSEARCH_HOSTS=http://es:9200 \ --network=es-net \ -p 5601:5601 \ kibana:7.12.1 --network es-net :加入一个名为es-net的网络中 ,与elasticsearch在同一个网络中 -e ELASTICSEARCH_HOSTS=http://es:9200":设置elasticsearch的地址 ,因为kibana已经与elasticsearch在一个网络 ,因此可以用容器名直接访问elasticsearch -p 5601:5601:端口映射配置kibana启动一般比较慢 ,需要多等待一会 ,可以通过命令:
docker logs -f kibana查看运行日志 ,当查看到下面的日志 ,说明成功:
此时 ,在浏览器输入地址访问:http://192.168.194.131:5601,即可看到结果如下图:
kibana左侧中提供了一个DevTools界面:
这个界面中可以编写DSL来操作elasticsearch 。并且对DSL语句有自动补全功能 。
4.3.安装IK分词器 4.3.1.在线安装ik插件(较慢) # 进入容器内部 docker exec -it elasticsearch /bin/bash # 在线下载并安装 ./bin/elasticsearch-plugin install https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v7.12.1/elasticsearch-analysis-ik-7.12.1.zip #退出 exit #重启容器 docker restart elasticsearch 4.3.2.离线安装ik插件(推荐)1)查看数据卷目录
安装插件需要知道elasticsearch的plugins目录位置 ,而我们用了数据卷挂载 ,因此需要查看elasticsearch的数据卷目录,通过下面命令查看:
docker volume inspect es-plugins显示结果:
[ { "CreatedAt": "2022-05-06T10:06:34+08:00", "Driver": "local", "Labels": null, "Mountpoint": "/var/lib/docker/volumes/es-plugins/_data", "Name": "es-plugins", "Options": null, "Scope": "local" } ]说明plugins目录被挂载到了:/var/lib/docker/volumes/es-plugins/_data 这个目录中 。
2)解压缩分词器安装包
下面我们需要把课前资料中的ik分词器解压缩 ,重命名为ik
3)上传到es容器的插件数据卷中
也就是/var/lib/docker/volumes/es-plugins/_data :
4)重启容器
# 4 、重启容器 docker restart es # 查看es日志 docker logs -f es5)测试:
IK分词器包含两种模式:
ik_smart:最少切分
ik_max_word:最细切分
在kibana的Dev tools中输入以下代码:
”analyzer“ 就是选择分词器模式
GET /_analyze { "analyzer": "ik_max_word", "text": "黑马程序员学习java太棒了" }结果:
{ "tokens" : [ { "token" : "黑马", "start_offset" : 0, "end_offset" : 2, "type" : "CN_WORD", "position" : 0 }, { "token" : "程序员", "start_offset" : 2, "end_offset" : 5, "type" : "CN_WORD", "position" : 1 }, { "token" : "程序", "start_offset" : 2, "end_offset" : 4, "type" : "CN_WORD", "position" : 2 }, { "token" : "员", "start_offset" : 4, "end_offset" : 5, "type" : "CN_CHAR", "position" : 3 }, { "token" : "学习", "start_offset" : 5, "end_offset" : 7, "type" : "CN_WORD", "position" : 4 }, { "token" : "java", "start_offset" : 7, "end_offset" : 11, "type" : "ENGLISH", "position" : 5 }, { "token" : "太棒了", "start_offset" : 11, "end_offset" : 14, "type" : "CN_WORD", "position" : 6 }, { "token" : "太棒", "start_offset" : 11, "end_offset" : 13, "type" : "CN_WORD", "position" : 7 }, { "token" : "了", "start_offset" : 13, "end_offset" : 14, "type" : "CN_CHAR", "position" : 8 } ] } 4.3.3 扩展词词典随着互联网的发展 ,“造词运动 ”也越发的频繁 。出现了很多新的词语 ,在原有的词汇列表中并不存在 。比如:“奥力给 ” ,“白嫖 ” 等 。
所以我们的词汇也需要不断的更新 ,IK分词器提供了扩展词汇的功能 。
1)打开IK分词器config目录:
2)在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">ext.dic</entry> </properties>3)新建一个 ext.dic ,可以参考config目录下复制一个配置文件进行修改
白嫖 奥力给4)重启elasticsearch
docker restart es # 查看 日志 docker logs -f elasticsearch日志中已经成功加载ext.dic配置文件
5)测试效果:
GET /_analyze { "analyzer": "ik_max_word", "text": "传智播客Java就业超过90%,奥力给!" }注意当前文件的编码必须是 UTF-8 格式 ,严禁使用Windows记事本编辑
4.3.4 停用词词典在互联网项目中 ,在网络间传输的速度很快 ,所以很多语言是不允许在网络上传递的 ,如:关于宗教、政治等敏感词语 ,那么我们在搜索时也应该忽略当前词汇 。
IK分词器也提供了强大的停用词功能 ,让我们在索引时就直接忽略当前的停用词汇表中的内容。
1)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">ext.dic</entry> <!--用户可以在这里配置自己的扩展停止词字典 *** 添加停用词词典--> <entry key="ext_stopwords">stopword.dic</entry> </properties>3)在 stopword.dic 添加停用词
大帅逼4)重启elasticsearch
# 重启服务 docker restart es docker restart kibana # 查看 日志 docker logs -f elasticsearch日志中已经成功加载stopword.dic配置文件
5)测试效果:
GET /_analyze { "analyzer": "ik_max_word", "text": "我是真的会谢Java就业率超过95%,大帅逼都点赞白嫖,奥力给!" }注意当前文件的编码必须是 UTF-8 格式,严禁使用Windows记事本编辑
索引库操作
索引库就类似数据库表 ,mapping映射就类似表的结构 。
我们要向es中存储数据 ,必须先创建“库 ”和“表 ” 。
1. Mapping映射属性
mapping是对索引库中文档的约束,常见的mapping属性包括:
type:字段数据类型 ,常见的简单类型有:
字符串:text(可分词的文本) 、keyword(精确值 ,例如:品牌 、国家 、ip地址)
keyword类型只能整体搜索 ,不支持搜索部分内容
数值:long 、integer 、short 、byte 、double 、float 、
布尔:boolean
日期:date
对象:object
index:是否创建索引 ,默认为true
analyzer:使用哪种分词器
properties:该字段的子字段
例如下面的json文档:
{ "age":21, "weight":52.1, "isMarried":false, "info":"真相只有一个!", "email":"zy@itcast.cn", "score":[99.1, 99.5, 98.9], "name":{ "firstName":"柯", "lastName":"南" } }对应的每个字段映射(mapping):
age:类型为 integer;参与搜索 ,因此需要index为true;无需分词器 weight:类型为float;参与搜索 ,因此需要index为true;无需分词器 isMarried:类型为boolean;参与搜索 ,因此需要index为true;无需分词器 info:类型为字符串 ,需要分词 ,因此是text;参与搜索 ,因此需要index为true;分词器可以用ik_smart email:类型为字符串 ,但是不需要分词 ,因此是keyword;不参与搜索,因此需要index为false;无需分词器 score:虽然是数组 ,但是我们只看元素的类型 ,类型为float;参与搜索,因此需要index为true;无需分词器 name:类型为object ,需要定义多个子属性 name.firstName;类型为字符串 ,但是不需要分词 ,因此是keyword;参与搜索 ,因此需要index为true;无需分词器 name.lastName;类型为字符串 ,但是不需要分词 ,因此是keyword;参与搜索 ,因此需要index为true;无需分词器2. 索引库的CRUD
CRUD简单描述:
创建索引库:PUT /索引库名 查询索引库:GET /索引库名 删除索引库:DELETE /索引库名 修改索引库(添加字段):PUT /索引库名/_mapping这里统一使用Kibana编写DSL的方式来演示。
2.1 创建索引库和映射基本语法:
请求方式:PUT 请求路径:/索引库名 ,可以自定义 请求参数:mapping映射格式:
PUT/索引库名称 { "mappings":{ "properties":{ "字段名":{ "type":"text", "analyzer":"ik_smart" }, "字段名2":{ "type":"keyword", "index":"false" }, "字段名3":{ "properties":{ "子字段":{ "type":"keyword" } } }, // ...略 } } }示例:
PUT /conan { "mappings": { "properties": { "column1":{ "type": "text", "analyzer": "ik_smart" }, "column2":{ "type": "keyword", "index": "false" }, "column3":{ "properties": { "子字段1": { "type": "keyword" }, "子字段2": { "type": "keyword" } } }, // ...略 } } } 2.2 查询索引库基本语法:
请求方式:GET
请求路径:/索引库名
请求参数:无
格式:
GET /索引库名示例:
2.3 修改索引库这里的修改是只能增加新的字段到mapping中
倒排索引结构虽然不复杂 ,但是一旦数据结构改变(比如改变了分词器) ,就需要重新创建倒排索引 ,这简直是灾难 。因此索引库一旦创建 ,无法修改mapping 。
虽然无法修改mapping中已有的字段,但是却允许添加新的字段到mapping中 ,因为不会对倒排索引产生影响 。
语法说明:
PUT /索引库名/_mapping { "properties": { "新字段名":{ "type": "integer" } } }示例:
2.4 删除索引库语法:
请求方式:DELETE
请求路径:/索引库名
请求参数:无
格式:
DELETE /索引库名在kibana中测试:
文档操作
文档操作有哪些?
创建文档:POST /{索引库名}/_doc/文档id 查询文档:GET /{索引库名}/_doc/文档id 删除文档:DELETE /{索引库名}/_doc/文档id 修改文档: 全量修改:PUT /{索引库名}/_doc/文档id 增量修改:POST /{索引库名}/_update/文档id { "doc": {字段}}1. 文档的CRUD
1.1 新增文档语法:
POST/索引库名/_doc/文档id { "字段1":"值1", "字段2":"值2", "字段3":{ "子属性1":"值3", "子属性2":"值4" }, // ... }示例:
POST/heima/_doc/1 { "info":"真相只有一个!", "email":"zy@itcast.cn", "name":{ "firstName":"柯", "lastName":"南" } }响应:
1.2 查询文档根据rest风格 ,新增是post,查询应该是get ,不过查询一般都需要条件 ,这里我们把文档id带上 。
语法:
GET /{索引库名称}/_doc/{id} //批量查询:查询该索引库下的全部文档 GET /{索引库名称}/_search通过kibana查看数据:
GET /heima/_doc/1查看结果:
1.3 删除文档删除使用DELETE请求 ,同样 ,需要根据id进行删除:
语法:
DELETE /{索引库名}/_doc/id值示例:
# 根据id删除数据 DELETE /heima/_doc/1结果:
1.4 修改文档修改有两种方式:
全量修改:直接覆盖原来的文档 增量修改:修改文档中的部分字段 1.4.1 全量修改全量修改是覆盖原来的文档 ,其本质是:
根据指定的id删除文档 新增一个相同id的文档注意:如果根据id删除时 ,id不存在 ,第二步的新增也会执行 ,也就从修改变成了新增操作了 。
语法:
PUT/{索引库名}/_doc/文档id { "字段1":"值1", "字段2":"值2", // ... 略 }示例:
PUT/heima/_doc/1 { "info":"黑马程序员高级Java讲师", "email":"zy@itcast.cn", "name":{ "firstName":"云", "lastName":"赵" } } 1.4.2 增量修改增量修改是只修改指定id匹配的文档中的部分字段 。
语法:
POST/{索引库名}/_update/文档id { "doc": { "字段名":"新的值", } }示例:
POST/heima/_update/1 { "doc":{ "email":"ZhaoYun@itcast.cn" } }RestAPI
ES官方提供了各种不同语言的客户端 ,用来操作ES 。这些客户端的本质就是组装DSL语句 ,通过http请求发送给ES 。官方文档地址:https://www.elastic.co/guide/en/elasticsearch/client/index.html
其中的Java Rest Client又包括两种:
Java Low Level Rest Client Java High Level Rest Client我们使用的是Java HighLevel Rest Client客户端API
API操作索引库
JavaRestClient操作elasticsearch的流程基本类似 。核心是client.indices()方法来获取索引库的操作对象 。
索引库操作的基本步骤:【可以根据发送请求那步的第一个参数 ,发过来判断需要创建什么XXXXRequest】
初始化RestHighLevelClient 创建XxxIndexRequest 。XXX是Create 、Get 、Delete 准备DSL( Create时需要 ,其它是无参) 发送请求 。调用RestHighLevelClient#indices().xxx()方法,xxx是create 、exists、delete 1. mapping映射分析根据MySQL数据库表结构(建表语句) ,去写索引库结构JSON。表和索引库一一对应
注意:地理坐标 、组合字段 。索引库里的地理坐标是一个字段:坐标:维度,精度 。copy_to组合字段作用是供用户查询(输入关键字可以查询多个字段)
创建索引库 ,最关键的是mapping映射,而mapping映射要考虑的信息包括:
字段名 字段数据类型 是否参与搜索 是否需要分词 如果分词 ,分词器是什么?其中:
字段名 、字段数据类型 ,可以参考数据表结构的名称和类型 是否参与搜索要分析业务来判断 ,例如图片地址 ,就无需参与搜索 是否分词呢要看内容 ,内容如果是一个整体就无需分词 ,反之则要分词 分词器 ,我们可以统一使用ik_max_word来看下酒店数据的索引库结构:
PUT /hotel { "mappings": { "properties": { "id": { "type": "keyword" }, "name":{ "type": "text", "analyzer": "ik_max_word", "copy_to": "all" }, "address":{ "type": "keyword", "index": false }, "price":{ "type": "integer" }, "score":{ "type": "integer" }, "brand":{ "type": "keyword", "copy_to": "all" }, "city":{ "type": "keyword", "copy_to": "all" }, "starName":{ "type": "keyword" }, "business":{ "type": "keyword" }, "location":{ "type": "geo_point" }, "pic":{ "type": "keyword", "index": false }, "all":{ "type": "text", "analyzer": "ik_max_word" } } } }几个特殊字段说明:
location:地理坐标 ,里面包含精度、纬度 all:一个组合字段 ,其目的是将多字段的值 利用copy_to合并 ,提供给用户搜索地理坐标说明:
copy_to说明:
2.初始化RestClient在elasticsearch提供的API中 ,与elasticsearch一切交互都封装在一个名为RestHighLevelClient的类中 ,必须先完成这个对象的初始化,建立与elasticsearch的连接。
分为三步:
1)引入es的RestHighLevelClient依赖:
<dependency> <groupId>org.elasticsearch.client</groupId> <artifactId>elasticsearch-rest-high-level-client</artifactId> </dependency>2)因为SpringBoot默认的ES版本是7.6.2 ,所以我们需要覆盖默认的ES版本:
<properties> <java.version>1.8</java.version> <elasticsearch.version>7.12.1</elasticsearch.version> </properties>3)初始化RestHighLevelClient:这里一般在启动类或者配置类里注入该Bean ,用于告诉Java 访问ES的ip地址
初始化的代码如下:
@Bean public RestHighLevelClient client(){ return new RestHighLevelClient(RestClient.builder( HttpHost.create("http://192.168.150.101:9200") )); }这里为了单元测试方便,我们创建一个测试类HotelIndexTest ,然后将初始化的代码编写在@BeforeEach方法中:
package cn.itcast.hotel; import org.apache.http.HttpHost; import org.elasticsearch.client.RestHighLevelClient; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import java.io.IOException; public class HotelIndexTest { private RestHighLevelClient client; @BeforeEach void setUp() { this.client = new RestHighLevelClient(RestClient.builder( HttpHost.create("http://192.168.150.101:9200") )); } @AfterEach void tearDown() throws IOException { this.client.close(); } } 3. 索引库CRUD 3.1 创建索引库代码分为三步:
1)创建Request对象 。因为是创建索引库的操作 ,因此Request是CreateIndexRequest 。 2)添加请求参数 ,其实就是DSL的JSON参数部分 。因为json字符串很长 ,这里是定义了静态字符串常量MAPPING_TEMPLATE ,让代码看起来更加优雅 。 3)发送请求 ,client.indices()方法的返回值是IndicesClient类型 ,封装了所有与索引库操作有关的方法 。创建索引库的API如下:
代码:
在hotel-demo的cn.itcast.hotel.constants包下 ,创建一个类 ,定义mapping映射的JSON字符串常量:
package cn.itcast.hotel.constants; public class HotelConstants { public static final String MAPPING_TEMPLATE = "{\n" + " \"mappings\": {\n" + " \"properties\": {\n" + " \"id\": {\n" + " \"type\": \"keyword\"\n" + " },\n" + " \"name\":{\n" + " \"type\": \"text\",\n" + " \"analyzer\": \"ik_max_word\",\n" + " \"copy_to\": \"all\"\n" + " },\n" + " \"address\":{\n" + " \"type\": \"keyword\",\n" + " \"index\": false\n" + " },\n" + " \"price\":{\n" + " \"type\": \"integer\"\n" + " },\n" + " \"score\":{\n" + " \"type\": \"integer\"\n" + " },\n" + " \"brand\":{\n" + " \"type\": \"keyword\",\n" + " \"copy_to\": \"all\"\n" + " },\n" + " \"city\":{\n" + " \"type\": \"keyword\",\n" + " \"copy_to\": \"all\"\n" + " },\n" + " \"starName\":{\n" + " \"type\": \"keyword\"\n" + " },\n" + " \"business\":{\n" + " \"type\": \"keyword\"\n" + " },\n" + " \"location\":{\n" + " \"type\": \"geo_point\"\n" + " },\n" + " \"pic\":{\n" + " \"type\": \"keyword\",\n" + " \"index\": false\n" + " },\n" + " \"all\":{\n" + " \"type\": \"text\",\n" + " \"analyzer\": \"ik_max_word\"\n" + " }\n" + " }\n" + " }\n" + "}"; }在hotel-demo中的HotelIndexTest测试类中 ,编写单元测试 ,实现创建索引:
@Test void createHotelIndex() throws IOException { // 1.创建Request对象 CreateIndexRequest request = new CreateIndexRequest("hotel"); // 2.准备请求的参数:DSL语句 request.source(MAPPING_TEMPLATE, XContentType.JSON); // 3.发送请求 client.indices().create(request, RequestOptions.DEFAULT); } 3.2 删除索引库三步走:
1)创建Request对象 。这次是DeleteIndexRequest对象 2)准备参数 。这里是无参 3)发送请求 。改用delete方法删除索引库的DSL语句非常简单:
DELETE /hotel在hotel-demo中的HotelIndexTest测试类中 ,编写单元测试,实现删除索引:
@Test void testDeleteHotelIndex() throws IOException { // 1.创建Request对象 DeleteIndexRequest request = new DeleteIndexRequest("hotel"); // 2.发送请求 client.indices().delete(request, RequestOptions.DEFAULT); } 3.3 查询索引库三步走:
1)创建Request对象 。这次是GetIndexRequest对象 2)准备参数 。这里是无参 3)发送请求 。改用exists方法判断索引库是否存在 ,本质就是查询 ,对应的DSL是:
GET /hotel @Test void testExistsHotelIndex() throws IOException { // 1.创建Request对象 GetIndexRequest request = new GetIndexRequest("hotel"); // 2.发送请求 boolean exists = client.indices().exists(request, RequestOptions.DEFAULT); // 3.输出 System.err.println(exists ? "索引库已经存在!" : "索引库不存在!"); }API操作文档
这里更多的是先读取Mysql中的数据,然后再存进ES中 。
文档操作的基本步骤:【可以根据发送请求那步的第一个参数 ,发过来判断需要创建什么XXXXRequest】
初始化RestHighLevelClient 创建XxxRequest。XXX是Index 、Get 、Update 、Delete 、Bulk 准备参数(Index 、Update 、Bulk时需要) 发送请求 。调用RestHighLevelClient#.xxx()方法 ,xxx是index 、get 、update 、delete 、bulk 解析结果(Get时需要) 1. 初始化RestClient在elasticsearch提供的API中 ,与elasticsearch一切交互都封装在一个名为RestHighLevelClient的类中 ,必须先完成这个对象的初始化 ,建立与elasticsearch的连接 。
分为三步:
1)引入es的RestHighLevelClient依赖:
<dependency> <groupId>org.elasticsearch.client</groupId> <artifactId>elasticsearch-rest-high-level-client</artifactId> </dependency>2)因为SpringBoot默认的ES版本是7.6.2 ,所以我们需要覆盖默认的ES版本:
<properties> <java.version>1.8</java.version> <elasticsearch.version>7.12.1</elasticsearch.version> </properties>3)初始化RestHighLevelClient:这里一般写在最前面 ,用于告诉Java 访问ES的ip地址
初始化的代码如下:
RestHighLevelClient client = new RestHighLevelClient(RestClient.builder( HttpHost.create("http://192.168.150.101:9200") ));这里为了单元测试方便 ,我们创建一个测试类HotelIndexTest ,然后将初始化的代码编写在@BeforeEach方法中:
package cn.itcast.hotel; import org.apache.http.HttpHost; import org.elasticsearch.client.RestHighLevelClient; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import java.io.IOException; public class HotelIndexTest { private RestHighLevelClient client; @BeforeEach void setUp() { this.client = new RestHighLevelClient(RestClient.builder( HttpHost.create("http://192.168.150.101:9200") )); } @AfterEach void tearDown() throws IOException { this.client.close(); } } 2. 文档CRUD 2.0 批量导入文档三步走:
1)创建Request对象。这里是BulkRequest 2)准备参数 。批处理的参数 ,就是其它Request对象 ,这里就是多个IndexRequest 3)发起请求 。这里是批处理 ,调用的方法为client.bulk()方法案例需求:利用BulkRequest批量将数据库数据导入到索引库中 。
步骤如下:
利用mybatis-plus查询酒店数据
将查询到的酒店数据(Hotel)转换为文档类型数据(HotelDoc)
利用JavaRestClient中的BulkRequest批处理,实现批量新增文档
语法说明:
批量处理BulkRequest ,其本质就是将多个普通的CRUD请求组合在一起发送 。
其中提供了一个add方法 ,用来添加其他请求:
可以看到,能添加的请求包括:
IndexRequest ,也就是新增 UpdateRequest ,也就是修改 DeleteRequest ,也就是删除因此Bulk中添加了多个IndexRequest ,就是批量新增功能了 。示例:
我们在导入酒店数据时 ,将上述代码改造成for循环处理即可 。
在hotel-demo的HotelDocumentTest测试类中 ,编写单元测试:
@Test void testBulkRequest() throws IOException { // 批量查询酒店数据 List<Hotel> hotels = hotelService.list(); // 1.创建Request BulkRequest request = new BulkRequest(); // 2.准备参数 ,添加多个新增的Request for (Hotel hotel : hotels) { // 2.1.转换为文档类型HotelDoc HotelDoc hotelDoc = new HotelDoc(hotel); // 2.2.创建新增文档的Request对象 request.add(new IndexRequest("hotel") .id(hotelDoc.getId().toString()) .source(JSON.toJSONString(hotelDoc), XContentType.JSON)); } // 3.发送请求 client.bulk(request, RequestOptions.DEFAULT); } 2.1 批量新增文档四步走:
0)创建索引库实体类 1)创建Request对象 2)准备请求参数 ,也就是DSL中的JSON文档 3)发送请求 (注意:这里直接使用client.xxx()的API ,不再需要client.indices()了)我们要将数据库的酒店数据查询出来 ,写入elasticsearch中 。
1)创建索引库实体类
一般实体类里包含经纬度都需要创建一个新的实体类 ,将经纬度拼成一个字段
数据库查询后的结果是一个Hotel类型的对象 。结构如下:
@Data @TableName("tb_hotel") public class Hotel { @TableId(type = IdType.INPUT) private Long id; private String name; private String address; private Integer price; private Integer score; private String brand; private String city; private String starName; private String business; private String longitude; private String latitude; private String pic; }与我们的索引库结构存在差异:
longitude和latitude需要合并为location因此 ,我们需要定义一个新的类型,与索引库结构吻合:
package cn.itcast.hotel.pojo; import lombok.Data; import lombok.NoArgsConstructor; @Data @NoArgsConstructor public class HotelDoc { private Long id; private String name; private String address; private Integer price; private Integer score; private String brand; private String city; private String starName; private String business; private String location; private String pic; public HotelDoc(Hotel hotel) { this.id = hotel.getId(); this.name = hotel.getName(); this.address = hotel.getAddress(); this.price = hotel.getPrice(); this.score = hotel.getScore(); this.brand = hotel.getBrand(); this.city = hotel.getCity(); this.starName = hotel.getStarName(); this.business = hotel.getBusiness(); this.location = hotel.getLatitude() + ", " + hotel.getLongitude(); this.pic = hotel.getPic(); } }2)新增代码
新增文档的DSL语句如下:
POST /{索引库名}/_doc/1 { "name": "Jack", "age": 21 }对应的java代码如图:
我们导入酒店数据 ,基本流程一致 ,但是需要考虑几点变化:
酒店数据来自于数据库,我们需要先查询出来 ,得到hotel对象 hotel对象需要转为HotelDoc对象 HotelDoc需要序列化为json格式在hotel-demo的HotelDocumentTest测试类中 ,编写单元测试:
@Test void testAddDocument() throws IOException { // 批量查询酒店数据 List<Hotel> hotels = hotelService.list(); // 1.创建Request BulkRequest request = new BulkRequest(); // 2.准备参数 ,添加多个新增的Request for (Hotel hotel : hotels) { // 2.1.转换为文档类型HotelDoc HotelDoc hotelDoc = new HotelDoc(hotel); // 2.2.创建新增文档的Request对象 request.add(new IndexRequest("hotel") .id(hotelDoc.getId().toString()) .source(JSON.toJSONString(hotelDoc), XContentType.JSON));//实体类转JSON ,指定JSON格式 request.add(new IndexRequest("xxx")...) } // 3.发送请求 client.bulk(request, RequestOptions.DEFAULT); } 2.2 查询文档查询文档是根据id查询的 ,所以没有批量查询
三步走:
1)准备Request对象 。这次是查询 ,所以是GetRequest 2)发送请求 ,得到结果 。因为是查询 ,这里调用client.get()方法 3)解析结果 ,就是对JSON做反序列化查询的DSL语句如下:
GET /hotel/_doc/{id}非常简单 ,因此代码大概分两步:
准备Request对象 发送请求不过查询的目的是得到结果 ,解析为HotelDoc ,因此难点是结果的解析 。完整代码如下:
可以看到,结果是一个JSON ,其中文档放在一个_source属性中 ,因此解析就是拿到_source,使用工具反序列化为Java对象即可 。
在hotel-demo的HotelDocumentTest测试类中 ,编写单元测试:
@Test void testGetDocumentById() throws IOException { // 1.准备Request GetRequest request = new GetRequest("hotel", "61082"); // 2.发送请求 ,得到响应 GetResponse response = client.get(request, RequestOptions.DEFAULT); // 3.解析响应结果 String json = response.getSourceAsString(); HotelDoc hotelDoc = JSON.parseObject(json, HotelDoc.class); System.out.println(hotelDoc); } 2.3 批量删除文档三步走:
1)准备Request对象 ,因为是删除 ,这次是DeleteRequest对象。要指定索引库名和id 2)准备参数 ,无参 3)发送请求 。因为是删除 ,所以是client.delete()方法删除的DSL为是这样的:
DELETE /hotel/_doc/{id}在hotel-demo的HotelDocumentTest测试类中 ,编写单元测试:
@Test void testDeleteDocument() throws IOException { //0.查询数据库中的数据 List<Hotel> list = hotelService.list(); // 1.创建Request BulkRequest request = new BulkRequest(); //2.批量转换实体类 ,顺便写入到ES中 for (Hotel hotel : list) { //2.1转换实体类 HotelDoc hotelDoc =new HotelDoc(hotel); //2.2写入ES request.add(new DeleteRequest("hotel") .id(hotel.getId().toString())); } //3.发送请求 client.bulk(request,RequestOptions.DEFAULT); } 2.4 批量修改文档三步走:
1)准备Request对象 。这次是修改 ,所以是UpdateRequest 2)准备参数。也就是JSON文档 ,里面包含要修改的字段 3)更新文档 。这里调用client.update()方法修改有两种方式:
全量修改:本质是先根据id删除 ,再新增 增量修改:修改文档中的指定字段值在RestClient的API中 ,全量修改与新增的API完全一致,判断依据是ID:
如果新增时 ,ID已经存在 ,则修改 如果新增时,ID不存在 ,则新增只演示增量修改:
代码示例如图:
在hotel-demo的HotelDocumentTest测试类中 ,编写单元测试:
@Test void创心域SEO版权声明:以上内容作者已申请原创保护,未经允许不得转载,侵权必究!授权事宜、对本内容有异议或投诉,敬请联系网站管理员,我们将尽快回复您,谢谢合作!