基础篇
数据库的模糊搜索功能单一,匹配条件非常苛刻,必须恰好包含用户搜索的关键字。而在搜索引擎中,用户输入出现个别错字,或者用拼音搜索、同义词搜索都能正确匹配到数据。
综上,在面临海量数据的搜索,或者有一些复杂搜索需求的时候,推荐使用专门的搜索引擎来实现搜索功能。
学习目标:
- 理解倒排索引原理
- 会使用IK分词器
- 理解索引库Mapping映射的属性含义
- 能创建索引库及映射
- 能实现文档的CRUD
1.初识elasticsearch
Elasticsearch的官方网站如下:
https://www.elastic.co/cn/elasticsearch
本章我们一起来初步了解一下Elasticsearch的基本原理和一些基础概念。
1.1.认识和安装
Elasticsearch是由elastic公司开发的一套搜索引擎技术,它是elastic技术栈中的一部分。完整的技术栈包括:
- Elasticsearch:用于数据存储、计算和搜索
- Logstash/Beats:用于数据收集
- Kibana:用于数据可视化
整套技术栈被称为ELK,经常用来做日志收集、系统监控和状态分析等等:

整套技术栈的核心就是用来存储、搜索、计算的Elasticsearch,因此我们接下来学习的核心也是Elasticsearch。
我们要安装的内容包含2部分:
- elasticsearch:存储、搜索和运算
- kibana:图形化展示
Elasticsearch,是提供核心的数据存储、搜索、分析功能的。
关于Kibana,Elasticsearch对外提供的是Restful风格的API,任何操作都可以通过发送http请求来完成。不过http请求的方式、路径、还有请求参数的格式都有严格的规范。这些规范我们肯定记不住,因此我们要借助于Kibana这个服务。
Kibana是elastic公司提供的用于操作Elasticsearch的可视化控制台。它的功能非常强大,包括:
- 对Elasticsearch数据的搜索、展示
- 对Elasticsearch数据的统计、聚合,并形成图形化报表、图形
- 对Elasticsearch的集群状态监控
- 它还提供了一个开发控制台(DevTools),在其中对Elasticsearch的Restful的API接口提供了语法提示(我们安装kibana的原因)
1.1.1.安装elasticsearch
通过下面的Docker命令即可安装单机版本的elasticsearch:
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 hmall \
-p 9200:9200 \
-p 9300:9300 \
elasticsearch:7.12.1提前关防火墙!

- -e “ES_JAVA_OPTS=-Xms512m -Xmx512m”:设置 Elasticsearch 的 Java 堆内存大小为 512MB(初始值和最大值)。
- -e “discovery.type=single-node”:指定 Elasticsearch 为单节点模式,防止集群发现功能启动。
这里我们采用的是elasticsearch的7.12.1版本,由于8以上版本的JavaAPI变化很大,在企业中应用并不广泛
安装完成后,访问9200端口,即可看到响应的Elasticsearch服务的基本信息:

1.1.2.安装Kibana
通过下面的Docker命令,即可部署Kibana:
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字段已经创建了索引,由于索引底层采用了B+树结构,因此我们根据id搜索的速度会非常快。但是其他字段例如title,只在叶子节点上存在。

因此要根据title搜索的时候只能遍历树中的每一个叶子节点,判断title数据是否符合要求。
比如用户的SQL语句为:
select * from tb_goods where title like '%手机%';那搜索的大概流程如图:

说明:
- 1)检查到搜索条件为
like '%手机%',需要找到title中包含手机的数据 - 2)逐条遍历每行数据(每个叶子节点),比如第1次拿到
id为1的数据 - 3)判断数据中的
title字段值是否符合条件 - 4)如果符合则放入结果集,不符合则丢弃
- 5)回到步骤1
综上,根据id精确匹配时,可以走索引,查询效率较高。而当搜索条件为模糊匹配时,由于索引无法生效,导致从索引查询退化为全表扫描,效率很差。
因此,正向索引适合于根据索引字段的精确搜索,不适合基于部分词条的模糊匹配。
而倒排索引恰好解决的就是根据部分词条模糊匹配的问题。
1.2.2.倒排索引

倒排索引中有两个非常重要的概念:
- 文档(
Document):用来搜索的数据,其中的每一条数据就是一个文档。例如一个网页、一个商品信息

- 词条(
Term):对文档数据或用户搜索数据,利用某种算法分词,得到的具备含义的词语就是词条。例如:我是中国人,就可以分为:我、是、中国人、中国、国人这样的几个词条
创建倒排索引是对正向索引的一种特殊处理和应用,流程如下:
- 将每一个文档的数据利用分词算法根据语义拆分,得到一个个词条
- 创建表,每行数据包括词条、词条所在文档id、位置等信息

- 因为词条唯一性,可以给词条创建正向索引
倒排索引的搜索流程如下(以搜索”华为手机”为例),如图:


流程描述:
- 用户搜索关键词:华为手机
- 分词:华为、手机
- 倒排索引查词条 → 得到文档 id 集合
- 根据文档 id 查找具体文档内容(正向索引)
⚠️ 倒排索引能高效工作是因为“词条”和“文档 id”都建立了高效索引。
虽然要先查询倒排索引,再查询正向索引,但是无论是词条、还是文档id都建立了索引,查询速度非常快!无需全表扫描。
正向索引 VS 倒排索引
| 比较项目 | 正向索引(传统数据库) | 倒排索引(Elasticsearch) |
|---|---|---|
| 查询方式 | 根据 id 查询某条记录,再看字段中是否包含关键词 | 先对关键词分词,再查每个词出现在哪些文档中 |
| 核心逻辑 | 文档 -> 词条 | 词条 -> 文档 |
| 适用场景 | 精确匹配:如根据 id、主键查询 | 模糊搜索、全文检索:如搜索 “小米手机” |
| 性能 | 非索引字段模糊搜索性能差(会全表扫描) | 性能极高,分词+索引查找词条 |
| 缺点 | 无法有效支持模糊匹配 | 无法用于字段排序或非分词字段的复杂查询 |

1.2.3.正向和倒排
那么为什么一个叫做正向索引,一个叫做倒排索引呢?
- 正向索引是最传统的,根据id索引的方式。但根据词条查询时,必须先逐条获取每个文档,然后判断文档中是否包含所需要的词条,是根据文档找词条的过程。
- 而倒排索引则相反,是先找到用户要搜索的词条,根据词条得到保护词条的文档的id,然后根据id获取文档。是根据词条找文档的过程。

是不是恰好反过来了?
| 名称 | 过程 |
|---|---|
| 正向索引 | 文档 → 词条 |
| 倒排索引 | 词条 → 文档 |
那么两者方式的优缺点是什么呢?
| 类型 | 优势 | 劣势 |
|---|---|---|
| 正向索引 | 精确查询速度快,支持排序、范围查询 | 模糊搜索时性能差,全表扫描 |
| 倒排索引 | 模糊搜索性能优越,支持全文检索,适合搜索引擎 | 不能用于排序,仅支持词条维度的索引 |
1.3.基础概念
elasticsearch中有很多独有的概念,与mysql中略有差别,但也有相似之处。
1.3.1.文档和字段
elasticsearch是面向文档(Document)存储的,可以是数据库中的一条商品数据,一个订单信息。文档数据会被序列化为json格式后存储在elasticsearch中:

因此,原本数据库中的一行数据就是ES中的一个JSON文档;而数据库中每行数据都包含很多列,这些列就转换为JSON文档中的字段(Field)。

1.3.2.索引和映射
索引:某个字段词义逻辑雷同的一系列文档的“文档集合”。不一定要json格式完全一致。
随着业务发展,需要在es中存储的文档也会越来越多,比如有商品的文档、用户的文档、订单文档等等:
所有文档都散乱存放显然非常混乱,也不方便管理。
因此,我们要将类型相同的文档集中在一起管理,称为索引(Index)。例如:

- 所有用户文档,就可以组织在一起,称为用户的索引;
- 所有商品的文档,可以组织在一起,称为商品的索引;
- 所有订单的文档,可以组织在一起,称为订单的索引;
因此,我们可以把索引当做是数据库中的表。(这里的索引也可以叫索引库)
数据库的表会有约束信息,用来定义表的结构、字段的名称、类型等信息。因此,索引库中就有映射(mapping),是索引中文档的字段约束信息,类似表的结构约束。


