
欢迎来到预见猿份,本站项目均为站长原创,学习中有问题可直接提交给站长老苗解决(微信:mrt_0607)。
苗润土老师,20余年一线项目经验,2014年加入黑马,星辰wms、云岚到家、学成在线项目作者,历任高级讲师、教学主管及课程研究员。 b站老苗
day06 Elasticsearch入门
1 学习目标
- 能够说出什么是倒排索引
- 能够说出我们用的中文分词器是什么
- 能够说出IK分词器如何扩展词典
- 能够使用Java Client向索引增删改查文档
- 能够使用Java Client向索引批量导入文档
- 能够使用Java Client进行Term查询
- 能够使用Java Client进行全文检索
- 能够使用Java Client实现排序和分页
- 能够使用Java Client实现布尔查询
2.索引操作
Mapping映射就类似表的结构。我们要向es中存储数据,必须先创建Index和Mapping
2.1 Mapping映射属性
在MySQL中创建表结构时需要指定每个字段的类型,同样,创建索引的映射也需要指定每个字段的类型及其它属性。
常见的Mapping属性包括:
type:字段数据类型,常见的简单类型有:- 字符串:
text(可分词的文本) keyword(精确值,例如:品牌、国家、ip地址),keyword类型主要用于存储不需要分词处理的字符串,例如电子邮件地址、标签、ID 等,这些字符串通常用于精确匹配搜索。- 数值:
long、integer、short、byte、double、float、 - 布尔:
boolean - 日期:
date - 对象:
object
- 字符串:
index:是否索引
index为true时可对此字段搜索,并且如果type为text则会对文本内容进行分词
index为false表示不分词也不能搜索。
analyzer:添加索引时使用哪种分词器分词properties:该字段的子字段- search_analyzer: 搜索时使用哪种分词器分词
通常情况下,我们在搜索和创建索引时使用的是同一分析器,默认情况下搜索将会使用字段映射时定义的分析器,也能通过search_analyzer 设置不同的分词器。
拿ik分词器举例:
通常会设置如下:
"analyzer": "ik_max_word",
"search_analyzer":"ik_smart"添加索引时分词使用细粒度模式ik_max_word ,因为ik_max_word 会尽可能多地分出词条,这对于分词创建索引是有益的,因为它可以增加索引中的词条数量
搜索时使用智能模式ik_smart 因为ik_smart 分词器会更加注重分词的准确性,减少不必要的词条,以提高搜索的精度。
比如:搜索“中华人民共和国”,如果搜索时采用ik_max_word模式,会将“中华人民共和国”分为中华、人民、华人等词语,此时就会拿这些词去匹配文档,搜索出来的文章可能并不是用户想要的结果,用户想要的结果是“中华人民共和国”相关的文章 。
如果搜索时使用ik_smart模式,此时“中华人民共和国”分词为“中华人民共和国”,此时去搜索出的文档正是用户想要的。
2.2 创建索引
基本语法:
- 请求方式:
PUT - 请求路径:
/索引名,可以自定义 - 请求参数:
mapping映射
格式:
PUT /索引名称
{
"mappings": {
"properties": {
"字段名":{
"type": "text",
"analyzer": "ik_smart"
},
"字段名2":{
"type": "keyword",
"index": "false"
},
"字段名3":{
"properties": {
"子字段": {
"type": "keyword"
}
}
},
// ...略
}
}
}举例:
例如下面的json文档:
{
"age": 21,
"weight": 52.1,
"isMarried": false,
"info": "黑马程序员Java讲师",
"email": "zy@itcast.cn",
"score": [99.1, 99.5, 98.9],
"name": {
"firstName": "云",`
"lastName": "赵"
}
}对应的每个字段映射(Mapping):
| 字段名 | 字段类型 | 类型说明 | 是否 参与搜索 | 是否 参与分词 | 分词器 | |
|---|---|---|---|---|---|---|
| age | integer | 整数 | —— | |||
| weight | float | 浮点数 | —— | |||
| isMarried | boolean | 布尔 | —— | |||
| info | text | 字符串,但需要分词 | IK | |||
keyword | 字符串,但是不分词 | —— | ||||
| score | float | 只看数组中元素类型 | —— | |||
| name | firstName | keyword | 字符串,但是不分词 | —— | ||
| lastName | keyword | 字符串,但是不分词 | —— | |||
AI:根据下边文档内容生成创建Elasticsearch映射语句:
elasticsearch的文档如下:
{
"age": 21,
"weight": 52.1,
"isMarried": false,
"info": "黑马程序员Java讲师",
"email": "zy@itcast.cn",
"score": [99.1, 99.5, 98.9],
"name": {
"firstName": "云",
"lastName": "赵"
}
}根据生成结果再对照所学知识进行微调。
正确的语句如下:
PUT /heima
{
"mappings": {
"properties": {
"age": {
"type": "integer"
},
"weight": {
"type": "float"
},
"isMarried": {
"type": "boolean"
},
"info": {
"type": "text",
"analyzer": "ik_max_word",
"search_analyzer":"ik_smart"
},
"email": {
"type": "keyword",
"index": false // 不对email字段进行索引,既不分词也不搜索
},
"score": {
"type": "float"
},
"name": {
"properties": {
"firstName": {
"type": "keyword"
},
"lastName": {
"type": "keyword"
}
}
}
}
}
}info字段说明:
"analyzer": "ik_max_word":表示索引时用细粒度分词,尽可能多的分多个词条
"search_analyzer":"ik_smart":表示搜索时用智能模式(粗粒度)
执行上边的语句如果报:index [heima/fQsg0fUbTfyUk3L-c6in0w] already exists
说明heima 索引已存在,需要先删除再创建。
执行:DELETE /heima 删除heima索引
创建成功返回下边的结果
{
"acknowledged" : true,
"shards_acknowledged" : true,
"index" : "heima"
}注意:如果在虚拟机中创建时报错resource_already_exists_exception,则需要先删除此索引。
执行:DELETE /heima
2.3 查询索引
基本语法:
- 请求方式:GET
- 请求路径:/索引名
- 请求参数:无
格式:
GET /索引名示例:
GET /heima2.4 修改索引
倒排索引结构虽然不复杂,但是一旦数据结构改变(比如改变了分词器),就需要重新创建倒排索引,这简直是灾难。因此索引一旦创建,无法修改mapping。
虽然无法修改mapping中已有的字段,但是却允许添加新的字段到mapping中,因为不会对倒排索引产生影响。因此修改索引能做的就是向索引中添加新字段,或者更新索引的基础属性。
语法说明:
PUT /索引名/_mapping
{
"properties": {
"新字段名":{
"type": "integer"
}
}
}示例:
添加新字段age
PUT /heima/_mapping
{
"properties": {
"age":{
"type": "integer"
}
}
}2.5 删除索引
语法:
- 请求方式:DELETE
- 请求路径:/索引名
- 请求参数:无
格式:
DELETE /索引名示例:
DELETE /heima2.6 总结
索引操作有哪些?
- 创建索引:PUT /索引名
- 查询索引:GET /索引名
- 删除索引:DELETE /索引名
- 修改索引,添加字段:PUT /索引名/_mapping
可以看到,对索引的操作基本遵循的Restful的风格,因此API接口非常统一,方便记忆。
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": "赵"
}
}响应:

3.2 查询文档
根据rest风格,新增是post,查询应该是get,不过查询一般都需要条件,这里我们把文档id带上。
语法:
GET /{索引名称}/_doc/{id}示例:
GET /heima/_doc/1查看结果:

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为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"} }其中:
index代表全量修改_index:指定索引名_id指定要操作的文档id{ "field1" : "value1" }:则是要新增的文档内容
create代表新增加,如果文档已存在则报错
_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 _bulk
4 Java Client
4.1. 配置Java client
前边我们都是在Kibana中使用DSL语法直接请求Elasticsearch的HTTP 接口进行测试,DSL是Elasticsearch的领域特定语言(Domain Specific Language, DSL)是一种基于JSON的查询语言。
在项目开发中为了提高开发效率ES官方提供了各种不同语言的客户端,这些客户端的本质就是组装DSL语句,通过http请求发送给ES。
官方文档地址:
https://www.elastic.co/guide/en/elasticsearch/client/index.html
由于ES目前最新版本是8.x,提供了全新版本的客户端Java Client,老版本的客户端Java REST Client已经被标记为过时。

我们使用的是7.17.x版本,新版本和老版本都支持,将来的版本会全面抛弃老版本的Java REST Client ,所以本教程使用新版本的Java Client。
Java Client要求:
- Java 8 或更高版本。
- JSON 对象映射库,可将您的应用程序类与 Elasticsearch API 无缝集成。Java 客户端支持Jackson或 JSON-B库(如 Eclipse Yasson)。
如何集成Java Client,可参考文档,地址在:https://www.elastic.co/guide/en/elasticsearch/client/java-api-client/7.17/installation.html
在hmall-parent中添加依赖管理:
<properties>
<es.version>7.17.7</es.version>
<jackson.version>2.13.0</jackson.version>
<jakarta.json-ai.version>2.0.1</jakarta.json-ai.version>
</properties>
<!-- 对依赖包进行管理 -->
<dependencyManagement>
<dependencies>
<!--es-->
<dependency>
<groupId>co.elastic.clients</groupId>
<artifactId>elasticsearch-java</artifactId>
<version>${es.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>${jackson.version}</version>
</dependency>
<dependency>
<groupId>jakarta.json</groupId>
<artifactId>jakarta.json-api</artifactId>
<version>${jakarta.json-ai.version}</version>
</dependency>
</dependencies>
</dependencyManagement>在hmall-item添加如下依赖:
<dependency>
<groupId>co.elastic.clients</groupId>
<artifactId>elasticsearch-java</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<dependency>
<groupId>jakarta.json</groupId>
<artifactId>jakarta.json-api</artifactId>
</dependency>这里为了单元测试方便,我们创建一个测试类IndexTest,然后将初始化的代码编写在@BeforeEach方法中:
参考:https://www.elastic.co/guide/en/elasticsearch/client/java-api-client/7.17/connecting.html
package com.hmall.item.es;
import co.elastic.clients.elasticsearch.ElasticsearchClient;
import co.elastic.clients.json.jackson.JacksonJsonpMapper;
import co.elastic.clients.transport.ElasticsearchTransport;
import co.elastic.clients.transport.rest_client.RestClientTransport;
import org.apache.http.HttpHost;
import org.elasticsearch.client.RestClient;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.io.IOException;
/**
* @author Mr.M
* @version 1.0
* @description 使用Java Client 操作ES
* @date 2024/8/18 17:40
*/
public class IndexTest {
private ElasticsearchClient esClient;
private RestClient restClient;
@BeforeEach
void setUp() {
// Create the low-level client
this.restClient = RestClient.builder(
new HttpHost("192.168.101.68", 9200)).build();
// Create the transport with a Jackson mapper
ElasticsearchTransport transport = new RestClientTransport(
restClient, new JacksonJsonpMapper());
// And create the API client
this.esClient = new ElasticsearchClient(transport);
}
@AfterEach
void tearDown() throws IOException {
this.restClient.close();
}
}4.2. 创建索引
下边我们以商城项目为例,使用Java Client维护索引数据。
搜索页面的效果如图所示:

最终我们使用Elaticsearch实现搜索接口。
4.2.1 分析索引的映射
首先我们需要创建索引,配置映射,首先针对上图去分析映射结构,包括哪些字段以及字段的类型等属性。
实现搜索功能需要的字段包括三大部分:
搜索关键字字段:
- 商品名称
过滤字段
- 分类
- 品牌
- 价格
排序字段
- 默认:按照更新时间降序排序
- 销量
- 价格
展示字段
- 商品id:用于点击后跳转
- 图片地址
- 是否是广告推广商品
- 名称
- 价格
- 评价数量
- 销量
对应的商品表结构如下,索引库无关字段已经划掉:

结合数据库表结构,以上字段对应的mapping映射属性如下:
| 字段名 | 字段类型 | 类型说明 | 是否 参与搜索 | 是否 参与分词 | 分词器 | |
| id | long | 长整数 | —— | |||
| name | text | 字符串,参与分词搜索 | IK | |||
| price | integer | 以分为单位,所以是整数 | —— | |||
| stock | integer | 字符串,但是不分词 | —— | |||
| image | keyword | 字符串,但是不分词 | —— | |||
| category | keyword | 字符串,但是不分词 | —— | |||
| brand | keyword | 字符串,但是不分词 | —— | |||
| sold | integer | 销量,整数 | —— | |||
| commentCount | integer | 评价,整数 | —— | |||
| isAD | boolean | 布尔类型 | —— | |||
| updateTime | Date | 更新时间 | —— | |||
4.2.2 创建索引
根据分析我们使用下边的语句创建items索引:
PUT /items
{
"mappings": {
"properties": {
"id": {
"type": "keyword"
},
"name":{
"type": "text",
"analyzer": "ik_max_word",
"search_analyzer": "ik_smart"
},
"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"
}
}
}
}我们为什么不用Java Client去创建索引呢?
这就好比在MySQL中创建表,通常我们使用DDL语句通过MySQL客户端执行,而对于数据的CRUD及复杂的SQL语句我们会通过jdbc 去访问mysql数据库一样。
所以,使用Elasticsearch通常我们使用Kibana通过DSL语句去创建索引,而不会使用Java Client去创建索引,使用Java Client主要是为了向索引中添加文档、从索引中搜索文档。
4.3. 新增文档
接下来我们使用Java Client向索引中添加文档。
Java Client的使用方法可以参考文档学习:
https://www.elastic.co/guide/en/elasticsearch/client/java-api-client/7.17/indexing.html
4.3.1 创建模型类
就和使用MyBatis一样操作数据库需要一个模型对象,使用Elasticsearch向索引执行CRUD操作也需要模型类。
依据索引映射创建模型类:
小技巧:创建模型类可以提供索引映射由AI去生成模型类。
package com.hmall.item.domain.po;
import co.elastic.clients.elasticsearch._types.mapping.FieldType;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.time.LocalDateTime;
/**
* @author Mr.M
* @version 1.0
* @description Elasticsearch模型类
* @date 2024/8/18 20:26
*/
@Data
@ApiModel(description = "索引库实体")
public class ItemDoc {
@ApiModelProperty("商品id")
private String id;
@ApiModelProperty("商品名称")
private String name;
@ApiModelProperty("价格(分)")
private Integer price;
@ApiModelProperty("库存")
private Integer stock;
@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;
}4.3.2 编写客户端代码
下边参考ES文档编写客户端代码。
@SpringBootTest
@Slf4j
public class IndexTest {
...
@Autowired
private IItemService itemService;
@Test
void testAddDocument() throws IOException {
//商品id
Long id = 100002644680L;
// 1.根据id查询商品数据
Item item = itemService.getById(id);
// 2.转换为文档类型
ItemDoc itemDoc = BeanUtil.copyProperties(item, ItemDoc.class);
IndexResponse response = esClient.index(i -> i
.index("items")//指定索引名称
.id(itemDoc.getId())//指定主键
.document(itemDoc)//指定文档对象
);
//结果
String s = response.result().jsonValue();
log.info("result:"+s);
}
}4.3.3 测试
下边进行运行测试方法。
报错:
Caused by: com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Java 8 date/time type `java.time.LocalDateTime` not supported by default: add Module "com.fasterxml.jackson.datatype:jackson-datatype-jsr310" to enable handling (through reference chain: com.hmall.item.domain.po.ItemDoc["updateTime"])根据提示猜测是数据绑定出问题,现在是要把Java 对象的信息映射为ES的索引文档,jackson-datatype-jsr310对于LocalDateTime不支持。
使用AI解决:
elasticsearch使用的是7.17.7,使用co.elastic.clients.elasticsearch.ElasticsearchClient 向索引新增文档报错如下:
Caused by: com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Java 8 date/time type `java.time.LocalDateTime` not supported by default: add Module "com.fasterxml.jackson.datatype:jackson-datatype-jsr310" to enable handling (through reference chain: com.hmall.item.domain.po.ItemDoc["updateTime"])根据AI提示修改如下:
添加 JavaTimeModule 以支持 LocalDateTime 类型。

完整代码如下:
package com.hmall.item.es;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.date.DatePattern;
import cn.hutool.json.JSONUtil;
import co.elastic.clients.elasticsearch.ElasticsearchClient;
import co.elastic.clients.elasticsearch._types.AcknowledgedResponse;
import co.elastic.clients.elasticsearch.core.IndexResponse;
import co.elastic.clients.elasticsearch.core.InfoRequest;
import co.elastic.clients.elasticsearch.indices.CreateIndexResponse;
import co.elastic.clients.elasticsearch.indices.PutMappingResponse;
import co.elastic.clients.json.jackson.JacksonJsonpMapper;
import co.elastic.clients.transport.ElasticsearchTransport;
import co.elastic.clients.transport.rest_client.RestClientTransport;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.PropertyNamingStrategies;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import com.hmall.item.domain.po.Item;
import com.hmall.item.domain.po.ItemDoc;
import com.hmall.item.service.IItemService;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.HttpHost;
import org.elasticsearch.client.RestClient;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.io.IOException;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
/**
* @author Mr.M
* @version 1.0
* @description 使用Java Client 操作ES
* @date 2024/8/18 17:40
*/
@SpringBootTest
@Slf4j
public class IndexTest {
private ElasticsearchClient esClient;
private RestClient restClient;
@Autowired
private IItemService itemService;
@BeforeEach
void setUp() {
// Create the low-level client
this.restClient = RestClient.builder(
new HttpHost("192.168.101.68", 9200)).build();
// 创建 ObjectMapper 实例
ObjectMapper objectMapper = new ObjectMapper();
// 添加 JavaTimeModule 以支持 LocalDateTime 类型
objectMapper.registerModule(new JavaTimeModule());
// Create the transport with a Jackson mapper
ElasticsearchTransport transport = new RestClientTransport(
restClient, new JacksonJsonpMapper(objectMapper));
// And create the API client
this.esClient = new ElasticsearchClient(transport);
}
//创建文档
@Test
void testAddDocument() throws IOException {
// 1.根据id查询商品数据
Item item = itemService.getById(100002644680L);
// 2.转换为文档类型
ItemDoc itemDoc = BeanUtil.copyProperties(item, ItemDoc.class);
IndexResponse response = esClient.index(i -> i
.index("items")//指定索引名称
.id(itemDoc.getId())//指定主键
.document(itemDoc)//指定文档对象
);
//结果
String s = response.result().jsonValue();
log.info("result:"+s);
}
@AfterEach
void tearDown() throws IOException {
this.restClient.close();
}
}执行成功进行验证
我们使用DSL查询:
GET /items/_search结果:

确定ES的索引中存在刚才添加的文档。
4.4 查询文档
参考文档:https://www.elastic.co/guide/en/elasticsearch/client/java-api-client/7.17/reading.html
通过阅读文档可知,这里实现的是根据ID查询文档。
编写代码:
@Test
void testGetDocumentById() throws IOException {
GetResponse<ItemDoc> response = esClient.get(g -> g
.index("items")
.id("100002644680"),
ItemDoc.class
);
if (response.found()) {
ItemDoc itemDoc = response.source();
log.info("itemDoc: " + itemDoc);
} else {
log.info ("itemDoc not found");
}
}4.5 删除文档
通过两个API方法的学习:
新增文档:esClient.index()方法
查询文档:esClient.get()方法
对于删除文档的方式可以根据代码提示自行编写,如下:
@Test
void testDeleteDocumentById() throws IOException {
DeleteResponse response = esClient.delete(d -> d
.index("items")
.id("100002644680")
);
String s = response.result().jsonValue();
log.info("result:"+s);
}4.6 修改文档
- 局部修改
如果要更新的文档不存在会报错。
@Test
void testUpdateDocumentById() throws IOException {
//更新对象
ItemDoc itemDoc = new ItemDoc();
itemDoc.setName("更新名称");
UpdateResponse<ItemDoc> response = esClient.update(u -> u
.index("items")
.id("100002644680")
.doc(itemDoc), ItemDoc.class);
String s = response.result().jsonValue();
log.info("result:"+s);
}- 有则更新,没有则添加。
@Test
void testUpdateDocumentById2() throws IOException {
//更新对象
ItemDoc itemDoc = new ItemDoc();
itemDoc.setName("更新名称");
UpdateResponse<ItemDoc> response = esClient.update(u -> u
.index("items")
.id("100002644680aa")
.doc(itemDoc)
.docAsUpsert(true), ItemDoc.class);
String s = response.result().jsonValue();
log.info("result:"+s);
}通过docAsUpsert(true)控制,如果没有该文档则添加新文档。
4.7. 批量导入
文档地址:
测试代码如下:
@Test
void testBatchAddDocment() throws Exception {
//取第一页10条数据
Page<Item> page = Page.of(0, 10);
//查询所有商品信息
Page<Item> itemPage = itemService.page(page, new LambdaQueryWrapper<Item>());
//获取商品集合
List<Item> items = itemPage.getRecords();
//拷贝属性
List<ItemDoc> itemDocs = BeanUtils.copyList(items, ItemDoc.class);
//批量添加请求
BulkRequest.Builder br = new BulkRequest.Builder();
itemDocs.forEach(itemDoc ->
br.operations(op -> op
.index(i -> i
.index("items")
.id(itemDoc.getId().toString())
.document(itemDoc))));
//构建请求
BulkRequest build = br.build();
//批量添加
BulkResponse bulkResponse = esClient.bulk(build);
//遍历结果
bulkResponse.items().forEach(item -> log.info("添加结果:{}",item.result().toString()));
//如果有错误
if(bulkResponse.errors()){
log.error("批量添加失败");
//遍历错误
bulkResponse.items().forEach(item -> log.error("添加失败:{}",item.error().reason()));
}
}5 搜索
5.1 搜索入门
5.1.1 介绍
最终我们使用Elasticsearch实现搜索功能,现在已经将文档添加到了索引中,接下来学习搜索的方法。
首先我们学习DSL搜索方式,参考DSL搜索语法再学习Java Client方式,最终在微服务中使用Java Client方式完成搜索接口的开发。
Elasticsearch的查询可以分为两大类:
文档参考:
精确查询:根据精确词条值查找数据,一般是查找keyword、数值、日期、boolean等类型字段。例如:
- ids: 根据文档 ID 查找文档
- range:返回包含指定范围内的文档,比如:查询年龄在10到20岁的学生信息。
- term: 根据精确值(例如价格、产品 ID 或用户名)查找文档。
全文检索(full text)查询:利用分词器对用户输入内容分词,然后去倒排索引库中匹配。例如:
- match_query:对一个字段进行全文检索
- multi_match_query:对多个字段进行全文检索
- 复合查询(Compound query clauses):以逻辑方式组合多个叶子查询或者更改叶子查询的行为方式。
第一类:基于逻辑运算组合叶子查询,实现组合条件,例如
- bool:实现组合条件查询,项目使用较多。
第二类:基于某种算法修改查询时的文档相关性算分,从而改变文档排名。例如:
- function_score:通过条件,指定算分函数,控制文档得分,得分越高排名越靠前,比如:百度竞价排名。
- dis_max:从多个查询中选择得分最高的结果。
其它复合查询及相关语法可以参考官方文档:
5.1.2 精确查询
精确查询,英文是Term-level query,顾名思义,词条级别的查询。也就是说不会对用户输入的搜索条件再分词,而是作为一个词条,与搜索的字段内容精确值匹配。因此推荐查找keyword、数值、日期、boolean类型的字段。例如:
- id
- price
- 城市
- 地名
- 人名
等等,作为一个整体才有含义的字段。
详情可以查看官方文档:
5.1.2.1 term
以term查询为例,其语法如下:
GET /{索引库名}/_search
{
"query": {
"term": {
"字段名": {
"value": "搜索条件"
}
}
}
}示例:

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

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

5.1.3 全文检索
全文检索的种类也很多,详情可以参考官方文档:
5.1.3.1 match
以全文检索中的match为例,语法如下:
GET /{索引库名}/_search
{
"query": {
"match": {
"字段名": "搜索条件"
}
}
}示例:

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

5.1.4 排序
elasticsearch默认是根据相关度算分(_score)来排序,但是也支持自定义方式对搜索结果排序。不过分词字段无法排序,能参与排序字段类型有:keyword类型、数值类型、地理坐标类型、日期类型等。
详细说明可以参考官方文档:
语法说明:
GET /indexName/_search
{
"query": {
"match_all": {}
},
"sort": [
{
"排序字段": {
"order": "排序方式asc和desc"
}
}
]
}示例,我们按照商品价格排序:
GET /items/_search
{
"query": {
"match_all": {}
},
"sort": [
{
"price": {
"order": "desc"
}
}
]
}5.1.5 分页查询
elasticsearch 默认情况下只返回top10的数据。而如果要查询更多数据就需要修改分页参数了。
elasticsearch中通过修改from、size参数来控制要返回的分页结果:
from:从第几个文档开始size:总共查询几个文档
类似于mysql中的limit ?, ?
官方文档如下:
语法如下:
GET /items/_search
{
"query": {
"match_all": {}
},
"from": 0, // 分页开始的位置,默认为0
"size": 10, // 每页文档数量,默认10
"sort": [
{
"price": {
"order": "desc"
}
}
]
}5.2. Java Client实现搜索
5.2.1. Term查询
文档:
根据DSL语句编写java代码:
GET /items/_search
{
"query": {
"term": {
"category": {
"value": "拉杆箱"
}
}
}
}代码如下:
@Test
void testTerm() throws IOException {
SearchResponse<ItemDoc> response = esClient.search(s -> s
.index("items")//指定索引名
.query(q -> q
.term(t -> t
.field("category").value("拉杆箱"))
),
ItemDoc.class
);
// 解析响应
handleResponse(response);
}
private void handleResponse(SearchResponse<ItemDoc> searchResponse) {
//获取总条数
long total = searchResponse.hits().total().value();
log.info("查询到{}条数据",total);
//解析结果
List<Hit<ItemDoc>> hits = searchResponse.hits().hits();
//遍历hits
hits.forEach(hit -> {
ItemDoc source = hit.source();
log.info("查询到数据:{}",source);
});
}说明:
esClient.search()方法的签名为:
public final <TDocument> SearchResponse<TDocument> search(Function<SearchRequest.Builder, ObjectBuilder<SearchRequest>> fn, Class<TDocument> tDocumentClass) throws IOException, ElasticsearchException {
return this.search((SearchRequest)((ObjectBuilder)fn.apply(new SearchRequest.Builder())).build(), tDocumentClass);
}search()方法第一个参数是一个函数式接口,第二个参数为模型类的Class类型。
SearchResponse为响应类型,可以依据DSL查询结果进行解析,如下:
{
"took" : 0,
"timed_out" : false,
"_shards" : {
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 60,
"relation" : "eq"
},
"max_score" : 0.35950777,
"hits" : [
{
"_index" : "items",
"_type" : "_doc",
"_id" : "317578",
"_score" : 0.35950777,
"_source" : {
"id" : "317578",
"name" : "RIMOWA 21寸托运箱拉杆箱 SALSA AIR系列果绿色 820.70.36.4",
"price" : 28900,
"stock" : 9985,
"image" : "https://m.360buyimg.com/mobilecms/s720x720_jfs/t6934/364/1195375010/84676/e9f2c55f/597ece38N0ddcbc77.jpg!q70.jpg.webp",
"category" : "拉杆箱",
"brand" : "RIMOWA",
"sold" : 0,
"commentCount" : 0,
"isAD" : false,
"updateTime" : [
2024,
10,
25,
17,
52,
8
]
}
},
...结合解析handleResponse()方法阅读代码:

代码解读:
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文档数据
我们是根据DSL编写java代码,也可以跟踪java代码查看最终执行的DSL是否正确。
上边的代码改为如下代码,并打断点:

再次运行跟踪断点

复制DSL语句

SearchRequest: POST /items/_search?typed_keys=true {"query":{"term":{"category":{"value":"拉杆箱"}}}}去掉前边的请求地址拿到DSL语句
{"query":{"term":{"category":{"value":"拉杆箱"}}}}然后在kibana中进行测试。
5.2.2. 全文检索
下边使用Java Client实现全文检索:
match查询:
根据DSL编写java代码:
GET /items/_search
{
"query": {
"match": {
"name": "绿色拉杆箱"
}
}
}java代码:
@Test
void testFullTextQuery() throws IOException {
SearchResponse<ItemDoc> searchResponse = esClient.search(s -> s
.index("items")
.query(q -> q
.match(t -> t
.field("name").query("绿色拉杆箱"))), ItemDoc.class);
//解析结果
handleResponse(searchResponse);
}multi_match查询:
DSL:
GET /items/_search
{
"query": {
"multi_match": {
"query": "绿色拉杆箱",
"fields": ["name","category"]
}
}
}Java:
@Test
void testMultiMatchQuery() throws IOException {
SearchResponse<ItemDoc> searchResponse = esClient.search(s -> s
.index("items")
.query(q -> q
.multiMatch(t -> t
.fields("name", "category").query("绿色拉杆箱"))), ItemDoc.class);
//解析结果
handleResponse(searchResponse);
}5.2.3 排序和分页
DSL:
GET /items/_search
{
"query": {
"multi_match": {
"query": "绿色拉杆箱",
"fields": ["name","category"]
}
},
"sort": [
{
"price": {
"order": "asc"
}
}
],
"size": 20,
"from": 0
}Java:
//测试multi_match查询并加入排序和分页查询
@Test
void testPageAndSort() throws IOException {
int pageNo = 1, pageSize = 5;
SearchResponse<ItemDoc> searchResponse = esClient.search(s -> s
.index("items")
.query(q -> q
.multiMatch(t -> t
.fields("name", "category").query("绿色拉杆箱")))
.sort(sort->sort
.field(field->field.field("price").order(SortOrder.Asc)))//排序
.size(pageSize)//每页显示条数
.from((pageNo - 1) * pageSize)//从第几条开始
, ItemDoc.class);
//解析结果
handleResponse(searchResponse);
}5.3 复合查询
第一类:基于逻辑运算组合叶子查询,实现组合条件,例如
- bool:实现组合条件查询,项目使用较多。
第二类:基于某种算法修改查询时的文档相关性算分,从而改变文档排名。例如:
- function_score:通过条件,指定算分函数,控制文档得分,得分越高排名越靠前,比如:百度竞价排名。
- dis_max:从多个查询中选择得分最高的结果。
其它复合查询及相关语法可以参考官方文档:
其它复合查询及相关语法可以参考官方文档:
5.3.1.布尔查询
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}}}
]
}
},
"sort": [
{
"brand": {
"order": "desc"
}
}
]
}这个查询的整体逻辑如下:
必须条件 (
must):- 文档的
name字段必须包含“手机”。
- 文档的
可选条件 (
should):- 文档的
brand字段应该是“vivo”或者“小米”。只要满足其中一个条件即可。
- 文档的
排除条件 (
must_not):- 文档的
price字段不能大于等于 2500 元。
- 文档的
过滤条件 (
filter):- 文档的
price字段必须小于等于 1000 元。
- 文档的
最终,这个查询会返回所有符合条件的文档,即名称包含“手机”,品牌为“vivo”或“小米”,并且价格在 1000 元以内,同时价格不低于 2500 元的文档会被排除在外。
运行上边的语句发现型号除了vivo和小米的记录也出现在查询结果中了,这说明should条件没有起作用。
当should与must、must_not同时使用时should会失效,需要指定minimum_should_match。
minimum_should_match:指定should中至少满足几个条件,默认为0。
语句修改如下:
GET /items/_search
{
"query": {
"bool": {
"must": [
{"match": {"name": "手机"}}
],
"should": [
{"term": {"brand": { "value": "小米" }}},
{"term": {"brand": { "value": "vivo" }}}
],
"minimum_should_match": 1
}
},
"sort": [
{
"brand": {
"order": "desc"
}
}
]
}minimum_should_match也可以指定百分比例,比如:minimum_should_match: "50%" 表示至少有一半的条件满足。
5.3.2 尽量使用filter
出于性能考虑,与搜索关键字无关的查询尽量采用must_not或filter逻辑运算,避免参与相关性算分。
例如黑马商城的搜索页面:

其中输入框的搜索条件肯定要参与相关性算分,可以采用match。但是价格范围过滤、品牌过滤、分类过滤等尽量采用filter,不要参与相关性算分。
比如,我们要搜索手机,但品牌必须是华为,价格必须是900~1599,那么可以这样写:
GET /items/_search
{
"query": {
"bool": {
"must": [
{"match": {"name": "手机"}}
],
"filter": [
{"term": {"brand": { "value": "华为" }}},
{"range": {"price": {"gte": 90000, "lt": 159900}}}
]
}
}
}5.3.3. Java Client
对照dsl语句结合AI编写Java Client程序
@Test
void testBoolQuery() throws Exception {
//构建请求
SearchRequest.Builder builder = new SearchRequest.Builder();
//设置索引
builder.index("items");
//设置查询条件
SearchRequest.Builder searchRequestBuilder = builder.query(q -> q
.bool(b -> b
.must(m -> m
.match(mm -> mm
.field("name")
.query("手机")))
.should(s1 -> s1
.term(t -> t
.field("brand")
.value("小米")
)
)
.should(s1 -> s1
.term(t -> t
.field("brand")
.value("vivo")
)
)
.minimumShouldMatch("1")
)
).sort(sort -> sort
.field(f -> f.field("brand").order(SortOrder.Asc)));
SearchRequest build = searchRequestBuilder.build();
//执行请求
SearchResponse<ItemDoc> searchResponse = esClient.search(build, ItemDoc.class);
//解析结果
handleResponse(searchResponse);
}5.4.高亮显示
5.4.1 高亮显示原理
什么是高亮显示呢?
我们在百度,京东搜索时,关键字会变成红色,比较醒目,这叫高亮显示:

观察页面源码,你会发现两件事情:
- 高亮词条都被加了
<em>标签 <em>标签都添加了红色样式
css样式肯定是前端实现页面的时候写好的,但是前端编写页面的时候是不知道页面要展示什么数据的,不可能给数据加标签。而服务端实现搜索功能,要是有elasticsearch做分词搜索,是知道哪些词条需要高亮的。
因此词条的高亮标签肯定是由服务端提供数据的时候已经加上的。
因此实现高亮的思路就是:
- 用户输入搜索关键字搜索数据
- 服务端根据搜索关键字到elasticsearch搜索,并给搜索结果中的关键字词条添加
html标签 - 前端提前给约定好的
html标签添加CSS样式
5.4.2 实现高亮
事实上elasticsearch已经提供了给搜索关键字加标签的语法,无需我们自己编码。
基本语法如下:
GET /{索引库名}/_search
{
"query": {
"match": {
"搜索字段": "搜索关键字"
}
},
"highlight": {
"fields": {
"高亮字段名称": {
"pre_tags": "<em>",
"post_tags": "</em>"
}
}
}
}示例:

下边用Java Client实现,对照dsl语句结合AI编写Java Client程序
@Test
void testHighlight() throws IOException {
SearchRequest.Builder builder = new SearchRequest.Builder();
builder.index("items");
builder.query(q -> q
.match(m -> m
.field("name").query("脱脂牛奶")));
builder.highlight(h -> h
.fields("name", f -> f
.preTags("<em>")
.postTags("</em>")));
SearchRequest request = builder.build();
SearchResponse<ItemDoc> response = esClient.search(request, ItemDoc.class);
//解析出高亮结果
for (Hit<ItemDoc> hit: response.hits().hits()) {
ItemDoc source = hit.source();
Map<String, List<String>> highlightFields = hit.highlight();
if (highlightFields != null) {
List<String> name = highlightFields.get("name");
if (name != null && name.size() > 0 ) {
String highlightName = name.get(0);
source.setName(highlightName);
}
}
log.info("查询结果:{}",source);
}
}6 数据聚合
6.1 介绍
聚合(aggregations)可以让我们极其方便的实现对数据的统计、分析、运算。例如:
- 什么品牌的手机最受欢迎?
- 这些手机的平均价格、最高价格、最低价格?
- 这些手机每月的销售情况如何?
实现这些统计功能的比数据库的sql要方便的多,而且查询速度非常快,可以实现近实时搜索效果。
应用场景:
- 对数据进行统计
- 在搜索界面显示符合条件的品牌、分类、规格等信息,如下图:

聚合常见的有三类:
官方文档:
桶(
Bucket)聚合:用来对文档做分组TermAggregation:按照文档字段值分组,例如按照品牌值分组、按照国家分组Date Histogram:按照日期阶梯分组,例如一周为一组,或者一月为一组度量(
Metric)聚合:用以计算一些值,比如:最大值、最小值、平均值等Avg:求平均值Max:求最大值Min:求最小值Stats:同时求max、min、avg、sum等管道(
pipeline)聚合:其它聚合的结果为基础做进一步运算
注意:参加聚合的字段必须是keyword、日期、数值、布尔类型
6.2. Bucket聚合
6.2.1 语法
例如我们要统计所有商品中共有哪些商品分类,其实就是以分类(category)字段对数据分组。category值一样的放在同一组,属于Bucket聚合中的Term聚合。
基本语法如下:
GET /items/_search
{
"size": 0,
"aggs": {
"category_agg": {
"terms": {
"field": "category",
"size": 20,
"order": {
"_count": "desc"
}
}
}
}
}语法说明:
size:设置size为0,查询0条数据即结果中不包含文档,只包含聚合aggregations:定义聚合category_agg:聚合名称,自定义,但不能重复terms:聚合的类型,按分类聚合,所以用termfield:参与聚合的字段名称size:希望返回的聚合结果的最大数量- order: 对聚合结果排序
来看下查询的结果:

6.2.2 多级聚合
同时对品牌分组统计,此时需要按分类统计,按品牌统计,这时需要定义多个桶,如下:
GET /items/_search
{
"size": 0,
"aggs": {
"category_agg": {
"terms": {
"field": "category",
"size": 20
}
},
"brand_agg":{
"terms": {
"field": "brand",
"size": 20
}
}
}
}结果:
{
"took" : 7,
"timed_out" : false,
"_shards" : {
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 2000,
"relation" : "eq"
},
"max_score" : null,
"hits" : [ ]
},
"aggregations" : {
"category_agg" : {
"doc_count_error_upper_bound" : 0,
"sum_other_doc_count" : 0,
"buckets" : [
{
"key" : "拉杆箱",
"doc_count" : 1323
},
{
"key" : "真皮包",
"doc_count" : 210
},
{
"key" : "手机",
"doc_count" : 151
},
{
"key" : "牛奶",
"doc_count" : 145
}
]
},
"brand_agg" : {
"doc_count_error_upper_bound" : 0,
"sum_other_doc_count" : 863,
"buckets" : [
{
"key" : "美旅箱包",
"doc_count" : 110
},
{
"key" : "汉客",
"doc_count" : 90
}
]
}
}
}现在需要统计同一分类下的不同品牌的商品数量,这时就需要对桶内的商品二次聚合,如下:
GET /items/_search
{
"aggs" : {
"category_agg" : {
"aggs" : {
"brand_agg" : {
"terms" : {
"field" : "brand",
"size" : 20
}
}
},
"terms" : {
"field" : "category",
"size" : 20
}
}
},
"size" : 0
}结果:
截取部分结果如下,拉杆箱是按分类聚合的一级聚合结果,其下的“汉客”、"新秀丽"是按品牌聚合的二级聚合结果。
{
"took" : 1,
"timed_out" : false,
"_shards" : {
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 4002,
"relation" : "eq"
},
"max_score" : null,
"hits" : [ ]
},
"aggregations" : {
"category_agg" : {
"doc_count_error_upper_bound" : 0,
"sum_other_doc_count" : 0,
"buckets" : [
{
"key" : "拉杆箱",
"doc_count" : 2088,
"brand_agg" : {
"doc_count_error_upper_bound" : 0,
"sum_other_doc_count" : 594,
"buckets" : [
{
"key" : "美旅箱包",
"doc_count" : 187
},
{
"key" : "PointKid",
"doc_count" : 128
},
{
"key" : "汉客",
"doc_count" : 120
},
{
"key" : "新秀丽",
"doc_count" : 104
},
{
"key" : "DELSEY",
"doc_count" : 99
},
{
"key" : "莎米特",
"doc_count" : 91
},
{
"key" : "文森保罗",
"doc_count" : 89
},
{
"key" : "旅行之家",
"doc_count" : 78
},
{
"key" : "爱华仕",
"doc_count" : 72
},
{
"key" : "梵地亚",
"doc_count" : 70
},
{
"key" : "瑞动",
"doc_count" : 66
},
{
"key" : "银座",
"doc_count" : 59
},
{
"key" : "Diplomat",
"doc_count" : 58
},
{
"key" : "博兿",
"doc_count" : 46
},
{
"key" : "宾豪",
"doc_count" : 41
},
{
"key" : "卡拉羊",
"doc_count" : 40
},
{
"key" : "瑞界",
"doc_count" : 38
},
{
"key" : "Kamiliant",
"doc_count" : 37
},
{
"key" : "ITO",
"doc_count" : 36
},
{
"key" : "EAZZ",
"doc_count" : 35
}
]
}
},6.2.3 思考
下边的语句中包含两个“brand_agg”,它们有什么不同?
GET /items/_search
{
"aggs" : {
"brand_agg" : {
"terms" : {
"field" : "brand",
"size" : 20,
"order": {
"_count": "desc"
}
}
},
"category_agg" : {
"aggs" : {
"brand_agg" : {
"terms" : {
"field" : "brand",
"size" : 20
}
}
},
"terms" : {
"field" : "category",
"size" : 20
}
}
},
"size" : 0
}6.3 带条件聚合
默认情况下,Bucket聚合是对索引库的所有文档做聚合,例如我们统计商品中所有的品牌,结果如下:

可以看到统计出的品牌非常多。
但真实场景下,用户会输入搜索条件,因此聚合必须是对搜索结果聚合。那么聚合必须添加限定条件。
例如,我想知道价格高于3000元的手机品牌有哪些,该怎么统计呢?
我们需要从需求中分析出搜索查询的条件和聚合的目标:
搜索查询条件:
- 价格高于3000
- 必须是手机
聚合目标:统计的是品牌,肯定是对brand字段做term聚合
语法如下:
增加"query"标签。
GET /items/_search
{
"query": {
"bool": {
"filter": [
{
"term": {
"category": "手机"
}
},
{
"range": {
"price": {
"gte": 300000
}
}
}
]
}
},
"size": 0,
"aggs": {
"brand_agg": {
"terms": {
"field": "brand",
"size": 20
}
}
}
}聚合结果如下:
{
"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个品牌了。
6.4 Metric聚合
上节课,我们统计了价格高于3000的手机品牌,形成了一个个桶。现在我们需要对桶内的商品做运算,获取每个品牌价格的最小值、最大值、平均值。
这就要用到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,
"order": {
"stats_metric.avg": "desc"
}
},
"aggs": {
"stats_metric": {
"stats": {
"field": "price"
}
}
}
}
}
}query部分就不说了,我们重点解读聚合部分语法。
可以看到我们在brand_agg聚合的内部,我们新加了一个aggs参数。这个聚合就是brand_agg的子聚合,会对brand_agg形成的每个桶中的文档分别统计。
stats_meric:聚合名称,自定义名称stats:聚合类型,stats是metric聚合的一种field:聚合字段,这里选择price,统计价格
由于stats是对brand_agg形成的每个品牌桶内文档分别做统计,因此每个品牌都会统计出自己的价格最小、最大、平均值。
结果如下:

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

6.5. Java Client
参考DSL语句编写Java Client代码。
@Test
void testAggs() throws Exception {
//构建请求
SearchRequest.Builder builder = new SearchRequest.Builder();
//设置索引名
builder.index("items");
//设置查询条件
builder.query(q -> q.bool(b -> b
.filter(f -> f.term(t -> t.field("category").value("手机")))
.filter(f -> f.range(r -> r.field("price").gte(JsonData.of(3000))))));
//设置返回数量
builder.size(0);
//设置聚合
builder.aggregations("brand_agg", a -> a
.terms(t -> t
.field("brand")
.size(10)
.order(NamedValue.of("stats_metric.avg", SortOrder.Desc)))
.aggregations("stats_metric", a1 -> a1.stats(s -> s.field("price")))
);
SearchRequest build = builder.build();
//执行请求
SearchResponse<ItemDoc> searchResponse = esClient.search(build, ItemDoc.class);
//解析出聚合结果
Aggregate brandAgg = searchResponse.aggregations().get("brand_agg");
brandAgg.sterms().buckets().array().forEach(bucket -> {
String key = bucket.key().stringValue();
Long docCount = bucket.docCount();
StatsAggregate statsMetric = bucket.aggregations().get("stats_metric").stats();
//平均价格
Double avg = statsMetric.avg();
//最大价格
Double max = statsMetric.max();
//最小价格
Double min = statsMetric.min();
log.info("品牌:{},商品数量:{},平均价格:{},最大价格:{},最小价格:{}", key, docCount, avg, max, min);
});
}作业
批量导入数据
使用批量导入将商品表的数据全部导入到ES中。
提示:一页一页查询商品数据库,每查询一页数据将其同步至ES中。
开发搜索服务
需求
搜索业务并发压力可能会比较高,目前与商品服务在一起,不方便后期优化。
将搜索相关功能抽取到这个微服务中。
搜索功能支持:
- 输入关键字进行全文检索,匹配商品名称、品牌、分类信息。
- 按分类、品牌、价格进行检索
- 商品名称高亮显示
示例:

提示
修改原来的搜索接口,改为使用Elasticsearch实现,实现根据关键字搜索,实现分页、排序。
创建搜索服务,命名为search-service,端口为8087。

创建配置类
package com.hmall.search.config;
import cn.hutool.core.date.DatePattern;
import co.elastic.clients.elasticsearch.ElasticsearchClient;
import co.elastic.clients.json.jackson.JacksonJsonpMapper;
import co.elastic.clients.transport.ElasticsearchTransport;
import co.elastic.clients.transport.rest_client.RestClientTransport;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.PropertyNamingStrategies;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import com.hmall.search.properties.EsProperties;
import org.apache.http.HttpHost;
import org.elasticsearch.client.RestClient;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
/**
* @author Mr.M
* @version 1.0
* @description Elasticsearch配置类
* @date 2024/8/19 18:09
*/
@Configuration
@EnableConfigurationProperties(EsProperties.class)
public class EsConfiguration {
@Bean
public ElasticsearchClient esClient(EsProperties esProperties) {
// Create the low-level client
RestClient restClient = RestClient.builder(
new HttpHost(esProperties.getHost(), esProperties.getPort())).build();
// 创建 ObjectMapper 实例
ObjectMapper objectMapper = new ObjectMapper();
// 添加 JavaTimeModule 以支持 LocalDateTime 类型
objectMapper.registerModule(new JavaTimeModule());
// Create the transport with a Jackson mapper
ElasticsearchTransport transport = new RestClientTransport(
restClient, new JacksonJsonpMapper(objectMapper));
// And create the API client
ElasticsearchClient esClient = new ElasticsearchClient(transport);
return esClient;
}
}EsProperties类如下:
package com.hmall.search.properties;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
@Configuration
@ConfigurationProperties(prefix = "hm.es")
@Data
public class EsProperties {
/**
* es host
*/
private String host;
/**
* es 端口
*/
private Integer port;
}在application.yaml中配置:
hm:
es:
host: 192.168.101.68
port: 9200屏蔽item-service中的SearchController
在网关中添加搜索服务的路由配置,修改完配置文件重启网关,如果网关无法启动需要查看控制台是否无法加载配置文件。
