# 初识 elasticsearch
# 了解 ES
# elasticsearch 作用
elasticsearch 是一款非常强大的开源搜索引擎,具备非常多强大功能,可以帮助我们从海量数据中快速找到需要的内容
# ELK 技术栈
elasticsearch 结合 kibana、Logstash、Beats,也就是 elastic stack(ELK)。被广泛应用在日志数据分析、实时监控等领域:
而 elasticsearch 是 elastic stack 的核心,负责存储、搜索、分析数据。
# 倒排索引
倒排索引的概念是基于 MySQL 这样的正向索引而言的
# 正向索引
逐行扫描,也就是全表扫描,随着数据量增加,其查询效率也会越来越低。当数据量达到数百万时,就是一场灾难
# 倒排索引
# 两个重要概念
- 文档(
Document
):用来搜索的数据,其中的每一条数据就是一个文档。例如一个网页、一个商品信息 - 词条(
Term
):对文档数据或用户搜索数据,利用某种算法分词,得到的具备含义的词语就是词条。例如:我是中国人,就可以分为:我、是、中国人、中国、国人这样的几个词条
# 创建倒排索引的流程
- 将每一个文档利用算法分词,得到一个词条
- 创建表,每行数据包括词条、词条所在文档 id、位置等信息
- 因为词条唯一性,可以给词条创建索引,例如 hash 表结构索引
# 倒排索引的搜索流程
- 用户输入条件
"华为手机"
进行搜索 - 对用户输入内容分词,得到词条:
华为
、手机
- 拿着词条在倒排索引中查找,可以得到包含词条的文档 id:1、2、3
- 拿着文档 id 到正向索引中查找具体文档
虽然要先查询倒排索引,再查询倒排索引,但是无论是词条、还是文档 id 都建立了索引,查询速度非常快!无需全表扫描
# 正向和倒排
# 介绍
- 正向索引是最传统的,根据 id 索引的方式。但根据词条查询时,必须先逐条获取每个文档,然后判断文档中是否包含所需要的词条,是根据文档找词条的过程
- 而倒排索引则相反,是先找到用户要搜索的词条,根据词条得到保护词条的文档的 id,然后根据 id 获取文档。是根据词条找文档的过程
# 正向索引优劣
- 优点:
- 可以给多个字段创建索引
- 根据索引字段搜索、排序速度非常快
- 缺点:
- 根据非索引字段,或者索引字段中的部分词条查找时,只能全表扫描
# 倒排索引优劣
- 优点:
- 根据词条搜索、模糊搜索时,速度非常快
- 缺点:
- 能给词条创建索引,而不是字段
- 无法根据字段做排序
# ES 的基本概念
# 文档和字段
elasticsearch 是面向 ** 文档(Document)** 存储的,可以是数据库中的一条商品数据,一个订单信息。文档数据会被序列化为 json 格式后存储在 elasticsearch 中
而 Json 文档中往往包含很多的字段(Field),类似于数据库中的列
# 索引和映射
索引(Index),就是相同类型的文档的集合
- 所有用户文档,就可以组织在一起,称为用户的索引;
- 所有商品的文档,可以组织在一起,称为商品的索引;
- 所有订单的文档,可以组织在一起,称为订单的索引;
索引相当于数据库中的表
数据库的表会有约束信息,用来定义表的结构、字段的名称、类型等信息。因此,索引库中就有映射(mapping),是索引中文档的字段约束信息,类似表的结构约束
# 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:擅长海量数据的搜索、分析、计算
# 安装 ES、kibana
# 部署单点 ES
# 创建网站
因为我们还需要部署 kibana 容器,因此需要让 es 和 kibana 容器互联。这里先创建一个网络
docker network create es-net |
# 加载镜像与运行
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.150.101:9200 即可看到 elasticsearch 的响应结果
# 部署 kibana
kibana 可以给我们提供一个 elasticsearch 的可视化界面,便于我们学习
# 部署
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
:端口映射配置
http://192.168.150.101:5601,即可看到结果
# DevTools
这个界面中可以编写 DSL 来操作 elasticsearch。并且对 DSL 语句有自动补全功能
# 安装 ik 分词器
# 在线安装 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 |
# 离线安装 ik 分词器插件
安装插件需要知道 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
这个目录中
解压缩分词器安装包
下面我们需要把课前资料中的 ik 分词器解压缩,重命名为 ik
上传到 es 容器的插件数据卷中
也就是 /var/lib/docker/volumes/es-plugins/_data
:
重启容器
# 4、重启容器 | |
docker restart es |
# 查看 es 日志 | |
docker logs -f es |
测试
ik_smart
:最少切分ik_max_word
:最细切分
# 扩展词词典
随着互联网的发展,“造词运动” 也越发的频繁。出现了很多新的词语
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 记事本编辑
# 停用词词典
在互联网项目中,在网络间传输的速度很快,所以很多语言是不允许在网络上传递的,如:关于宗教、政治等敏感词语,那么我们在搜索时也应该忽略当前词汇。
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> |
2)在 stopword.dic 添加停用词
习大大 |
3)重启 elasticsearch
# 重启服务 | |
docker restart elasticsearch | |
docker restart kibana | |
# 查看 日志 | |
docker logs -f elasticsearch |
日志中已经成功加载 stopword.dic 配置文件
4)测试效果:
GET /_analyze | |
{ | |
"analyzer": "ik_max_word", | |
"text": "传智播客Java就业率超过95%,习大大都点赞,奥力给!" | |
} |
注意当前文件的编码必须是 UTF-8 格式,严禁使用 Windows 记事本编辑
# 索引库操作
# mapping 映射属性
mapping 是对索引库中文档的约束,常见的 mapping 属性包括:
- type:字段数据类型,常见的简单类型有:
- 字符串:text(可分词的文本)、keyword(精确值,例如:品牌、国家、ip 地址)
- 数值:long、integer、short、byte、double、float、
- 布尔:boolean
- 日期:date
- 对象:object
- index:是否创建索引,默认为 true
- analyzer:使用哪种分词器
- properties:该字段的子字段
# 索引库的 CRUD
# 创建索引库和映射
# 基本语法
- 请求方式:PUT
- 请求路径:/ 索引库名,可以自定义
- 请求参数:mapping 映射
# 格式
PUT /索引库名称 | |
{ | |
"mappings": { | |
"properties": { | |
"字段名":{ | |
"type": "text", | |
"analyzer": "ik_smart" | |
}, | |
"字段名2":{ | |
"type": "keyword", | |
"index": "false" | |
}, | |
"字段名3":{ | |
"properties": { | |
"子字段": { | |
"type": "keyword" | |
} | |
} | |
}, | |
//... 略 | |
} | |
} | |
} |
# 示例
PUT /baozi | |
{ | |
"mappings": { | |
"properties": { | |
"info":{ | |
"type": "text", | |
"analyzer": "ik_smart" | |
}, | |
"email":{ | |
"type": "keyword", | |
"index": "falsae" | |
}, | |
"name":{ | |
"properties": { | |
"firstName": { | |
"type": "keyword" | |
} | |
} | |
}, | |
//... 略 | |
} | |
} | |
} |
# 查询索引库
# 基本语法
- 请求方式:GET
- 请求路径:/ 索引库名
- 请求参数:无
# 格式
GET /索引库名
# 示例
GET /baozi
# 修改索引库
倒排索引结构虽然不复杂,但是一旦数据结构改变(比如改变了分词器),就需要重新创建倒排索引,这简直是灾难。因此索引库一旦创建,无法修改 mapping
虽然无法修改 mapping 中已有的字段,但是却允许添加新的字段到 mapping 中,因为不会对倒排索引产生影响
# 语法说明
PUT /索引库名/_mapping | |
{ | |
"properties": { | |
"新字段名":{ | |
"type": "integer" | |
} | |
} | |
} |
# 示例
PUT /baozi/_mapping | |
{ | |
"properties": { | |
"age": { | |
"type": "integer" | |
} | |
} | |
} |
# 删除索引库
# 语法
- 请求方式:DELETE
- 请求路径:/ 索引库名
- 请求参数:无
# 格式
DELETE /索引库名字
# 示例
DELETE /baozi
# 文档操作
# 新增文档
# 语法
POST /索引库名/_doc/文档id | |
{ | |
"字段1": "值1", | |
"字段2": "值2", | |
"字段3": { | |
"子属性1": "值3", | |
"子属性2": "值4" | |
}, | |
// ... | |
} |
# 示例
POST /baozi/_doc/1 | |
{ | |
"info": "包子", | |
"email": "zy@itcast.cn", | |
"name": { | |
"firstName": "云", | |
"lastName": "赵" | |
} | |
} |
# 相应
# 查询文档
根据 REST 风格,新增是 post,查询应该是 get,不过查询一般都需要条件,这里我们把文档 id 带上
# 语法
GET /{索引库名称}/_doc/{id}
# 示例
GET /baozi/_doc/1
# 查看结果
# 删除文档
删除使用 DELETE 请求,同样,需要根据 id 进行删除
# 语法
DELETE /{索引库名}/_doc/id值
# 示例
# 根据id删除数据
DELETE /baozi/_doc/1
# 结果
# 修改文档
# 全量修改
# 介绍
全量修改是覆盖原来的文档,其本质是:
- 根据指定的 id 删除文档
- 新增一个相同 id 的文档
注意:如果根据 id 删除时,id 不存在,第二步的新增也会执行,也就从修改变成了新增操作了。
# 语法
PUT /{索引库名}/_doc/文档id | |
{ | |
"字段1": "值1", | |
"字段2": "值2", | |
//... 略 | |
} |
# 示例
PUT /baozi/_doc/1 | |
{ | |
"info": "包子", | |
"email": "zy@itcast.cn", | |
"name": { | |
"firstName": "云", | |
"lastName": "赵" | |
} | |
} |
# 增量修改
增量修改是只修改指定 id 匹配的文档中的部分字段
# 语法
POST /{索引库名}/_update/文档id | |
{ | |
"doc": { | |
"字段名": "新的值", | |
} | |
} |
# 示例
POST /baozi/_update/1 | |
{ | |
"doc": { | |
"email": "ZhaoYun@itcast.cn" | |
} | |
} |
# RestAPI
# 初始化 RestClient
在 elasticsearch 提供的 API 中,与 elasticsearch 一切交互都封装在一个名为 RestHighLevelClient 的类中,必须先完成这个对象的初始化,建立与 elasticsearch 的连接
# 引入 ES 的 RestHightClient 依赖
<dependency> | |
<groupId>org.elasticsearch.client</groupId> | |
<artifactId>elasticsearch-rest-high-level-client</artifactId> | |
</dependency> |
# 覆盖 ES 版本 7.6.2
<properties> | |
<java.version>1.8</java.version> | |
<elasticsearch.version>7.12.1</elasticsearch.version> | |
</properties> |
# 初始化 RestHighLevelClient
# 初始化代码
RestHighLevelClient client = new RestHighLevelClient(RestClient.builder( | |
HttpHost.create("http://192.168.150.101:9200") | |
)); |
# 编写测试用例
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(); | |
} | |
} |
# 创建索引库
# 代码解读
- 创建 Request 对象,因为创建索引库的操作,因此 Request 是 CreateIndexRequest
- 添加请求参数,其实就是 DSL 的 JSON 参数部分
- 发送请求,
client.indices()
方法的返回值就是IndicesClient
类型,封装了所有索引库操作有关的方法
# 完整案例
# 定义一个类,存储常量参数 JSON
在 hotel-demo 的 cn.itcast.hotel.constants 包下,创建一个类,定义 mapping 映射的 JSON 字符串常量
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" + | |
"}"; | |
} |
# 单元测试
@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); | |
} |
# 删除索引库
# 单元测试
@Test | |
void testDeleteHotelIndex() throws IOException { | |
// 1. 创建 Request 对象 | |
DeleteIndexRequest request = new DeleteIndexRequest("hotel"); | |
// 2. 发送请求 | |
client.indices().delete(request, RequestOptions.DEFAULT); | |
} |
# 判断索引库是否存在
# 单元测试
@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 ? "索引库已经存在!" : "索引库不存在!"); | |
} |
# RestClient 操作文档
# 新增文档
# 索引库实体类
# 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; | |
} |
# HotelDoc 实体
@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(); | |
} | |
} |
# 语法说明
# DSL 语句
POST /{索引库名}/_doc/1 | |
{ | |
"name": "Jack", | |
"age": 21 | |
} |
# Java 代码
# 查询文档
# 语法说明
# DSL 语句
GET /hotel/_doc/{id}
# Java 代码
# 完整代码
@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); | |
} |
# 删除文档
# 语法说明
# DSL 语句
DELETE /hotel/_doc/{id}
# Java 代码
@Test | |
void testDeleteDocument() throws IOException { | |
// 1. 准备 Request | |
DeleteRequest request = new DeleteRequest("hotel", "61083"); | |
// 2. 发送请求 | |
client.delete(request, RequestOptions.DEFAULT); | |
} |
# 修改文档
# 语法说明
# 增量修改 Java 代码
# 完整代码
@Test | |
void testUpdateDocument() throws IOException { | |
// 1. 准备 Request | |
UpdateRequest request = new UpdateRequest("hotel", "61083"); | |
// 2. 准备请求参数 | |
request.doc( | |
"price", "952", | |
"starName", "四钻" | |
); | |
// 3. 发送请求 | |
client.update(request, RequestOptions.DEFAULT); | |
} |
# 批量导入文档
# 语法说明
# Java 代码
# 完整代码
@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); | |
} |