1.3.3.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 的库 → ES 的索引库
- MySQL 的表结构 → ES 的 Mapping
- MySQL 的行 → ES 的文档
- MySQL 的列 → ES 的字段
那是不是说,我们学习了elasticsearch就不再需要mysql了呢?
并不是如此,两者各自有自己的擅长之处:
- Mysql:擅长事务类型操作,可以确保数据的安全和一致性
- Elasticsearch:擅长海量数据的搜索、分析、计算

因此在企业中,往往是两者结合使用:
- 对安全性要求较高的写操作,使用mysql实现
- 对查询性能要求较高的搜索需求,使用elasticsearch实现
- 两者再基于某种方式(Logstash 通过 JDBC 插件从 MySQL、PostgreSQL 等数据库读取数据,同步到 Elasticsearch 用于搜索。例如:将用户信息表同步到 ES,支持实时搜索),实现数据的同步,保证一致性

1.4.IK分词器
Elasticsearch的关键就是倒排索引,而倒排索引依赖于对文档内容的分词,而分词则需要高效、精准的分词算法,IK分词器就是这样一个中文分词算法。

1.4.1.安装IK分词器
方案一:在线安装
运行一个命令即可:
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.zip然后重启es容器:
docker restart es方案二:离线安装
如果网速较差,也可以选择离线安装。
首先,查看之前安装的Elasticsearch容器的plugins数据卷目录:
docker volume inspect es-plugins结果如下:
[
{
"CreatedAt": "2024-11-06T10:06:34+08:00",
"Driver": "local",
"Labels": null,
"Mountpoint": "/var/lib/docker/volumes/es-plugins/_data",
"Name": "es-plugins",
"Options": null,
"Scope": "local"
}
]可以看到elasticsearch的插件挂载到了/var/lib/docker/volumes/es-plugins/_data这个目录。我们需要把IK分词器上传至这个目录。
1.4.2.使用IK分词器
IK分词器包含两种模式:
| 模式名 | 描述 |
|---|---|
ik_smart | 智能分词,粗粒度,只保留最核心词义(如搜索标题、精确匹配) |
ik_max_word | 最大词粒度,细粒度,列出所有可能组合(如文章内容、模糊搜索) |
ik_smart:智能语义切分(最小划分,只保留最核心语义的词。在不影响分词后词元的含义下“粗”粒度划分,减少过多划分后冗余词干扰。需要用户输入明确的关键词来严格匹配。适合主搜索字段(如标题、名称),和需要精准匹配的场景(如订单号、用户名)。)ik_max_word:最细粒度切分(穷尽所有可能组合,覆盖所有可能子词。保证用户输入可能不完整或包含子词。例如:“中华人民共和国”,搜索:“人民共和国”,也可以搜到整个句子。适合长文本内容(如文章正文、评论),和需要模糊搜索或高召回的场景(如日志关键词检索)。)
我们在Kibana的DevTools上来测试分词器,首先测试Elasticsearch官方提供的标准分词器:

POST /_analyze
{
"analyzer": "standard",
"text": "搁浅学习java太棒了"
}
可以看到,标准分词器只能1字1词条,无法正确对中文做分词。
我们再测试IK分词器:
POST /_analyze
{
"analyzer": "ik_smart",
"text": "搁浅学习java太棒了"
}
1.4.3.拓展词典
随着互联网的发展,“造词运动”也越发的频繁。出现了很多新的词语,在原有的词汇列表中并不存在。比如:“泰裤辣”,“小黑子漏出鸡脚了吧” 等。
IK分词器无法对这些词汇分词,测试一下:

所以要想正确分词,IK分词器的词库也需要不断的更新,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)在IK分词器的config目录新建一个 ext.dic,可以参考config目录下复制一个配置文件进行修改
王源
封个烟4)重启elasticsearch
docker restart es
# 查看 日志
docker logs -f elasticsearch再次测试,可以发现传智播客和泰裤辣都正确分词了:

1.4.4.总结
分词器作用:
- 创建索引时对文档分词
- 查询时对用户输入分词
IK 分词模式:
ik_smart:粗粒度ik_max_word:细粒度
拓展词库方法:
- 配置
IKAnalyzer.cfg.xml - 新建
ext.dic添加自定义词条
2.索引库操作


2.1.Mapping映射属性
Mapping是对索引库中文档的约束,常见的Mapping属性包括:
type:字段数据类型,常见的简单类型有:- 字符串:
text(可分词的文本)、keyword(精确值,例如:品牌、国家、ip地址) - 数值:
long、integer、short、byte、double、float、 - 布尔:
boolean - 日期:
date - 对象:
object
- 字符串:
index:是否创建索引,默认为trueanalyzer:使用哪种分词器properties:该字段的子字段
| 类型 | 说明 |
|---|---|
text | 可分词,适用于长文本,如文章、标题等 |
keyword | 精确匹配,如分类、邮箱、IP等 |
integer | 整型,适用于年龄、数量等 |
boolean | 布尔值 true / false |
date | 日期时间格式 |
object | 嵌套对象(JSON子结构) |
其他属性:
index: 是否创建倒排索引(默认 true)- 设置为 false 则该字段无法被搜索
analyzer: 使用的分词器(ik_smart、standard等)properties: 用于定义嵌套对象的字段结构(类似 JSON 的子字段)

例如下面的json文档:
{
"age": 21,
"weight": 52.1,
"isMarried": false,
"info": "黑马程序员Java讲师",
"email": "zy@itcast.cn",
"score": [99.1, 99.5, 98.9],
"name": {
"firstName": "云",
"lastName": "赵"
}
}

2.2.索引库的CRUD
由于Elasticsearch采用的是Restful风格的API,因此其请求方式和路径相对都比较规范,而且请求参数也都采用JSON风格。
我们直接基于Kibana的DevTools来编写请求做测试,由于有语法提示,会非常方便。
2.2.1.创建索引库和映射
基本语法:
- 请求方式:
PUT - 请求路径:
/索引库名,可以自定义 - 请求参数:
mapping映射
格式:
PUT /索引库名称
{
"mappings": {
"properties": {
"字段名":{
"type": "text",
"analyzer": "ik_smart"
},
"字段名2":{
"type": "keyword",
"index": "false"
},
"字段名3":{
"properties": {
"子字段": {
"type": "keyword"
}
}
},
// ...略
}
}
}示例:
PUT /heima
{
"mappings": {
"properties": {
"info": { "type": "text", "analyzer": "ik_smart" },
"email": { "type": "keyword", "index": false },
"name": {
"properties": {
"firstName": { "type": "keyword" }
}
}
}
}
}
2.2.2.查询索引库
基本语法:
- 请求方式:GET
- 请求路径:/索引库名
- 请求参数:无
格式:
GET /索引库名示例:
GET /heima2.2.3.修改索引库
❗ 注意:Elasticsearch 不允许修改已有字段类型或分词器,只能添加新字段!
倒排索引结构虽然不复杂,但是一旦数据结构改变(比如改变了分词器),就需要重新创建倒排索引,这简直是灾难。因此索引库一旦创建,无法修改mapping。
语法说明:
PUT /索引库名/_mapping
{
"properties": {
"新字段名":{
"type": "integer"
}
}
}示例:
PUT /heima/_mapping
{
"properties": {
"age":{
"type": "integer"
}
}
}2.2.4.删除索引库
语法:
- 请求方式:DELETE
- 请求路径:/索引库名
- 请求参数:无
格式:
DELETE /索引库名示例:
DELETE /heima2.2.5.总结
索引库操作有哪些?
- 创建索引库:PUT /索引库名
- 查询索引库:GET /索引库名
- 删除索引库:DELETE /索引库名
- 修改索引库,添加字段:PUT /索引库名/_mapping
可以看到,对索引库的操作基本遵循的Restful的风格,因此API接口非常统一,方便记忆。
| 操作 | 请求方式 | 请求路径 | 请求体 |
|---|---|---|---|
| 创建索引 | PUT | /索引名 | 有 |
| 查询索引 | GET | /索引名 | 无 |
| 删除索引 | DELETE | /索引名 | 无 |
| 添加字段 | PUT | /索引名/_mapping | 有 |
注:POST /索引库名 是用于向索引库中新增文档(数据),而不是创建索引结构


