elasticsearch_elasticsearch csdn
目录
1 认识
1.1 认识与安装
1.2 倒排索引
1.2.1 正向索引
1.2.2 倒排索引
1.2.3 正向索引 vs 倒排索引
1.3 基础概念
1.3.2 索引和映射
1.3.2 MySQL 与 Elasticsearch 对比
1.4 IK分词器
1.4.1 安装
1.4.2 使用IK分词器
1.4.3 拓展词典
1.4.4 总结
2 索引库操作
2.1 Mapping映射属性
2.2 索引库的CRUD
2.2.1 创建索引库和映射
2.2.2 查询索引库
2.2.3 修改索引库
2.2.4 删除索引库
2.2.5 总结
索引库操作
注意事项
3 文档操作
3.1 新增
3.2 查询文档
3.3 删除文档
3.4 修改文档
3.4.1 全量修改
3.4.2 局部修改
3.5 批处理
3.6 总结
4 RestAPI
4.1 初始化RestClient
4.2 创建索引库
4.2.1 Mapping映射
4.2.2 创建索引
4.3 删除索引库
4.4 判断索引库是否存在
4.5 总结
5 RestClient操作文档
5.1 新增文档
5.2 查询文档
5.3 删除文档
5.4 修改文档
5.5 批量导入文档
6 DSL查询
6.1 Elasticsearch查询分类
6.2 DSL查询基本结构
6.3 叶子查询
6.3.1 全文检索查询
6.3.2 精确查询
6.4 复合查询-bool查询
6.5 排序
6.7 分页
6.7.1 基础分页
6.7.2 深度分页
6.8 高亮
6.8.1 高亮原理
6.8.2 实现高亮
7 RestClient查询
7.1 查询步骤
7.2 叶子查询
7.3 复合查询-bool查询
7.4 排序
7.5 分页
7.6 高亮
8 数据聚合
8.1 聚合类型
(1)Bucket 聚合
(2)带条件聚合
(3)Metric聚合
8.2 聚合总结
1 认识
1.1 认识与安装
Elasticsearch是由elastic公司开发的一套搜索引擎技术,它是elastic技术栈中的一部分。完整的技术栈包括:
-
Elasticsearch:用于数据存储、计算和搜索
-
Logstash/Beats:用于数据收集
-
Kibana:用于数据可视化
Elasticsearch 提供核心的数据存储、搜索、分析功能,而 Kibana 作为 Elasticsearch 的可视化控制台,支持数据搜索、展示、统计、聚合,并形成图形化报表,还能监控 Elasticsearch 集群状态,并提供开发控制台(DevTools)以支持 Elasticsearch 的 Restful API 接口。
(1)安装 Elasticsearch 可以通过 Docker 命令实现单机版本的部署:
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 hm-net \\ -p 9200:9200 \\ -p 9300:9300 \\ elasticsearch:7.12.1
这个命令使用了 Elasticsearch 的 7.12.1 版本,因为企业中应用较多的是 8 以下的版本。
(2)安装 Kibana 同样可以通过 Docker 命令完成:
docker run -d \\--name kibana \\-e ELASTICSEARCH_HOSTS=http://es:9200 \\--network=hm-net \\-p 5601:5601 \\kibana:7.12.1
安装完成后,可以通过访问 5601 端口看到控制台页面,选择 \"Explore on my own\" 进入主页面,然后选中 Dev tools 进入开发工具页面。
1.2 倒排索引
倒排索引是Elasticsearch实现高效搜索的核心技术之一,与传统的正向索引不同,它专为快速全文搜索设计。
倒排索引的概念是基于MySQL这样的正向索引而言的。
1.2.1 正向索引
-
定义:传统的索引方式,基于文档ID进行索引。
-
特点:
-
适合精确匹配(如根据ID查询)。
-
对于模糊匹配(如
like \'%手机%\'
),需要全表扫描,效率低。
-
-
流程:
-
检查搜索条件(如
like \'%手机%\'
)。 -
逐条遍历每行数据(叶子节点)。
-
判断数据是否符合条件。
-
符合则放入结果集,否则丢弃。
-
重复上述步骤。
-
1.2.2 倒排索引
-
定义:基于词条的索引方式,解决模糊匹配问题。
-
核心概念:
-
文档(Document):搜索的数据单元(如一条商品信息)。
-
词条(Term):对文档内容分词后得到的词语(如“小米”、“手机”)。
-
-
创建流程:
-
对文档内容分词,得到词条。
-
创建倒排索引表,记录词条、文档ID、位置等信息。
-
为词条创建正向索引。
-
-
搜索流程(以“华为手机”为例):
-
对搜索内容分词,得到词条(“华为”、“手机”)。
-
在倒排索引中查找词条,得到包含词条的文档ID(1, 2, 3)。
-
根据文档ID在正向索引中查找具体文档。
-
-
优点:
-
模糊搜索速度快。
-
无需全表扫描。
-
-
缺点:
-
只能对词条创建索引,无法对字段排序。
-
1.2.3 正向索引 vs 倒排索引
-
正向索引是最传统的,根据id索引的方式。但根据词条查询时,必须先逐条获取每个文档,然后判断文档中是否包含所需要的词条,是根据文档找词条的过程。
-
优点:
-
可以给多个字段创建索引
-
根据索引字段搜索、排序速度非常快
-
-
缺点:
-
根据非索引字段,或者索引字段中的部分词条查找时,只能全表扫描。
-
-
-
而倒排索引则相反,是先找到用户要搜索的词条,根据词条得到保护词条的文档的id,然后根据id获取文档。是根据词条找文档的过程。
-
优点:
-
根据词条搜索、模糊搜索时,速度非常快
-
-
缺点:
-
只能给词条创建索引,而不是字段
-
无法根据字段做排序
-
-
1.3 基础概念
1.3.1 文档和字段
文档(Document):ES中的基本数据单元,存储为JSON格式。
示例:
{ \"id\": 1, \"title\": \"小米手机\", \"price\": 3499}
字段(Field):文档中的属性(如title
、price
)。
1.3.2 索引和映射
索引(Index):类似数据库中的表,用于存储相同类型的文档。
-
示例:
-
商品索引:存储所有商品文档。
-
用户索引:存储所有用户文档。
-
映射(Mapping):定义索引中文档的字段类型和约束,类似数据库的表结构。
1.3.2 MySQL 与 Elasticsearch 对比
DSL是elasticsearch提供的JSON风格的请求语句,用来操作elasticsearch,实现CRUD
-
MySQL:
-
适合事务操作,确保数据安全和一致性。
-
-
Elasticsearch:
-
适合海量数据的搜索、分析和计算。
-
-
结合使用:
-
对安全性要求较高的写操作,使用mysql实现
-
对查询性能要求较高的搜索需求,使用elasticsearch实现
-
1.4 IK分词器
Elasticsearch的关键就是倒排索引,而倒排索引依赖于对文档内容的分词,而分词则需要高效、精准的分词算法,IK分词器就是这样一个中文分词算法。
1.4.1 安装
(1)在线安装
docker exec -it es ./bin/elasticsearch-plugin install https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v7.12.1/elasticsearch-analysis-ik-7.12.1.zipdocker restart es
(2)离线安装
-
解压 IK 分词器插件。
-
上传至 Elasticsearch 的插件目录(如
/var/lib/docker/volumes/es-plugins/_data
)。 -
重启 Elasticsearch。
1.4.2 使用IK分词器
IK分词器包含两种模式:
-
ik_smart
:智能语义切分 -
ik_max_word
:最细粒度切分
POST /_analyze{ \"analyzer\": \"ik_smart\", \"text\": \"黑马程序员学习java太棒了\"}
{ \"tokens\": [ {\"token\": \"黑马\", \"type\": \"CN_WORD\"}, {\"token\": \"程序员\", \"type\": \"CN_WORD\"}, {\"token\": \"学习\", \"type\": \"CN_WORD\"}, {\"token\": \"java\", \"type\": \"ENGLISH\"}, {\"token\": \"太棒了\", \"type\": \"CN_WORD\"} ]}
1.4.3 拓展词典
要想正确分词,IK分词器的词库也需要不断的更新,IK分词器提供了扩展词汇的功能。
1.4.4 总结
分词器的作用是什么?
-
创建倒排索引时,对文档分词
-
用户搜索时,对输入的内容分词
IK分词器有几种模式?
-
ik_smart
:智能切分,粗粒度 -
ik_max_word
:最细切分,细粒度
IK分词器如何拓展词条?如何停用词条?
-
利用config目录的
IkAnalyzer.cfg.xml
文件添加拓展词典和停用词典 -
在词典中添加拓展词条或者停用词条
2 索引库操作
2.1 Mapping映射属性
-
Mapping 是对索引库中文档的约束,类似于数据库中的表结构。
-
它定义了字段的类型、是否参与搜索、是否分词等信息。
text
(可分词的文本)、keyword
(精确值,如品牌、IP地址)long
、integer
、short
、byte
、double
、float
boolean
date
object
true
。ik_smart
)。例如:
age
integer
weight
float
isMarried
boolean
info
text
email
keyword
score
float
float
)name.firstName
keyword
name.lastName
keyword
2.2 索引库的CRUD
2.2.1 创建索引库和映射
PUT /索引库名{ \"mappings\": { \"properties\": { \"字段名\": { \"type\": \"字段类型\", \"analyzer\": \"分词器\" // 可选 }, \"字段名2\": { \"type\": \"字段类型\", \"index\": \"是否创建索引\" // 可选,默认为 true }, \"字段名3\": { \"properties\": { \"子字段\": { \"type\": \"字段类型\" } } } } }}
PUT /heima{ \"mappings\": { \"properties\": { \"info\": { \"type\": \"text\", \"analyzer\": \"ik_smart\" }, \"email\": { \"type\": \"keyword\", \"index\": \"false\" }, \"name\": { \"properties\": { \"firstName\": { \"type\": \"keyword\" } } } } }}
2.2.2 查询索引库
GET /索引库名
2.2.3 修改索引库
-
引库一旦创建,无法修改已有字段的 Mapping。
-
但可以添加新字段,因为不会影响倒排索引。
PUT /索引库名/_mapping{ \"properties\": { \"新字段名\": { \"type\": \"字段类型\" } }}
PUT /heima/_mapping{ \"properties\": { \"age\": { \"type\": \"integer\" } }}
2.2.4 删除索引库
DELETE /索引库名
2.2.5 总结
索引库操作
PUT
/索引库名
GET
/索引库名
PUT
/索引库名/_mapping
DELETE
/索引库名
注意事项
-
Mapping 不可修改:一旦创建,无法修改已有字段的 Mapping。
-
添加新字段:可以通过修改操作添加新字段。
-
Restful 风格:Elasticsearch 的 API 遵循 Restful 风格,操作统一且易于记忆。
3 文档操作
3.1 新增
POST /索引库名/_doc/文档id{ \"字段1\": \"值1\", \"字段2\": \"值2\", \"字段3\": { \"子属性1\": \"值3\", \"子属性2\": \"值4\" },}
POST /heima/_doc/1{ \"info\": \"mikey\", \"email\": \"zy@itcast.cn\", \"name\": { \"firstName\": \"云\", \"lastName\": \"赵\" }}
3.2 查询文档
-
语法:
GET /{索引库名称}/_doc/{id}
-
示例:
GET /heima/_doc/1
3.3 删除文档
-
语法:
DELETE /{索引库名}/_doc/id值
-
示例:
DELETE /heima/_doc/1
3.4 修改文档
3.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\": \"赵\" }}
3.4.2 局部修改
局部修改是只修改指定id匹配的文档中的部分字段。
POST /{索引库名}/_update/文档id{ \"doc\": { \"字段名\": \"新的值\", }}
POST /heima/_update/1{ \"doc\": { \"email\": \"ZhaoYun@itcast.cn\" }}
3.5 批处理
批处理采用POST请求,可以同时执行多个操作(新增、删除、更新)。
POST /_bulk{ \"index\" : { \"_index\" : \"test\", \"_id\" : \"1\" } }{ \"field1\" : \"value1\" }{ \"delete\" : { \"_index\" : \"test\", \"_id\" : \"2\" } }{ \"create\" : { \"_index\" : \"test\", \"_id\" : \"3\" } }{ \"field1\" : \"value3\" }{ \"update\" : {\"_id\" : \"1\", \"_index\" : \"test\"} }{ \"doc\" : {\"field2\" : \"value2\"} }
-
index
代表新增操作-
_index
:指定索引库名 -
_id
指定要操作的文档id -
{ \"field1\" : \"value1\" }
:则是要新增的文档内容
-
-
delete
代表删除操作-
_index
:指定索引库名 -
_id
指定要操作的文档id
-
-
update
代表更新操作-
_index
:指定索引库名 -
_id
指定要操作的文档id -
{ \"doc\" : {\"field2\" : \"value2\"} }
:要更新的文档字段
-
批量新增 示例:
POST /_bulk{\"index\": {\"_index\":\"heima\", \"_id\": \"3\"}}{\"info\": \"黑马程序员C++讲师\", \"email\": \"ww@itcast.cn\", \"name\":{\"firstName\": \"五\", \"lastName\":\"王\"}}{\"index\": {\"_index\":\"heima\", \"_id\": \"4\"}}{\"info\": \"黑马程序员前端讲师\", \"email\": \"zhangsan@itcast.cn\", \"name\":{\"firstName\": \"三\", \"lastName\":\"张\"}}
批量删除 示例:
POST /_bulk{\"delete\":{\"_index\":\"heima\", \"_id\": \"3\"}}{\"delete\":{\"_index\":\"heima\", \"_id\": \"4\"}}
3.6 总结
-
创建文档:
POST /{索引库名}/_doc/文档id { json文档 }
-
查询文档:
GET /{索引库名}/_doc/文档id
-
删除文档:
DELETE /{索引库名}/_doc/文档id
-
修改文档:
-
全量修改:
PUT /{索引库名}/_doc/文档id { json文档 }
-
局部修改:
POST /{索引库名}/_update/文档id { \"doc\": {字段}}
-
4 RestAPI
Elasticsearch提供了RestAPI,允许通过HTTP请求与Elasticsearch集群进行交互。Elasticsearch官方提供了不同语言的客户端,这些客户端本质上是组装DSL语句并通过HTTP请求发送给Elasticsearch。
4.1 初始化RestClient
在Elasticsearch中,所有交互都封装在RestHighLevelClient
类中。必须先完成这个对象的初始化,建立与Elasticsearch的连接。
(1)引入依赖
org.elasticsearch.client elasticsearch-rest-high-level-client
(2)覆盖默认的ES版本(如果需要):
7.12.1
(3)初始化RestHighLevelClient:
package com.hmall.item.es;import org.apache.http.HttpHost;import org.elasticsearch.client.RestClient;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 IndexTest { private RestHighLevelClient client; @BeforeEach void setUp() { this.client = new RestHighLevelClient(RestClient.builder( HttpHost.create(\"http://192.168.150.101:9200\") )); } @Test void testConnect() { System.out.println(client); } @AfterEach void tearDown() throws IOException { this.client.close(); }}
4.2 创建索引库
4.2.1 Mapping映射
以商城为例
实现搜索功能需要的字段包括三大部分:
-
搜索过滤字段
-
分类
-
品牌
-
价格
-
-
排序字段
-
默认:按照更新时间降序排序
-
销量
-
价格
-
-
展示字段
-
商品id:用于点击后跳转
-
图片地址
-
是否是广告推广商品
-
名称
-
价格
-
评价数量
-
销量
-
对应的商品表结构如下,索引库无关字段已经划掉
以上字段对应的mapping映射属性如下
字段名
字段类型
类型说明
是否
参与搜索
id
long
长整数
是
name
text
字符串,参与分词搜索
是
price
integer
以分为单位,所以是整数
是
stock
integer
字符串,但需要分词
是
image
keyword
字符串,但是不分词
否
category
keyword
字符串,但是不分词
是
brand
keyword
字符串,但是不分词
是
sold
integer
销量,整数
是
commentCount
integer
评价,整数
否
isAD
boolean
布尔类型
是
updateTime
Date
更新时间
是
最终我们的索引库文档结构应该是这样:
PUT /items{ \"mappings\": { \"properties\": { \"id\": { \"type\": \"keyword\" }, \"name\":{ \"type\": \"text\", \"analyzer\": \"ik_max_word\" }, \"price\":{ \"type\": \"integer\" }, \"stock\":{ \"type\": \"integer\" }, \"image\":{ \"type\": \"keyword\", \"index\": false }, \"category\":{ \"type\": \"keyword\" }, \"brand\":{ \"type\": \"keyword\" }, \"sold\":{ \"type\": \"integer\" }, \"commentCount\":{ \"type\": \"integer\", \"index\": false }, \"isAD\":{ \"type\": \"boolean\" }, \"updateTime\":{ \"type\": \"date\" } } }}
4.2.2 创建索引
代码分为三步:
-
1)创建Request对象。
-
因为是创建索引库的操作,因此Request是
CreateIndexRequest
。
-
-
2)添加请求参数
-
其实就是Json格式的Mapping映射参数。因为json字符串很长,这里是定义了静态字符串常量
MAPPING_TEMPLATE
,让代码看起来更加优雅。
-
-
3)发送请求
-
client.
indices
()
方法的返回值是IndicesClient
类型,封装了所有与索引库操作有关的方法。例如创建索引、删除索引、判断索引是否存在等
-
具体代码如下:
@Testvoid testCreateIndex() throws IOException { // 1.创建Request对象 CreateIndexRequest request = new CreateIndexRequest(\"items\"); // 2.准备请求参数 request.source(MAPPING_TEMPLATE, XContentType.JSON); // 3.发送请求 client.indices().create(request, RequestOptions.DEFAULT);}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\" + \" },\\n\" + \" \\\"price\\\":{\\n\" + \" \\\"type\\\": \\\"integer\\\"\\n\" + \" },\\n\" + \" \\\"stock\\\":{\\n\" + \" \\\"type\\\": \\\"integer\\\"\\n\" + \" },\\n\" + \" \\\"image\\\":{\\n\" + \" \\\"type\\\": \\\"keyword\\\",\\n\" + \" \\\"index\\\": false\\n\" + \" },\\n\" + \" \\\"category\\\":{\\n\" + \" \\\"type\\\": \\\"keyword\\\"\\n\" + \" },\\n\" + \" \\\"brand\\\":{\\n\" + \" \\\"type\\\": \\\"keyword\\\"\\n\" + \" },\\n\" + \" \\\"sold\\\":{\\n\" + \" \\\"type\\\": \\\"integer\\\"\\n\" + \" },\\n\" + \" \\\"commentCount\\\":{\\n\" + \" \\\"type\\\": \\\"integer\\\"\\n\" + \" },\\n\" + \" \\\"isAD\\\":{\\n\" + \" \\\"type\\\": \\\"boolean\\\"\\n\" + \" },\\n\" + \" \\\"updateTime\\\":{\\n\" + \" \\\"type\\\": \\\"date\\\"\\n\" + \" }\\n\" + \" }\\n\" + \" }\\n\" + \"}\";
4.3 删除索引库
@Testvoid testDeleteIndex() throws IOException { // 1.创建Request对象 DeleteIndexRequest request = new DeleteIndexRequest(\"items\"); // 2.发送请求 client.indices().delete(request, RequestOptions.DEFAULT);}
4.4 判断索引库是否存在
@Testvoid testExistsIndex() throws IOException { GetIndexRequest request = new GetIndexRequest(\"items\"); boolean exists = client.indices().exists(request, RequestOptions.DEFAULT); System.err.println(exists ? \"索引库已经存在!\" : \"索引库不存在!\");}
4.5 总结
JavaRestClient操作elasticsearch的流程基本类似。核心是client.indices()
方法来获取索引库的操作对象。
索引库操作的基本步骤:
-
初始化
RestHighLevelClient
-
创建XxxIndexRequest。XXX是
Create
、Get
、Delete
-
准备请求参数(
Create
时需要,其它是无参,可以省略) -
发送请求。调用
RestHighLevelClient#indices().xxx()
方法,xxx是create
、exists
、delete
5 RestClient操作文档
索引库结构与数据库结构还存在一些差异,因此我们要定义一个索引库结构对应的实体。
5.1 新增文档
代码整体步骤如下:
-
1)根据id查询商品数据
Item
-
2)将
Item
封装为ItemDoc
-
3)将
ItemDoc
序列化为JSON -
4)创建IndexRequest,指定索引库名和id
-
5)准备请求参数,也就是JSON文档
-
6)发送请求
@Testvoid testAddDocument() throws IOException { // 1.根据id查询商品数据 Item item = itemService.getById(100002644680L); // 2.转换为文档类型 ItemDoc itemDoc = BeanUtil.copyProperties(item, ItemDoc.class); // 3.将ItemDTO转json String doc = JSONUtil.toJsonStr(itemDoc); // 1.准备Request对象 IndexRequest request = new IndexRequest(\"items\").id(itemDoc.getId()); // 2.准备Json文档 request.source(doc, XContentType.JSON); // 3.发送请求 client.index(request, RequestOptions.DEFAULT);}
可以看到与索引库操作的API非常类似,同样是三步走:
-
1)创建Request对象,这里是
IndexRequest
,因为添加文档就是创建倒排索引的过程 -
2)准备请求参数,本例中就是Json文档
-
3)发送请求
变化的地方在于,这里直接使用client.xxx()
的API,不再需要client.indices()
了。
5.2 查询文档
@Testvoid testGetDocumentById() throws IOException { // 1.准备Request对象 GetRequest request = new GetRequest(\"items\").id(\"100002644680\"); // 2.发送请求 GetResponse response = client.get(request, RequestOptions.DEFAULT); // 3.获取响应结果中的source String json = response.getSourceAsString(); ItemDoc itemDoc = JSONUtil.toBean(json, ItemDoc.class); System.out.println(\"itemDoc= \" + ItemDoc);}
5.3 删除文档
@Testvoid testDeleteDocument() throws IOException { // 1.准备Request,两个参数,第一个是索引库名,第二个是文档id DeleteRequest request = new DeleteRequest(\"item\", \"100002644680\"); // 2.发送请求 client.delete(request, RequestOptions.DEFAULT);}
5.4 修改文档
-
全量修改:本质是先根据id删除,再新增
-
局部修改:修改文档中的指定字段值
在RestClient的API中,全量修改与新增的API完全一致,判断依据是ID:
-
如果新增时,ID已经存在,则修改
-
如果新增时,ID不存在,则新增
局部修改
@Testvoid testUpdateDocument() throws IOException { // 1.准备Request UpdateRequest request = new UpdateRequest(\"items\", \"100002644680\"); // 2.准备请求参数 request.doc( \"price\", 58800, \"commentCount\", 1 ); // 3.发送请求 client.update(request, RequestOptions.DEFAULT);}
5.5 批量导入文档
批处理与前面讲的文档的CRUD步骤基本一致:
-
创建Request,但这次用的是
BulkRequest
-
准备请求参数
-
发送请求,这次要用到
client.bulk()
方法
BulkRequest
本身其实并没有请求参数,其本质就是将多个普通的CRUD请求组合在一起发送。例如:
-
批量新增文档,就是给每个文档创建一个
IndexRequest
请求,然后封装到BulkRequest
中,一起发出。 -
批量删除,就是创建N个
DeleteRequest
请求,然后封装到BulkRequest
,一起发出
因此BulkRequest
中提供了add
方法,用以添加其它CRUD的请求
@Testvoid testBulk() throws IOException { // 1.创建Request BulkRequest request = new BulkRequest(); // 2.准备请求参数 request.add(new IndexRequest(\"items\").id(\"1\").source(\"json doc1\", XContentType.JSON)); request.add(new IndexRequest(\"items\").id(\"2\").source(\"json doc2\", XContentType.JSON)); // 3.发送请求 client.bulk(request, RequestOptions.DEFAULT);}
当我们要导入商品数据时,由于商品数量达到数十万,因此不可能一次性全部导入。建议采用循环遍历方式,每次导入1000条左右的数据。
@Testvoid testLoadItemDocs() throws IOException { // 分页查询商品数据 int pageNo = 1; int size = 1000; while (true) { Page page = itemService.lambdaQuery().eq(Item::getStatus, 1).page(new Page(pageNo, size)); // 非空校验 List items = page.getRecords(); if (CollUtils.isEmpty(items)) { return; } log.info(\"加载第{}页数据,共{}条\", pageNo, items.size()); // 1.创建Request BulkRequest request = new BulkRequest(\"items\"); // 2.准备参数,添加多个新增的Request for (Item item : items) { // 2.1.转换为文档类型ItemDTO ItemDoc itemDoc = BeanUtil.copyProperties(item, ItemDoc.class); // 2.2.创建新增文档的Request对象 request.add(new IndexRequest() .id(itemDoc.getId()) .source(JSONUtil.toJsonStr(itemDoc), XContentType.JSON)); } // 3.发送请求 client.bulk(request, RequestOptions.DEFAULT); // 翻页 pageNo++; }}
6 DSL查询
Elasticsearch提供了基于JSON的DSL(Domain Specific Language)语句来定义查询条件,其JavaAPI就是在组织DSL条件。
6.1 Elasticsearch查询分类
ES查询分为两大类:
-
叶子查询(Leaf query clauses):简单查询,通常在特定字段中查找特定值,很少单独使用。
-
复合查询(Compound query clauses):组合多个查询,可以是逻辑组合叶子查询或改变叶子查询的行为方式。
6.2 DSL查询基本结构
GET /{索引库名}/_search{ \"query\": { \"查询类型\": { // 查询条件 } }}
GET /items/_search{ \"query\": { \"match_all\": { } }}
以最简单的无条件查询为例,无条件查询的类型是:match_all,因此其查询语句如下:
只能查到10条
处于安全考虑,elasticsearch设置了默认的查询页数。
6.3 叶子查询
6.3.1 全文检索查询
利用分词器对用户输入搜索条件先分词,得到词条,然后再利用倒排索引搜索词条。
(1)match
GET /{索引库名}/_search{ \"query\": { \"match\": { \"字段名\": \"搜索条件\" } }}
(2)multi_match
与match
类似的有multi_match
,区别在于可以同时对多个字段搜索,而且多个字段都要满足,语法示例:
GET /{索引库名}/_search{ \"query\": { \"multi_match\": { \"query\": \"搜索条件\", \"fields\": [\"字段1\", \"字段2\"] } }}
6.3.2 精确查询
不会对用户输入的搜索条件再分词,而是作为一个词条,与搜索的字段内容精确值匹配。因此推荐查找keyword
、数值、日期、boolean
类型的字段。例如:
-
id
-
price
-
城市
-
地名
-
人名
(1)term查询
GET /{索引库名}/_search{ \"query\": { \"term\": { \"字段名\": { \"value\": \"搜索条件\" } } }}
(2)range查询
GET /{索引库名}/_search{ \"query\": { \"range\": { \"字段名\": { \"gte\": {最小值}, \"lte\": {最大值} } } }}
range
是范围查询,对于范围筛选的关键字有:
-
gte
:大于等于 -
gt
:大于 -
lte
:小于等于 -
lt
:小于
6.4 复合查询-bool查询
bool查询,即布尔查询。就是利用逻辑运算来组合一个或多个查询子句的组合。bool查询支持的逻辑运算有:
-
must:必须匹配每个子查询,类似“与”
-
should:选择性匹配子查询,类似“或”
-
must_not:必须不匹配,不参与算分,类似“非”
-
filter:必须匹配,不参与算分
bool查询的语法如下:
GET /items/_search{ \"query\": { \"bool\": { \"must\": [ {\"match\": {\"name\": \"手机\"}} ], \"should\": [ {\"term\": {\"brand\": { \"value\": \"vivo\" }}}, {\"term\": {\"brand\": { \"value\": \"小米\" }}} ], \"must_not\": [ {\"range\": {\"price\": {\"gte\": 2500}}} ], \"filter\": [ {\"range\": {\"price\": {\"lte\": 1000}}} ] } }}
例如:我们要搜索手机
,但品牌必须是华为
,价格必须是900~1599
,那么可以这样写:
GET /items/_search{ \"query\": { \"bool\": { \"must\": [ {\"match\": {\"name\": \"手机\"}} ], \"filter\": [ {\"term\": {\"brand\": { \"value\": \"华为\" }}}, {\"range\": {\"price\": {\"gte\": 90000, \"lt\": 159900}}} ] } }}
6.5 排序
elasticsearch默认是根据相关度算分(_score
)来排序,但是也支持自定义方式对搜索结果排序。不过分词字段无法排序,能参与排序字段类型有:keyword
类型、数值类型、地理坐标类型、日期类型等。
GET /indexName/_search{ \"query\": { \"match_all\": {} }, \"sort\": [ { \"排序字段\": { \"order\": \"排序方式asc和desc\" } } ]}
例如:按照商品价格排序:
GET /items/_search{ \"query\": { \"match_all\": {} }, \"sort\": [ { \"price\": { \"order\": \"desc\" } } ]}
6.7 分页
elasticsearch 默认情况下只返回top10的数据。而如果要查询更多数据就需要修改分页参数了。
6.7.1 基础分页
elasticsearch中通过修改from
、size
参数来控制要返回的分页结果:
-
from
:从第几个文档开始 -
size
:总共查询几个文档
类似于mysql中的limit ?, ?
GET /items/_search{ \"query\": { \"match_all\": {} }, \"from\": 0, // 分页开始的位置,默认为0 \"size\": 10, // 每页文档数量,默认10 \"sort\": [ { \"price\": { \"order\": \"desc\" } } ]}
6.7.2 深度分页
elasticsearch的数据一般会采用分片存储,也就是把一个索引中的数据分成N份,存储到不同节点上。这种存储方式比较有利于数据扩展,但给分页带来了一些麻烦。
比如一个索引库中有100000条数据,分别存储到4个分片,每个分片25000条数据。
要查询第990~1000名的数据。从实现思路来分析,肯定是将所有数据排序,找出前1000名,截取其中的990~1000的部分。但问题来了,我们如何才能找到所有数据中的前1000名呢?
要知道每一片的数据都不一样,第1片上的第900~1000,在另1个节点上并不一定依然是900~1000名。所以我们只能在每一个分片上都找出排名前1000的数据,然后汇总到一起,重新排序,才能找出整个索引库中真正的前1000名,此时截取990~1000的数据即可。
但当查询分页深度较大时,汇总数据过多,对内存和CPU会产生非常大的压力。
因此elasticsearch会禁止from+ size
超过10000的请求。
针对深度分页,elasticsearch提供了两种解决方案:
-
search after
:分页时需要排序,原理是从上一次的排序值开始,查询下一页数据。官方推荐使用的方式。 -
scroll
:原理将排序后的文档id形成快照,保存下来,基于快照做分页。官方已经不推荐使用。
6.8 高亮
6.8.1 高亮原理
-
用户输入搜索关键字搜索数据
-
服务端根据搜索关键字到elasticsearch搜索,并给搜索结果中的关键字词条添加
html
标签 -
前端提前给约定好的
html
标签添加CSS
样式
6.8.2 实现高亮
GET /{索引库名}/_search{ \"query\": { \"match\": { \"搜索字段\": \"搜索关键字\" } }, \"highlight\": { \"fields\": { \"高亮字段名称\": { \"pre_tags\": \"\", \"post_tags\": \"\" } } }}
默认情况下参与高亮的字段要与搜索字段一致,除非添加:required_field_match=false
7 RestClient查询
7.1 查询步骤
(1)创建SearchRequest 对象:
-
指定索引库名称。
SearchRequest request = new SearchRequest(\"items\");
(2)准备请求参数(DSL):
-
使用
request.source()
构建 DSL。 -
DSL 中可以包含查询条件、分页、排序、高亮等。
request.source().query(QueryBuilders.matchAllQuery());
(3)发送请求:
-
使用
client.search()
发送请求,得到SearchResponse
。
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
(4)解析响应:
-
逐层解析
SearchResponse
,获取命中结果。
SearchHits searchHits = response.getHits();long total = searchHits.getTotalHits().value; // 总条数SearchHit[] hits = searchHits.getHits(); // 文档数组for (SearchHit hit : hits) { String source = hit.getSourceAsString(); // 原始文档数据 ItemDoc item = JSONUtil.toBean(source, ItemDoc.class); // 反序列化 System.out.println(item);}
7.2 叶子查询
//Match 查询request.source().query(QueryBuilders.matchQuery(\"name\", \"脱脂牛奶\"));//Multi Match 查询request.source().query(QueryBuilders.multiMatchQuery(\"脱脂牛奶\", \"name\", \"category\"));//Range 查询request.source().query(QueryBuilders.rangeQuery(\"price\").gte(10000).lte(30000));//Term 查询request.source().query(QueryBuilders.termQuery(\"brand\", \"华为\"));
7.3 复合查询-bool查询
@Testvoid testBool() throws IOException { // 1.创建Request SearchRequest request = new SearchRequest(\"items\"); // 2.组织请求参数 // 2.1.准备bool查询 BoolQueryBuilder bool = QueryBuilders.boolQuery(); // 2.2.关键字搜索 bool.must(QueryBuilders.matchQuery(\"name\", \"脱脂牛奶\")); // 2.3.品牌过滤 bool.filter(QueryBuilders.termQuery(\"brand\", \"德亚\")); // 2.4.价格过滤 bool.filter(QueryBuilders.rangeQuery(\"price\").lte(30000)); request.source().query(bool); // 3.发送请求 SearchResponse response = client.search(request, RequestOptions.DEFAULT); // 4.解析响应 handleResponse(response);}
7.4 排序
request.source().sort(\"price\", SortOrder.ASC);
7.5 分页
int pageNo = 1, pageSize = 5;request.source().from((pageNo - 1) * pageSize).size(pageSize);
7.6 高亮
(1)高亮条件
request.source().highlighter( SearchSourceBuilder.highlight() .field(\"name\") .preTags(\"\") .postTags(\"\"));
(2)解析高亮结果
Map hfs = hit.getHighlightFields();if (CollUtils.isNotEmpty(hfs)) { HighlightField hf = hfs.get(\"name\"); if (hf != null) { String hfName = hf.getFragments()[0].string(); // 高亮结果 item.setName(hfName); }}
完整代码
private void handleResponse(SearchResponse response) { SearchHits searchHits = response.getHits(); // 1.获取总条数 long total = searchHits.getTotalHits().value; System.out.println(\"共搜索到\" + total + \"条数据\"); // 2.遍历结果数组 SearchHit[] hits = searchHits.getHits(); for (SearchHit hit : hits) { // 3.得到_source,也就是原始json文档 String source = hit.getSourceAsString(); // 4.反序列化 ItemDoc item = JSONUtil.toBean(source, ItemDoc.class); // 5.获取高亮结果 Map hfs = hit.getHighlightFields(); if (CollUtils.isNotEmpty(hfs)) { // 5.1.有高亮结果,获取name的高亮结果 HighlightField hf = hfs.get(\"name\"); if (hf != null) { // 5.2.获取第一个高亮结果片段,就是商品名称的高亮值 String hfName = hf.getFragments()[0].string(); item.setName(hfName); } } System.out.println(item); }}
8 数据聚合
聚合(aggregations
)可以让我们极其方便的实现对数据的统计、分析、运算。例如:
-
什么品牌的手机最受欢迎?
-
这些手机的平均价格、最高价格、最低价格?
-
这些手机每月的销售情况如何?
实现这些统计功能的比数据库的sql要方便的多,而且查询速度非常快,可以实现近实时搜索效果。
8.1 聚合类型
(1)Bucket 聚合
对文档分组。
GET /items/_search{ \"size\": 0, \"aggs\": { \"category_agg\": { \"terms\": { \"field\": \"category\", \"size\": 20 } } }}
-
size
:设置size
为0,就是每页查0条,则结果中就不包含文档,只包含聚合 -
aggs
:定义聚合-
category_agg
:聚合名称,自定义,但不能重复-
terms
:聚合的类型,按分类聚合,所以用term
-
field
:参与聚合的字段名称 -
size
:希望返回的聚合结果的最大数量
-
-
-
示例:按品牌分组。
DSL:
GET /items/_search{ \"size\": 0, \"aggs\": { \"brand_agg\": { \"terms\": { \"field\": \"brand\", \"size\": 20 } } }}
JavaAPI:
request.source().aggregation( AggregationBuilders.terms(\"brand_agg\").field(\"brand\").size(5));
(2)带条件聚合
例如:
我们需要从需求中分析出搜索查询的条件和聚合的目标:
-
搜索查询条件:
-
价格高于3000
-
必须是手机
-
-
聚合目标:统计的是品牌,肯定是对brand字段做term聚合
GET /items/_search{ \"query\": { \"bool\": { \"filter\": [ { \"term\": { \"category\": \"手机\" } }, { \"range\": { \"price\": { \"gte\": 300000 } } } ] } }, \"size\": 0, \"aggs\": { \"brand_agg\": { \"terms\": { \"field\": \"brand\", \"size\": 20 } } }}
JavaAPI:
BoolQueryBuilder bool = QueryBuilders.boolQuery() .filter(QueryBuilders.termQuery(\"category\", \"手机\")) .filter(QueryBuilders.rangeQuery(\"price\").gte(300000));request.source().query(bool).size(0);request.source().aggregation( AggregationBuilders.terms(\"brand_agg\").field(\"brand\").size(5));
完整代码如下:
@Testvoid testAgg() throws IOException { // 1.创建Request SearchRequest request = new SearchRequest(\"items\"); // 2.准备请求参数 BoolQueryBuilder bool = QueryBuilders.boolQuery() .filter(QueryBuilders.termQuery(\"category\", \"手机\")) .filter(QueryBuilders.rangeQuery(\"price\").gte(300000)); request.source().query(bool).size(0); // 3.聚合参数 request.source().aggregation( AggregationBuilders.terms(\"brand_agg\").field(\"brand\").size(5) ); // 4.发送请求 SearchResponse response = client.search(request, RequestOptions.DEFAULT); // 5.解析聚合结果 Aggregations aggregations = response.getAggregations(); // 5.1.获取品牌聚合 Terms brandTerms = aggregations.get(\"brand_agg\"); // 5.2.获取聚合中的桶 List buckets = brandTerms.getBuckets(); // 5.3.遍历桶内数据 for (Terms.Bucket bucket : buckets) { // 5.4.获取桶内key String brand = bucket.getKeyAsString(); System.out.print(\"brand = \" + brand); long count = bucket.getDocCount(); System.out.println(\"; count = \" + count); }}
(3)Metric聚合
以上,我们统计了价格高于3000的手机品牌,形成了一个个桶。现在我们需要对桶内的商品做运算,获取每个品牌价格的最小值、最大值、平均值。
这就要用到Metric
聚合了,例如stat
聚合,就可以同时获取min
、max
、avg
等结果。
语法如下:
GET /items/_search{ \"query\": { \"bool\": { \"filter\": [ { \"term\": { \"category\": \"手机\" } }, { \"range\": { \"price\": { \"gte\": 300000 } } } ] } }, \"size\": 0, \"aggs\": { \"brand_agg\": { \"terms\": { \"field\": \"brand\", \"size\": 20 }, \"aggs\": { \"stats_meric\": { \"stats\": { \"field\": \"price\" } } } } }}
JavaAPI:
request.source().aggregation( AggregationBuilders.terms(\"brand_agg\").field(\"brand\").size(5) .subAggregation(AggregationBuilders.stats(\"stats_meric\").field(\"price\")));
聚合条件的要利用AggregationBuilders
这个工具类来构造
聚合结果与搜索文档同一级别,因此需要单独获取和解析。具体解析语法如下:
可以看到我们在brand_agg
聚合的内部,我们新加了一个aggs
参数。这个聚合就是brand_agg
的子聚合,会对brand_agg
形成的每个桶中的文档分别统计。
-
stats_meric
:聚合名称-
stats
:聚合类型,stats是metric
聚合的一种-
field
:聚合字段,这里选择price
,统计价格
-
-
由于stats是对brand_agg形成的每个品牌桶内文档分别做统计,因此每个品牌都会统计出自己的价格最小、最大、平均值。
另外,我们还可以让聚合按照每个品牌的价格平均值排序:
8.2 聚合总结
aggs代表聚合,与query同级,此时query的作用是?
-
限定聚合的的文档范围
聚合必须的三要素:
-
聚合名称
-
聚合类型
-
聚合字段
聚合可配置属性有:
-
size:指定聚合结果数量
-
order:指定聚合结果排序方式
-
field:指定聚合字段
好多字儿啊.....