| 操作类型 | 请求方法 | 路径 | 用途 |
|---|---|---|---|
| 创建索引库 | PUT | /index_name | 创建索引和结构(Mapping) |
| 添加文档(自动ID) | POST | /index_name | 插入数据文档 |
| 添加文档(指定ID) | POST | /index_name/_doc/{id} | 插入/更新数据文档 |
3.文档操作
有了索引库,接下来就可以向索引库中添加数据了。
Elasticsearch中的数据其实就是JSON风格的文档。操作文档自然保护增、删、改、查等几种常见操作,我们分别来学习。
3.1.新增文档
语法:
POST /索引库名/_doc/文档id
{
"字段1": "值1",
"字段2": "值2",
"字段3": {
"子属性1": "值3",
"子属性2": "值4"
},
}示例:
POST /heima/_doc/1
{
"info": "黑马程序员Java讲师",
"email": "zy@itcast.cn",
"name": {
"firstName": "云",
"lastName": "赵"
}
}说明:
- 路径中的
_doc是文档类型,ES7+ 虽然只支持一个类型,但这个字段保留。 - 文档ID可指定(如
/1),也可不写让系统自动生成。
响应:


3.2.查询文档
根据rest风格,新增是post,查询应该是get,不过查询一般都需要条件,这里我们把文档id带上。
语法:
GET /{索引库名称}/_doc/{id}示例:
GET /heima/_doc/1说明:
- 查询的是整个文档内容,包括字段值和元信息(如
_id,_index,_version等)。
查看结果:

3.3.删除文档
删除使用DELETE请求,同样,需要根据id进行删除:
语法:
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": "赵"
}
}说明:
- 会完全覆盖原文档(未提供的字段将被删除)。
- 如果 ID 不存在,会创建新文档(created);存在则覆盖(updated)。
由于id为1的文档已经被删除,所以第一次执行时,得到的反馈是created:

所以如果执行第2次时,得到的反馈则是updated:

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"} }



说明:
- 每个操作是一对 JSON 对象,一行一个,不能少。
- 用于提高写入或删除的性能,非常适合大批量数据导入。
其中:
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": {字段}}
- 全量修改:
| 操作类型 | 方法 | 路径结构 | 内容 |
|---|---|---|---|
| 新增文档 | POST | /index/_doc/id | 整个文档内容 |
| 查询文档 | GET | /index/_doc/id | 无内容体 |
| 删除文档 | DELETE | /index/_doc/id | 无内容体 |
| 修改文档(全) | PUT | /index/_doc/id | 完整替换原文档 |
| 修改文档(局) | POST | /index/_update/id | { "doc": { "字段": 值 }} |
| 批量操作 | POST | /_bulk | 结构化一组操作 |
4.RestAPI
ES官方提供了各种不同语言的客户端,用来操作ES。这些客户端的本质就是组装DSL语句,通过http请求发送给ES。
官方文档地址:
https://www.elastic.co/guide/en/elasticsearch/client/index.html
由于ES目前最新版本是8.8,提供了全新版本的客户端,老版本的客户端已经被标记为过时。而我们采用的是7.12版本,因此只能使用老版本客户端:

然后选择7.12版本,HighLevelRestClient版本
4.1.初始化RestClient
Elasticsearch 官方提供的 Java 客户端叫做 RestHighLevelClient,所有操作(增删改查、索引管理等)都通过这个对象完成。
分为三步:
① 添加 Maven 依赖
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-high-level-client</artifactId>
</dependency>② 指定 ES 版本(避免与 Spring Boot 默认版本冲突)
<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
<elasticsearch.version>7.12.1</elasticsearch.version>
</properties>③ 编写连接代码(推荐放在单元测试的 @BeforeEach 中)
初始化的代码如下:
RestHighLevelClient client = new RestHighLevelClient(RestClient.builder(
HttpHost.create("http://192.168.32.128:9200")
));这里为了单元测试方便,我们创建一个测试类IndexTest,然后将初始化的代码编写在@BeforeEach方法中:
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 {
client.close();
}
}别忘了释放资源,防止连接泄漏!

4.2.创建索引库
由于要实现对商品搜索,所以我们需要将商品添加到Elasticsearch中,不过需要根据搜索业务的需求来设定索引库结构,而不是一股脑的把MySQL数据写入Elasticsearch.
4.2.1.Mapping映射


实现搜索功能需要的字段包括三大部分:
- 搜索过滤字段
- 分类
- 品牌
- 价格
- 排序字段
- 默认:按照更新时间降序排序
- 销量
- 价格
- 展示字段
- 商品id:用于点击后跳转
- 图片地址
- 是否是广告推广商品
- 名称
- 价格
- 评价数量
- 销量
对应的商品表结构如下,索引库无关字段已经划掉:

结合数据库表结构,以上字段对应的mapping映射属性如下:


最终我们的索引库文档结构应该是这样:
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.创建索引
创建索引库的API如下:

代码分为三步:
- 1)创建Request对象。
- 因为是创建索引库的操作,因此Request是
CreateIndexRequest。
- 因为是创建索引库的操作,因此Request是
- 2)添加请求参数
- 其实就是Json格式的Mapping映射参数。因为json字符串很长,这里是定义了静态字符串常量
MAPPING_TEMPLATE,让代码看起来更加优雅。
- 其实就是Json格式的Mapping映射参数。因为json字符串很长,这里是定义了静态字符串常量
- 3)发送请求
client.indices()方法的返回值是IndicesClient类型,封装了所有与索引库操作有关的方法。例如创建索引、删除索引、判断索引是否存在等

在item-service中的IndexTest测试类中,具体代码如下:
@Test
void 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.删除索引库
注:删除索引库的操作时,索引库以及其中的所有文档会被直接删除。这意味着索引及其包含的数据、设置和映射都会被移除。此操作是不可逆的,所以在执行之前应确保已经备份了所有需要的数据。
删除索引库的请求非常简单:
DELETE /hotel与创建索引库相比:
- 请求方式从PUT变为DELTE
- 请求路径不变
- 无请求参数
所以代码的差异,注意体现在Request对象上。流程如下:
- 1)创建Request对象。这次是DeleteIndexRequest对象
- 2)准备参数。这里是无参,因此省略
- 3)发送请求。改用delete方法
在item-service中的IndexTest测试类中,编写单元测试,实现删除索引:
@Test
void testDeleteIndex() throws IOException {
// 1.创建Request对象
DeleteIndexRequest request = new DeleteIndexRequest("items");
// 2.发送请求
client.indices().delete(request, RequestOptions.DEFAULT);
}
4.4.判断索引库是否存在
判断索引库是否存在,本质就是查询,对应的请求语句是:
GET /hotel因此与删除的Java代码流程是类似的,流程如下:
- 1)创建Request对象。这次是GetIndexRequest对象
- 2)准备参数。这里是无参,直接省略
- 3)发送请求。改用exists方法
@Test
void testExistsIndex() throws IOException {
// 1.创建Request对象
GetIndexRequest request = new GetIndexRequest("items");
// 2.发送请求
boolean exists = client.indices().exists(request, RequestOptions.DEFAULT);
// 3.输出
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操作文档
索引库准备好以后,就可以操作文档了。为了与索引库操作分离,我们再次创建一个测试类,做两件事情:
- 初始化RestHighLevelClient
- 我们的商品数据在数据库,需要利用IItemService去查询,所以注入这个接口
package com.hmall.item.es;
import com.hmall.item.service.IItemService;
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.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.io.IOException;
@SpringBootTest(properties = "spring.profiles.active=local")
public class DocumentTest {
private RestHighLevelClient client;
@Autowired
private IItemService itemService;
@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();
}
}
5.1.新增文档
数据源是数据库(如 MySQL)
不直接造假数据,而是将真实商品导入 ES
5.1.1.实体类
索引库结构与数据库结构还存在一些差异,因此我们要定义一个索引库结构对应的实体。
在item-service模块的com.hmall.item.domain.po包中定义一个新的DTO:
package com.hmall.item.domain.po;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.time.LocalDateTime;
@Data
@ApiModel(description = "索引库实体")
public class ItemDoc{
@ApiModelProperty("商品id")
private String id;
@ApiModelProperty("商品名称")
private String name;
@ApiModelProperty("价格(分)")
private Integer price;
@ApiModelProperty("商品图片")
private String image;
@ApiModelProperty("类目名称")
private String category;
@ApiModelProperty("品牌名称")
private String brand;
@ApiModelProperty("销量")
private Integer sold;
@ApiModelProperty("评论数")
private Integer commentCount;
@ApiModelProperty("是否是推广广告,true/false")
private Boolean isAD;
@ApiModelProperty("更新时间")
private LocalDateTime updateTime;
}5.1.2.API语法
新增文档的请求语法如下:
POST /{索引库名}/_doc/1
{
"name": "Jack",
"age": 21
}对应的JavaAPI如下:

可以看到与索引库操作的API非常类似,同样是三步走:
- 1)创建Request对象,这里是
IndexRequest,因为添加文档就是创建倒排索引的过程 - 2)准备请求参数,本例中就是Json文档
- 3)发送请求
变化的地方在于,这里直接使用client.xxx()的API,不再需要client.indices()了。
5.1.3.完整代码
我们导入商品数据,除了参考API模板“三步走”以外,还需要做几点准备工作:
- 商品数据来自于数据库,我们需要先查询出来,得到
Item对象 Item对象需要转为ItemDoc对象ItemDoc需要序列化为json格式
因此,代码整体步骤如下:
- 1)根据id查询商品数据
Item - 2)将
Item封装为ItemDoc - 3)将
ItemDoc序列化为JSON - 4)创建IndexRequest,指定索引库名和id
- 5)准备请求参数,也就是JSON文档
- 6)发送请求
在item-service的DocumentTest测试类中,编写单元测试:
@Test
void 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);
}5.2.查询文档
我们以根据id查询文档为例
5.2.1.语法说明
查询的请求语句如下:
GET /{索引库名}/_doc/{id}与之前的流程类似,代码大概分2步:
- 创建Request对象
准备请求参数,这里是无参,直接省略- 发送请求
不过查询的目的是得到结果,解析为ItemDTO,还要再加一步对结果的解析。示例代码如下:

可以看到,响应结果是一个JSON,其中文档放在一个_source属性中,因此解析就是拿到_source,反序列化为Java对象即可。
其它代码与之前类似,流程如下:
- 1)准备Request对象。这次是查询,所以是
GetRequest - 2)发送请求,得到结果。因为是查询,这里调用
client.get()方法 - 3)解析结果,就是对JSON做反序列化
5.2.2.完整代码
在item-service的DocumentTest测试类中,编写单元测试:
@Test
void 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);
}特点:
- 返回内容在
_source字段中 - 需要进行反序列化成 Java 对象
5.3.删除文档
删除的请求语句如下:
DELETE /{索引库名称}/_doc/{id}与查询相比,仅仅是请求方式从DELETE变成GET,可以想象Java代码应该依然是2步走:
- 1)准备Request对象,因为是删除,这次是
DeleteRequest对象。要指定索引库名和id - 2)
准备参数,无参,直接省略 - 3)发送请求。因为是删除,所以是
client.delete()方法
在item-service的DocumentTest测试类中,编写单元测试:
@Test
void 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不存在,则新增
我们主要关注局部修改的API即可。
5.4.1.语法说明
局部修改的请求语法如下:
POST /{索引库名}/_update/{id}
{
"doc": {
"字段名": "字段值",
"字段名": "字段值"
}
}代码示例如图:

与之前类似,也是三步走:
- 1)准备
Request对象。这次是修改,所以是UpdateRequest - 2)准备参数。也就是JSON文档,里面包含要修改的字段
- 3)更新文档。这里调用
client.update()方法
5.4.2.完整代码
在item-service的DocumentTest测试类中,编写单元测试:
@Test
void 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.批量导入文档
一次性导入大量文档,避免一条一条处理,提升效率。
- 利用Logstash批量导入
- 需要安装Logstash
- 对数据的再加工能力较弱
- 无需编码,但要学习编写Logstash导入配置
- 利用JavaAPI批量导入
- 需要编码,但基于JavaAPI,学习成本低
- 更加灵活,可以任意对数据做再加工处理后写入索引库
接下来,我们就学习下如何利用JavaAPI实现批量文档导入。
5.5.1.语法说明
批处理与前面讲的文档的CRUD步骤基本一致:
- 创建Request,但这次用的是
BulkRequest

- 准备请求参数
- 发送请求,这次要用到
client.bulk()方法
BulkRequest本身其实并没有请求参数,其本质就是将多个普通的CRUD请求组合在一起发送。例如:
- 批量新增文档,就是给每个文档创建一个
IndexRequest请求,然后封装到BulkRequest中,一起发出。 - 批量删除,就是创建N个
DeleteRequest请求,然后封装到BulkRequest,一起发出
因此BulkRequest中提供了add方法,用以添加其它CRUD的请求:

可以看到,能添加的请求有:
IndexRequest,也就是新增UpdateRequest,也就是修改DeleteRequest,也就是删除
因此Bulk中添加了多个IndexRequest,就是批量新增功能了。示例:
@Test
void 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);
}5.5.2.完整代码
从数据库中 分页读取商品数据,每次取 1000 条,然后 批量写入到 Elasticsearch 的 items 索引中。
item-service的DocumentTest测试类中,编写单元测试:
@Test
void testLoadItemDocs() throws IOException {
// 分页查询商品数据
int pageNo = 1;
int size = 1000;
while (true) {
Page<Item> page = itemService.lambdaQuery().eq(Item::getStatus, 1).page(new Page<Item>(pageNo, size));
// 非空校验
List<Item> 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++;
}
}






GET /items/_count

索引库里有这么多数据
5.6.小结
文档操作的基本步骤:
- 初始化
RestHighLevelClient - 创建XxxRequest。
- XXX是
Index、Get、Update、Delete、Bulk
- XXX是
- 准备参数(
Index、Update、Bulk时需要) - 发送请求。
- 调用
RestHighLevelClient#.xxx()方法,xxx是index、get、update、delete、bulk
- 调用
- 解析结果(
Get时需要)
| 操作类型 | Java 类 | 方法调用 | 参数形式 |
|---|---|---|---|
| 新增 | IndexRequest | client.index() | JSON 文档 |
| 查询 | GetRequest | client.get() | 文档 ID |
| 删除 | DeleteRequest | client.delete() | 文档 ID |
| 修改 | UpdateRequest | client.update() | "doc": {字段} |
| 批量操作 | BulkRequest | client.bulk() | 多个 Index/Update 等 |
基础篇完
高级篇
我们已经导入了大量数据到elasticsearch中,实现了商品数据的存储。不过查询商品数据时依然采用的是根据id查询,而非模糊搜索。
我们来研究下elasticsearch的数据搜索功能。Elasticsearch提供了基于JSON的DSL(Domain Specific Language)语句来定义查询条件,其JavaAPI就是在组织DSL条件。
1.DSL查询

Elasticsearch的查询可以分为两大类:
复合查询(Compound query clauses):以逻辑方式组合多个叶子查询或者更改叶子查询的行为方式。
叶子查询(Leaf query clauses):一般是在特定的字段里查询特定值,属于简单查询,很少单独使用。

1.1.快速入门
我们依然在Kibana的DevTools中学习查询的DSL语法。首先来看查询的语法结构:

说明:
GET /{索引库名}/_search:其中的_search是固定路径,不能修改
例如,我们以最简单的无条件查询为例,无条件查询的类型是:match_all,因此其查询语句如下:
//查询所有
GET /items/_search
{
"query": {
"match_all": {
}
}
}这个例子是一个最基础的无条件查询,使用 match_all,相当于 SQL 中的 SELECT * FROM table。
⚠️:默认返回前 10 条记录,可以通过 size 参数设置更多返回结果。
执行结果如下:

你会发现虽然是match_all,但是响应结果中并不会包含索引库中的所有文档,而是仅有10条。这是因为处于安全考虑,elasticsearch设置了默认的查询页数。
1.2.叶子查询
叶子查询的类型也可以做进一步细分,详情大家可以查看官方文档:
Query DSL | Elasticsearch Guide [7.12] | Elastic

这里列举一些常见的,例如:
- 全文检索查询(Full Text Queries):利用分词器对用户输入搜索条件先分词,得到词条,然后再利用倒排索引搜索词条。例如:
match:multi_match
- 精确查询(Term-level queries):不对用户输入搜索条件分词,根据字段内容精确值匹配。但只能查找keyword、数值、日期、boolean类型的字段。例如:
idstermrange
- 地理坐标查询:用于搜索地理位置,搜索方式很多,例如:
geo_bounding_box:按矩形搜索geo_distance:按点和半径搜索


- …略
1.2.1.全文检索查询(Full Text Queries)
全文检索的种类也很多,详情可以参考官方文档:
Full text queries | Elasticsearch Guide [7.12] | Elastic
原理:分词器先对搜索条件进行分词,然后匹配倒排索引。
| 类型 | 用途说明 |
|---|---|
match | 匹配单个字段,支持分词 |
multi_match | 匹配多个字段,任一字段命中即可 |
1.以全文检索中的match为例,语法如下:
GET /{索引库名}/_search
{
"query": {
"match": {
"字段名": "搜索条件"
}
}
}示例:

2.与match类似的还有multi_match,区别在于可以同时对多个字段搜索,而且多个字段其中之一满足即可,语法示例:
GET /{索引库名}/_search
{
"query": {
"multi_match": {
"query": "搜索条件",
"fields": ["字段1", "字段2"]
}
}
}示例:

1.2.2.精确查询(Term-level Queries)
原理:不分词,直接匹配输入值,适合 keyword、数字、日期、boolean 等字段。
精确查询,英文是Term-level query,顾名思义,词条级别的查询。也就是说不会对用户输入的搜索条件再分词,而是作为一个词条,与搜索的字段内容精确值匹配。因此推荐查找keyword、数值、日期、boolean类型的字段。例如:
- id
- price
- 城市
- 地名
- 人名
等等,作为一个整体才有含义的字段。
详情可以查看官方文档:
Term-level queries | Elasticsearch Guide [7.12] | Elastic
| 类型 | 用途说明 |
|---|---|
term | 精确匹配一个值 |
terms | 精确匹配多个值之一 |
ids | 根据 id 查询 |
range | 范围查询(适用于数字、日期) |
1.以term查询为例,其语法如下:
GET /{索引库名}/_search
{
"query": {
"term": {
"字段名": {
"value": "搜索条件"
}
}
}
}示例:

当你输入的搜索条件不是词条,而是短语时,由于不做分词,你反而搜索不到:


2.再来看下range查询,语法如下:
GET /{索引库名}/_search
{
"query": {
"range": {
"字段名": {
"gte": {最小值},
"lte": {最大值}
}
}
}
}range是范围查询,对于范围筛选的关键字有:
gte:大于等于gt:大于lte:小于等于lt:小于
示例:
再来看下range查询,语法如下:
GET /{索引库名}/_search
{
"query": {
"range": {
"字段名": {
"gte": {最小值},
"lte": {最大值}
}
}
}
}range是范围查询,对于范围筛选的关键字有:
gte:大于等于gt:大于lte:小于等于lt:小于
示例:

总结对比
| 类型 | 分词? | 精度 | 适合字段 | 示例用途 |
|---|---|---|---|---|
match | ✅ | 模糊 | text(分词字段) | 文章搜索 |
term | ❌ | 精确 | keyword、数字等 | 状态码、用户名 |
range | ❌ | 精确 | 数值、时间 | 价格、日期筛选 |
multi_match | ✅ | 模糊 | 多个 text 字段 | 多字段搜索 |
1.3.复合查询
复合查询大致分为两类:
| 类别 | 说明 | 示例 |
|---|---|---|
| 组合逻辑条件 | 用逻辑运算组合多个叶子查询 | bool |
| 控制文档相关性得分 | 修改搜索结果文档的相关性得分 _score | function_score, dis_max |
其它复合查询及相关语法可以参考官方文档:
Compound queries | Elasticsearch Guide [7.12] | Elastic
1.3.1.算分函数查询
当我们利用match查询时,文档结果会根据与搜索词条的关联度打分(_score),返回结果时按照分值降序排列。
例如,我们搜索 “手机”,结果如下:


例如:页面搜索steam第一个确实steam游戏管家
基本语法:

function score 查询中包含四部分内容:
- 原始查询条件:query部分,基于这个条件搜索文档,并且基于BM25算法给文档打分,原始算分(query score)
- 过滤条件:filter部分,符合该条件的文档才会重新算分
- 算分函数:符合filter条件的文档要根据这个函数做运算,得到的函数算分(function score),有四种函数
- weight:函数结果是常量
- field_value_factor:以文档中的某个字段值作为函数结果
- random_score:以随机数作为函数结果
- script_score:自定义算分函数算法
- 运算模式:算分函数的结果、原始查询的相关性算分,两者之间的运算方式,包括:
- multiply:相乘
- replace:用function score替换query score
- 其它,例如:sum、avg、max、min

Function Score 查询结构
{
"query": {
"function_score": {
"query": {...}, // 原始查询(计算原始得分)
"functions": [ // 算分函数数组
{
"filter": {...}, // 过滤条件,哪些文档应用函数
"weight": 10 // 算分函数结果(此处为常量)
}
],
"boost_mode": "multiply" // 原始得分 × 函数得分
}
}
}
function score的运行流程如下:
- 原始查询:用 BM25 计算
_score,称为原始得分query score - 过滤条件:只对满足条件的文档重新打分
- 算分函数:对符合条件的文档执行函数打分
function score - 得分合并:根据
boost_mode决定如何合并两个得分(如乘法、替换、求和)
因此,其中的关键点是:
- 过滤条件:决定哪些文档的算分被修改
- 算分函数:决定函数算分的算法
- 运算模式:决定最终算分结果
示例:给IPhone这个品牌的手机算分提高十倍,分析如下:
实现逻辑:
| 步骤 | 内容 |
|---|---|
| 原始查询 | 搜索所有手机 |
| 过滤条件 | brand = IPhone |
| 算分函数 | weight = 10 |
| 合并模式 | multiply(原始得分 × 10) |
对应代码如下:
GET /hotel/_search
{
"query": {
"function_score": {
"query": {
"match": {
"all": "手机"
}
},
"functions": [
{
"filter": {
"term": {
"brand": "Iphone"
}
},
"weight": 10
}
],
"boost_mode": "multiply"
}
}
}

1.3.2.bool查询
bool查询,即布尔查询。就是利用逻辑运算来组合一个或多个查询子句的组合。它的本质是:
用多种逻辑关系组合多个查询子句,控制哪些文档匹配,哪些文档得分。
bool查询支持的逻辑运算有:
- must:必须匹配每个子查询,类似“与”
- should:选择性匹配子查询,类似“或”
- must_not:必须不匹配,不参与算分,类似“非”
- filter:必须匹配,不参与算分
bool查询的语法如下:
{
"query": {
"bool": {
"must": [ // 必须匹配所有的子查询(类似 AND)
{"match": {"name": "手机"}}
],
"should": [ // 选择性匹配任意一个子查询(类似 OR)
{"term": {"brand": "vivo"}},
{"term": {"brand": "小米"}}
],
"must_not": [ // 必须不匹配(类似 NOT),**不参与算分**
{"range": {"price": {"gte": 2500}}}
],
"filter": [ // 必须匹配,但不参与算分
{"range": {"price": {"lte": 1000}}}
]
}
}
}

例如黑马商城的搜索页面:

| 类型 | 建议用法 | 是否参与算分 |
|---|---|---|
| 输入框 | must | ✅ 是 |
| 品牌选择 | filter | ❌ 否 |
| 分类选择 | filter | ❌ 否 |
| 价格区间 | filter | ❌ 否 |
| 排除高价 | must_not | ❌ 否 |
比如,我们要搜索手机,但品牌必须是华为,价格必须是900~1599,那么可以这样写:
GET /items/_search
{
"query": {
"bool": {
"must": [
{"match": {"name": "手机"}}
],
"filter": [
{"term": {"brand": { "value": "华为" }}},
{"range": {"price": {"gte": 90000, "lt": 159900}}}
]
}
}
}must中匹配关键词 “手机”filter中品牌和价格的约束,不参与算分,提升查询效率
| 关键词 | 匹配要求 | 是否参与算分 | 类比逻辑 | 典型用途 |
|---|---|---|---|---|
must | 必须匹配 | ✅ 参与 _score | AND | 主查询条件,例如关键词 |
should | 匹配任意即可 | ✅ 参与 _score | OR | 加分项、推荐项 |
must_not | 不能匹配 | ❌ 不参与算分 | NOT | 排除项,例如黑名单、价格上限 |
filter | 必须匹配 | ❌ 不参与算分 | AND | 过滤项,例如分类、价格区间 |
1.4.排序

elasticsearch默认是根据相关度算分(_score)来排序,但是也支持自定义方式对搜索结果排序。
不能对所有字段排序,比如以下类型就不能排序:
| 字段类型 | 是否支持排序 | 说明 |
|---|---|---|
text(分词) | ❌ 不支持 | 被分析器切分,不支持排序 |
keyword | ✅ 支持 | 原始不分词字符串 |
| 数值类型(int等) | ✅ 支持 | 常用于排序(价格、权重等) |
| 日期类型 | ✅ 支持 | 比如发布时间、更新时间等 |
| 地理坐标类型 | ✅ 支持 | 可用于地理距离排序(更复杂) |
详细说明可以参考官方文档:
https://www.elastic.co/guide/en/elasticsearch/reference/7.12/sort-search-results.html
语法说明:
GET /indexName/_search
{
"query": {
"match_all": {}
},
"sort": [
{
"排序字段": {
"order": "asc" 或 "desc"
}
}
]
}sort: 是一个数组,支持多字段排序(按优先级逐层排序)
order: 可以是 "asc"(升序) 或 "desc"(降序)
示例,我们按照商品价格排序:
GET /items/_search
{
"query": {
"match_all": {}
},
"sort": [
{
"price": {
"order": "desc"
}
}
]
}这个查询会把所有 items 索引中的商品按 price 字段从高到低排列。

1.5.分页
Elasticsearch 分页查询机制
elasticsearch 默认情况下只返回top10的数据。而如果要查询更多数据就需要修改分页参数了。
1.5.1.基础分页
elasticsearch中通过修改from、size参数来控制要返回的分页结果:
from:从第几个文档开始size:总共查询几个文档
类似于mysql中的limit ?, ?

官方文档如下:
https://www.elastic.co/guide/en/elasticsearch/reference/7.12/paginate-search-results.html
语法如下:
GET /items/_search
{
"query": {
"match_all": {}
},
"from": 0, // 分页开始的位置,默认为0
"size": 10, // 每页文档数量,默认10
"sort": [
{
"price": {
"order": "desc"
}
}
]
}1.5.2.深度分页
当你查询比如 第 1000 页,每页 10 条,那就是 from=9990, size=10,这就需要从每个分片中取出前 10000 条 数据,然后汇总后排序、再取 9990~10000 的部分,对内存和 CPU 开销巨大!
因此 Elasticsearch 默认限制:
from + size <= 10000,否则报错。
GET /items/_search
{
"from": 990, // 从第990条开始查询
"size": 10, // 每页查询10条
"sort": [
{
"price": "asc"
}
]
}针对深度分页,elasticsearch提供了两种解决方案:
search after:分页时需要排序,原理是从上一次的排序值开始,查询下一页数据。官方推荐使用的方式。scroll:原理将排序后的文档id形成快照,保存下来,基于快照做分页。官方已经不推荐使用。





总结:
深度分页是 Elasticsearch 的性能陷阱,分页方式要结合实际业务场景合理选用。

大多数情况下,我们采用普通分页就可以了。查看百度、京东等网站,会发现其分页都有限制。例如百度最多支持77页,每页不足20条。京东最多100页,每页最多60条。
因此,一般我们采用限制分页深度的方式即可,无需实现深度分页。
1.6.高亮
1.6.1.高亮原理
什么是高亮显示呢?

观察页面源码,你会发现两件事情:
- 高亮词条都被加了
<em>标签 <em>标签都添加了红色样式
css样式肯定是前端实现页面的时候写好的,但是前端编写页面的时候是不知道页面要展示什么数据的,不可能给数据加标签。而服务端实现搜索功能,要是有elasticsearch做分词搜索,是知道哪些词条需要高亮的。
因此词条的高亮标签肯定是由服务端提供数据的时候已经加上的。
因此实现高亮的思路就是:
- 用户输入搜索关键字
- 服务端通过 Elasticsearch 执行搜索(如
match查询) - ES 会在匹配的字段中把关键字用
<em>包裹 - 前端拿到数据后展示,并通过样式控制
<em>的表现
1.6.2.实现高亮
事实上elasticsearch已经提供了给搜索关键字加标签的语法,无需我们自己编码。
基本语法如下:
GET /{索引库名}/_search
{
"query": {
"match": {
"搜索字段": "搜索关键字"
}
},
"highlight": {
"fields": {
"高亮字段名称": {
"pre_tags": "<em>",
"post_tags": "</em>"
}
}
}
}注意:
| 条件 | 说明 |
|---|---|
| 查询类型 | 必须是 全文检索类型,如 match、multi_match |
| 字段类型 | 高亮字段必须是 text 类型(可分词) |
| 默认行为 | 只有搜索字段和高亮字段一致才会高亮 |
| 特殊设置 | 若想搜索字段和高亮字段不一致,需加 required_field_match: false |
示例:


1.7总结

返回结果示意

查询的DSL是一个大的JSON对象,包含下列属性:
query:查询条件from和size:分页条件sort:排序条件highlight:高亮条件
2.RestClient查询
这部分讲的是如何通过 Java 的 RestHighLevelClient API 实现对 Elasticsearch 的文档查询,核心是模拟 Kibana 中的 DSL 查询,但用 Java 代码方式实现。
文档的查询依然使用昨天学习的 RestHighLevelClient对象,查询的基本步骤如下:
- 1)创建
request对象,这次是搜索,所以是SearchRequest - 2)准备请求参数,也就是查询DSL对应的JSON参数
- 3)发起请求
- 4)解析响应,响应结果相对复杂,需要逐层解析

2.1.快速入门
之前说过,由于Elasticsearch对外暴露的接口都是Restful风格的接口,因此JavaAPI调用就是在发送Http请求。而我们核心要做的就是利用利用Java代码组织请求参数,解析响应结果。
这个参数的格式完全参考DSL查询语句的JSON结构,因此我们在学习的过程中,会不断的把JavaAPI与DSL语句对比。大家在学习记忆的过程中,也应该这样对比学习。
| DSL 示例 | Java API 构建 |
|---|---|
match_all 查询 | QueryBuilders.matchAllQuery() |
| 带分页的查询 | request.source().from(x).size(y) |
| 带排序的查询 | request.source().sort("field", SortOrder) |
| 高亮查询 | HighlightBuilder 构造高亮规则 |
2.1.1.发送请求
首先以match_all查询为例,其DSL和JavaAPI的对比如图:

代码解读:
- 第一步,创建
SearchRequest对象,指定索引库名 - 第二步,利用
request.source()构建DSL,DSL中可以包含查询、分页、排序、高亮等query():代表查询条件,利用QueryBuilders.matchAllQuery()构建一个match_all查询的DSL
- 第三步,利用
client.search()发送请求,得到响应
这里关键的API有两个,一个是request.source(),它构建的就是DSL中的完整JSON参数。其中包含了query、sort、from、size、highlight等所有功能:

另一个是QueryBuilders,其中包含了我们学习过的各种叶子查询、复合查询等:

2.1.2.解析响应结果
在发送请求以后,得到了响应结果SearchResponse,这个类的结构与我们在kibana中看到的响应结果JSON结构完全一致:
{
"took" : 0,
"timed_out" : false,
"hits" : {
"total" : {
"value" : 2,
"relation" : "eq"
},
"max_score" : 1.0,
"hits" : [
{
"_index" : "heima",
"_type" : "_doc",
"_id" : "1",
"_score" : 1.0,
"_source" : {
"info" : "Java讲师",
"name" : "赵云"
}
}
]
}
}因此,我们解析SearchResponse的代码就是在解析这个JSON结果,对比如下:
// 4.解析响应结果
SearchHits hits = search.getHits();
hits.forEach(e -> {
String sourceAsString = e.getSourceAsString(); ItemDoc item = JSON.parseObject(sourceAsString, ItemDoc.class);
System.out.println("item = " + item);});
代码解读:
elasticsearch返回的结果是一个JSON字符串,结构包含:
hits:命中的结果total:总条数,其中的value是具体的总条数值max_score:所有结果中得分最高的文档的相关性算分hits:搜索结果的文档数组,其中的每个文档都是一个json对象_source:文档中的原始数据,也是json对象
因此,我们解析响应结果,就是逐层解析JSON字符串,流程如下:
SearchHits:通过response.getHits()获取,就是JSON中的最外层的hits,代表命中的结果SearchHits#getTotalHits().value:获取总条数信息SearchHits#getHits():获取SearchHit数组,也就是文档数组SearchHit#getSourceAsString():获取文档结果中的_source,也就是原始的json文档数据
2.1.3.总结
文档搜索的基本步骤是:
- 创建
SearchRequest对象 - 准备
request.source(),也就是DSL。QueryBuilders来构建查询条件- 传入
request.source()的query()方法
- 发送请求,得到结果
- 解析结果(参考JSON结果,从外到内,逐层解析)
完整代码如下:
@Test
void testMatchAll() throws IOException {
// 1.创建Request
SearchRequest request = new SearchRequest("items");
// 2.组织请求参数
request.source().query(QueryBuilders.matchAllQuery());
// 3.发送请求
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
// 4.解析响应
handleResponse(response);
}
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);
System.out.println(item);
}
}小结
| 步骤 | 说明 |
|---|---|
| 创建请求 | new SearchRequest("索引名") |
| 构造查询 | request.source().query(...) |
| 发送请求 | client.search(request, ...) |
| 解析结果 | 从 SearchHits 提取每个文档 _source |

接下来讲解如何使用 Java API 实现 Elasticsearch 中的两种查询方式:叶子查询 和 复合查询,核心是通过 QueryBuilders 来构建查询条件,然后发送请求获取结果。
2.2.叶子查询
叶子查询是最基本的查询方式,它直接作用于某个字段的值,不涉及多层组合。

所有的查询条件都是由QueryBuilders来构建的,叶子查询也不例外。因此整套代码中变化的部分仅仅是query条件构造的方式,其它不动。
例如match查询:
@Test
void testMatch() throws IOException {
// 1.创建Request
SearchRequest request = new SearchRequest("items");
// 2.组织请求参数
request.source().query(QueryBuilders.matchQuery("name", "脱脂牛奶"));
// 3.发送请求
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
// 4.解析响应
handleResponse(response);
}
再比如multi_match查询:
@Test
void testMultiMatch() throws IOException {
// 1.创建Request
SearchRequest request = new SearchRequest("items");
// 2.组织请求参数
request.source().query(QueryBuilders.multiMatchQuery("脱脂牛奶", "name", "category"));
// 3.发送请求
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
// 4.解析响应
handleResponse(response);
}
还有range查询:
@Test
void testRange() throws IOException {
// 1.创建Request
SearchRequest request = new SearchRequest("items");
// 2.组织请求参数
request.source().query(QueryBuilders.rangeQuery("price").gte(10000).lte(30000));
// 3.发送请求
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
// 4.解析响应
handleResponse(response);
}
还有term查询:
@Test
void testTerm() throws IOException {
// 1.创建Request
SearchRequest request = new SearchRequest("items");
// 2.组织请求参数
request.source().query(QueryBuilders.termQuery("brand", "华为"));
// 3.发送请求
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
// 4.解析响应
handleResponse(response);
}
2.3.复合查询
复合查询是将多个查询组合到一起的方式,常用 bool、function_score 等。
复合查询也是由QueryBuilders来构建
算分的代码示例:
@Test
void testFunctionScore() throws IOException {
// 1. 创建 Request 对象
SearchRequest request = new SearchRequest("items");
// 2. 构建 Function Score 查询
// 2.1 创建Function Score查询
FunctionScoreQueryBuilder functionScoreQuery = QueryBuilders.functionScoreQuery( QueryBuilders.matchQuery("name", "手机"),
// 基础查询
new FunctionScoreQueryBuilder.FilterFunctionBuilder[]{
// 添加过滤条件函数:品牌为华为时权重x10
new FunctionScoreQueryBuilder.FilterFunctionBuilder( QueryBuilders.termQuery("brand", "华为"),
// 过滤条件
ScoreFunctionBuilders.weightFactorFunction(10) // 权重 ) } ).boostMode(CombineFunction.MULTIPLY);
// 分数计算模式
// 2.2 将查询加入请求
request.source().query(functionScoreQuery);
// 3. 发送请求
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
// 4. 解析响应
handleResponse(response);}
以bool查询为例,DSL和JavaAPI的对比如图:

完整代码如下:
@Test
void 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);
}
| 查询方式 | QueryBuilders 示例 | 特点 |
|---|---|---|
| matchQuery | matchQuery("name", "牛奶") | 分词模糊匹配 |
| multiMatch | multiMatchQuery("牛奶", "name", "category") | 多字段模糊匹配 |
| termQuery | termQuery("brand", "华为") | 精确匹配,适用于 keyword |
| rangeQuery | rangeQuery("price").gte(100).lte(300) | 数值/时间范围匹配 |
| boolQuery | must + filter 等组合查询 | 多条件组合 |
| functionScore | functionScoreQuery(...).boostMode(...) | 加权评分 |
接下来讲解如何通过 Java API 实现 Elasticsearch 的 排序、分页和高亮查询
2.4.排序和分页

之前说过,requeset.source()就是整个请求JSON参数,所以排序、分页都是基于这个来设置,其DSL和JavaAPI的对比如下:

完整示例代码:
@Test
void testPageAndSort() throws IOException {
int pageNo = 1, pageSize = 5;
// 1.创建Request
SearchRequest request = new SearchRequest("items");
// 2.组织请求参数
// 2.1.搜索条件参数
request.source().query(QueryBuilders.matchQuery("name", "脱脂牛奶"));
// 2.2.排序参数
request.source().sort("price", SortOrder.ASC);
// 2.3.分页参数
request.source().from((pageNo - 1) * pageSize).size(pageSize);
// 3.发送请求
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
// 4.解析响应
handleResponse(response);
}2.5.高亮

高亮查询与前面的查询有两点不同:
- 条件同样是在
request.source()中指定,只不过高亮条件要基于HighlightBuilder来构造 - 高亮响应结果与搜索的文档结果不在一起,需要单独解析
首先来看高亮条件构造,其DSL和JavaAPI的对比如图:

示例代码如下:
@Test
void testHighlight() throws IOException {
// 1.创建Request
SearchRequest request = new SearchRequest("items");
// 2.组织请求参数
// 2.1.query条件
request.source().query(QueryBuilders.matchQuery("name", "脱脂牛奶"));
// 2.2.高亮条件
request.source().highlighter(
SearchSourceBuilder.highlight()
.field("name")// 设置高亮字段
.preTags("<em>") // 高亮前缀
.postTags("</em>") // 高亮后缀
);
// 3.发送请求
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
// 4.解析响应
handleResponse(response);
}再来看结果解析,文档解析的部分不变,主要是高亮内容需要单独解析出来,其DSL和JavaAPI的对比如图:

代码解读:
- 第
3、4步:从结果中获取_source。hit.getSourceAsString(),这部分是非高亮结果,json字符串。还需要反序列为ItemDoc对象 - 第
5步:获取高亮结果。hit.getHighlightFields(),返回值是一个Map,key是高亮字段名称,值是HighlightField对象,代表高亮值 - 第
5.1步:从Map中根据高亮字段名称,获取高亮字段值对象HighlightField - 第
5.2步:从HighlightField中获取Fragments,并且转为字符串。这部分就是真正的高亮字符串了 - 最后:用高亮的结果替换
ItemDoc中的非高亮结果
完整代码如下:
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<String, HighlightField> 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);
}
}| 功能 | JavaAPI写法示例 | 说明 |
|---|---|---|
| 分页 | from(x).size(y) | from 是起始偏移,size 是每页大小 |
| 排序 | .sort("字段", SortOrder.ASC/DESC) | 指定排序字段及顺序 |
| 高亮 | .highlighter(SearchSourceBuilder.highlight()...) | 高亮字段需独立解析并替换 |
| 高亮结果获取 | getHighlightFields().get("字段").getFragments() | 取出高亮片段并替换进原始字段 |
3.数据聚合
这部分内容讲解的是 Elasticsearch 中的数据聚合(Aggregations),它是 ES 非常强大的功能之一,允许我们像写 SQL 的 GROUP BY 一样,对数据做分组、统计、计算,非常适合做报表分析和数据洞察。
聚合(aggregations)可以让我们极其方便的实现对数据的统计、分析、运算。例如:
- 什么品牌的手机最受欢迎?
- 这些手机的平均价格、最高价格、最低价格?
- 这些手机每月的销售情况如何?
实现这些统计功能的比数据库的sql要方便的多,而且查询速度非常快,可以实现近实时搜索效果。
官方文档:
https://www.elastic.co/guide/en/elasticsearch/reference/7.12/search-aggregations.html
聚合常见的有三类:
- 桶(
Bucket)聚合:用来对文档做分组TermAggregation:按照文档字段值分组,例如按照品牌值分组、按照国家分组Date Histogram:按照日期阶梯分组,例如一周为一组,或者一月为一组
- 度量(
Metric)聚合:用以计算一些值,比如:最大值、最小值、平均值等Avg:求平均值Max:求最大值Min:求最小值Stats:同时求max、min、avg、sum等
- 管道(
pipeline)聚合:其它聚合的结果为基础做进一步运算
聚合的三大类型
| 聚合类型 | 英文名 | 作用 | 示例 |
|---|---|---|---|
| 桶聚合 | Bucket | 分组 | 按品牌分类、按日期分组 |
| 度量聚合 | Metric | 计算 | 最大价格、平均价格 |
| 管道聚合 | Pipeline | 基于其它聚合结果再计算 | 最大值差值、百分比变化 |
注意:参加聚合的字段必须是keyword、日期、数值、布尔类型
3.1.DSL实现聚合
与之前的搜索功能类似,我们依然先学习DSL的语法,再学习JavaAPI.
3.1.1.Bucket聚合
例如我们要统计所有商品中共有哪些商品分类,其实就是以分类(category)字段对数据分组。category值一样的放在同一组,属于Bucket聚合中的Term聚合。
基本语法如下:
GET /items/_search
{
"size": 0,
"aggs": {
"category_agg": {
"terms": {
"field": "category",
"size": 20
}
}
}
}语法说明:
size:设置size为0,就是每页查0条,则结果中就不包含文档,只包含聚合aggs:定义聚合category_agg:聚合名称,自定义,但不能重复terms:聚合的类型,按分类聚合,所以用termfield:按照哪个字段聚合(如category)size:希望返回的聚合结果的最大数量(最多返回多少个桶)
来看下查询的结果:

3.1.2.带条件聚合
例子:统计价格 > 3000 的手机品牌有哪些
默认情况下,Bucket聚合是对索引库的所有文档做聚合,例如我们统计商品中所有的品牌,结果如下:

可以看到统计出的品牌非常多。
但真实场景下,我想知道价格高于3000元的手机品牌有哪些,该怎么统计呢?
我们需要从需求中分析出搜索查询的条件和聚合的目标:
- 搜索查询条件:
- 价格高于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
}
}
}
}❗️注意:query 用于限定参与聚合的文档范围。
聚合结果如下:
{
"took" : 2,
"timed_out" : false,
"hits" : {
"total" : {
"value" : 13,
"relation" : "eq"
},
"max_score" : null,
"hits" : [ ]
},
"aggregations" : {
"brand_agg" : {
"doc_count_error_upper_bound" : 0,
"sum_other_doc_count" : 0,
"buckets" : [
{
"key" : "华为",
"doc_count" : 7
},
{
"key" : "Apple",
"doc_count" : 5
},
{
"key" : "小米",
"doc_count" : 1
}
]
}
}
}
可以看到,结果中只剩下3个品牌了。
3.1.3.Metric聚合
例子:每个品牌的手机价格最大值、最小值、平均值
这就要用到Metric聚合了,例如stats聚合,就可以同时获取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"
}
}
}
}
}
}brand_agg是外层的 分组(桶聚合)stats_metric是对每个桶里的文档做 统计(度量聚合)
结果会返回每个品牌下的最小值、最大值、平均值、总和、计数。
可以看到我们在brand_agg聚合的内部,我们新加了一个aggs参数。这个聚合就是brand_agg的子聚合,会对brand_agg形成的每个桶中的文档分别统计。
stats_meric:聚合名称stats:聚合类型,stats是metric聚合的一种field:聚合字段,这里选择price,统计价格
由于stats是对brand_agg形成的每个品牌桶内文档分别做统计,因此每个品牌都会统计出自己的价格最小、最大、平均值。
结果如下:

另外,我们还可以让聚合按照每个品牌的价格平均值排序:


聚合使用注意点
| 项目 | 说明 |
|---|---|
| aggs | 聚合顶层关键字,和 query 同级 |
| 必须字段 | 聚合名称、聚合类型、聚合字段 |
| 类型限制 | 只能用 keyword、数值、布尔、日期字段 |
| 子聚合 | 可用于桶中进一步聚合,比如 桶 -> 度量 |
3.1.4.总结
aggs代表聚合,与query同级,此时query的作用是?
- 限定聚合的的文档范围
聚合必须的三要素:
- 聚合名称
- 聚合类型
- 聚合字段
聚合可配置属性有:
- size:指定聚合结果数量(聚合结果有多个桶,size可以选择保留多少个桶)
- order:指定聚合结果排序方式
- field:指定聚合字段

3.2.RestClient实现聚合
可以看到在DSL中,aggs聚合条件与query条件是同一级别,都属于查询JSON参数。因此依然是利用request.source()方法来设置。

不过聚合条件的要利用AggregationBuilders这个工具类来构造。DSL与JavaAPI的语法对比如下:

聚合结果与搜索文档同一级别,因此需要单独获取和解析。具体解析语法如下:

完整代码如下:
@Test
void testAgg() throws IOException {
// 1. 创建请求对象,指定索引
SearchRequest request = new SearchRequest("items");
// 2. 构建 bool 查询条件(只查询“手机”且价格大于等于 300000)
BoolQueryBuilder bool = QueryBuilders.boolQuery()
.filter(QueryBuilders.termQuery("category", "手机"))
.filter(QueryBuilders.rangeQuery("price").gte(300000));
request.source().query(bool).size(0); // 不需要文档内容,只要聚合结果
// 3. 添加聚合条件:按 brand 字段分组聚合,取前5个
request.source().aggregation(
AggregationBuilders.terms("brand_agg").field("brand").size(5)
);
// 4. 执行查询
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
// 5. 获取聚合结果
Aggregations aggregations = response.getAggregations();
Terms brandTerms = aggregations.get("brand_agg"); // 根据名称取聚合结果
// 6. 遍历每个桶,获取 brand 值和文档数
for (Terms.Bucket bucket : brandTerms.getBuckets()) {
String brand = bucket.getKeyAsString(); // 分组 key
long count = bucket.getDocCount(); // 每组文档数
System.out.println("brand = " + brand + "; count = " + count);
}
}










