文章目录
阅读本文前可以先阅读我的另一篇博文: ElasticSearch快速入门——上篇(认识ElasticSearch、安装ElasticSearch、安装kibana、IK分词器、ElasticSearch中的基本概念、索引库操作、文档操作)
10. JavaRestClient
ElasticSearch 为我们提供了 Java 客户端,名为 JavaRestClient,为什么带有 Rest 关键字呢,因为这个客户端本质上就是帮我们发送 Restful 风格的 Http 请求
Elasticsearch 目前的最新版本是 8.x,其 Java 客户端有很大变化,特别是 API 方面,8.x 版本的 API 大都是响应式编程、与 lambda 表达式相关的 API
由于大多数企业使用的还是 8 以下的版本,所以我们选择使用早期的 JavaRestClient 客户端(虽然已被标记为过时)来学习
官方文档地址:Elasticsearch Clients
10.1 导入依赖
引入 ElasticSearch 的 RestHighLevelClient 依赖
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-high-level-client</artifactId>
</dependency>
10.2 指定版本
如果是 SpringBoot 项目,会有一个默认的 ElasticSearch 版本,我们需要覆盖默认的 ElasticSearch 版本
在父工程的 pom.xml 文件中添加以下属性
<properties>
<elasticsearch.version>7.17.18</elasticsearch.version>
</properties>
10.3 编写测试类
编写一个测试类,测试能否成功连接 ElasticSearch
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;
public class ElasticSearchTests {
private RestHighLevelClient restHighLevelClient;
@Test
public void testConnect() {
System.out.println(restHighLevelClient);
}
@BeforeEach
public void setUp() {
restHighLevelClient = new RestHighLevelClient(RestClient.builder(
new HttpHost("127.0.0.1", 9200, "http")
));
}
@AfterEach
public void tearDown() throws Exception {
restHighLevelClient.close();
}
}
11. 创建索引库时如何编写 Mapping 映射
创建索引库前需要先准备好索引库的 Mapping 映射,我们以商品数据为例,分析该如何编写 Mapping 映射
我们要实现商品搜索,那么索引库的字段肯定要满足页面搜索的需求,但是我们不能照搬商品表的结构作为 Mapping 映射的结构,因为商品表里面有非常多的字段,但这些字段不一定是索引库所需要的,如果直接照搬商品表的结构作为 Mapping 映射的结构,会增加 ElasticSearch 的存储负担
编写 Mapping 字段,需要结合我们的业务逻辑,我们以商品搜索页面为例来分析
以下是商品表的结构
根据搜索逻辑,我们可以判断出表中的哪些字段需要保存到 ElasticSearch 中,哪些字段参与搜索
根据分析,我们可以得到以下创建索引库的语句
PUT /shopping_mall
{
"mappings": {
"properties": {
"id": {
"type": "keyword"
},
"name": {
"type": "text",
"analyzer": "ik_smart"
},
"price": {
"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"
}
}
}
}
12. JavaRestClient 操作索引库
12.1 新增索引库
新增索引库的语法如下
其中的 indices 是 index 的复数形式,indices 方法会返回一个对象,该对象中包含了操作索引库的所有方法
具体的 Java 代码如下
private static final String MAPPING_TEMPLATE = """
{
"mappings": {
"properties": {
"id": {
"type": "keyword"
},
"name": {
"type": "text",
"analyzer": "ik_smart"
},
"price": {
"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",
"format": ["yyyy-MM-dd HH:mm:ss"]
}
}
}
}""";
@Test
public void testCreateIndex() throws IOException {
// 1.创建 CreateIndexRequest 对象
CreateIndexRequest createIndexRequest = new CreateIndexRequest("shopping_mall");
// 2.指定请求参数,其中 MAPPING_TEMPLATE 是一个静态常量字符串,内容是 JSON 格式的请求
createIndexRequest.source(MAPPING_TEMPLATE, XContentType.JSON);
// 3.发起请求
restHighLevelClient.indices().create(createIndexRequest, RequestOptions.DEFAULT);
}
12.2 查询索引库
查询索引库的语法如下
具体的 Java 代码
注意:GetIndexRequest 类的包名为 org.elasticsearch.client.indices.GetIndexRequest
@Test
public void testGetIndex() throws IOException {
// 1.创建 GetIndexRequest 对象
GetIndexRequest shoppingMall = new GetIndexRequest("shopping_mall");
// 2.发起请求
boolean exists = restHighLevelClient.indices().exists(shoppingMall, RequestOptions.DEFAULT);
System.err.println("exists = " + exists);
}
12.3 删除索引库
删除索引库的语法如下
具体的 Java 代码如下
@Test
public void testDeleteIndex() throws IOException {
// 1.创建 DeleteIndexRequest 对象
DeleteIndexRequest shoppingMall = new DeleteIndexRequest("shopping_mall");
// 2.发起请求
AcknowledgedResponse acknowledgedResponse = restHighLevelClient.indices().delete(shoppingMall, RequestOptions.DEFAULT);
System.err.println("acknowledged = " + acknowledgedResponse.isAcknowledged());
}
12.4 查看当前有哪些索引库
在 kibana 提供的 Dev Tools 控制台中
GET _cat/indices?v
13. JavaRestClient 操作文档
我们新建一个名为 ElasticSearchDocumentTests 的测试类,在这个类中进行文档的 CRUD 操作
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;
public class ElasticSearchDocumentTests {
private RestHighLevelClient restHighLevelClient;
@BeforeEach
public void setUp() {
restHighLevelClient = new RestHighLevelClient(RestClient.builder(
new HttpHost("127.0.0.1", 9200, "http")
));
}
@AfterEach
public void tearDown() throws Exception {
restHighLevelClient.close();
}
}
13.1 新增文档
新增文档的 Java API 如下
具体的 Java 代码
private String sourceString = """
{
"id": "1",
"name": "小米11",
"price": 3999,
"image": "https://img.alicdn.com/imgextra/i4/O1CN01LX6jqs1E5W8Y8X1qG_!!6000000001648-2-tps-200-200.png",
"category": "手机",
"brand": "小米",
"sold": 100,
"commentCount": 100,
"isAD": true,
"updateTime": "2021-01-01"
}""";
@Test
public void testCreateDocument() throws IOException {
// 1.准备 IndexRequest 对象
IndexRequest indexRequest = new IndexRequest("shopping_mall").id("1");
// 2.准备请求参数
indexRequest.source(sourceString, XContentType.JSON);
// 3.发送请求
IndexResponse indexResponse = restHighLevelClient.index(indexRequest, RequestOptions.DEFAULT);
System.err.println("新增文档操作的返回结果 = " + indexResponse.getResult());
}
注意:
- 在实际业务中,会有一个专门的实体类用于往索引库中新增文档
- 将实体类转换成 JSON 格式的字符串可以使用 Alibaba 提供的 fastjson2 工具
13.2 查询文档
查询文档包含查询和解析响应结果两部分,对应的 Java API 如下
具体的 Java 代码
@Test
public void testGetDocument() throws IOException {
// 1.准备 GetRequest 对象
GetRequest getRequest = new GetRequest("shopping_mall", "1");
// 2.发送请求
GetResponse getResponse = restHighLevelClient.get(getRequest, RequestOptions.DEFAULT);
System.err.println("getResponse = " + getResponse);
System.err.println("source = " + getResponse.getSourceAsString());
}
13.3 删除文档
删除文档的 Java API 如下
具体的 Java 代码
@Test
public void testDeleteDocument() throws IOException {
// 1.准备 DeleteRequest 对象
DeleteRequest deleteRequest = new DeleteRequest("shopping_mall", "1");
// 2.发送请求
DeleteResponse deleteResponse = restHighLevelClient.delete(deleteRequest, RequestOptions.DEFAULT);
System.err.println("deleteResponse = " + deleteResponse.getResult());
}
13.4 修改文档
修改文档数据有两种方式:
- 方式一:全量更新,再次写入 id 一样的文档,就会制除旧文档,添加新文档,与新增的 Java API 一致(新增时返回的结果为 created ,全量更新时返回的结果为 updated)
- 方式二:局部更新,只更新指定部分字段
局部更新的 Java API 如下
具体的 Java 代码
@Test
public void testUpdateDocument() throws IOException {
// 1.准备 UpdateRequest 对象
UpdateRequest updateRequest = new UpdateRequest("shopping_mall", "1");
// 2.准备请求参数
HashMap<String, Object> stringObjectHashMap = new HashMap<>();
stringObjectHashMap.put("sold", 101);
updateRequest.doc(stringObjectHashMap, XContentType.JSON);
// 3.发送请求
UpdateResponse updateResponse = restHighLevelClient.update(updateRequest, RequestOptions.DEFAULT);
System.err.println("局部更新文档操作的返回结果 = " + updateResponse.getResult());
}
13.5 文档批处理(可以进行不同类型的文档操作)
批处理代码流程与之前类似,不过构建请求会用到一个名为 BulkRequest 的类来封装普通的 CRUD 请求
具体的 Java API 如下
具体的 Java 代码
@Test
public void testBulk() throws IOException {
// 1.准备 BulkRequest 对象
BulkRequest bulkRequest = new BulkRequest();
// 2.准备请求参数
// 2.1 新增文档
HashMap<String, Object> indexMap = new HashMap<>();
indexMap.put("sold", 102);
bulkRequest.add(new IndexRequest("shopping_mall").id("1").source(indexMap, XContentType.JSON));
// 2.2 修改文档
HashMap<String, Object> updateMap = new HashMap<>();
updateMap.put("commentCount", 103);
bulkRequest.add(new UpdateRequest("shopping_mall", "1").doc(updateMap, XContentType.JSON));
// 3.发起 bulk 请求
BulkResponse bulkResponse = restHighLevelClient.bulk(bulkRequest, RequestOptions.DEFAULT);
System.err.println("批量处理文档操作的返回结果如下");
Arrays.stream(bulkResponse.getItems()).forEach(item -> System.err.println(item.getResponse()));
}
13.6 利用 JavaRestClient 批量新增文档
在批量新增文档前,我们先准备一些数据
13.6.1 建表
首先,创建一个名为 item 的表,建表语句如下
/*
Navicat Premium Data Transfer
Source Server : localhost
Source Server Type : MySQL
Source Server Version : 80034 (8.0.34)
Source Host : localhost:3306
Source Schema : blog
Target Server Type : MySQL
Target Server Version : 80034 (8.0.34)
File Encoding : 65001
Date: 25/08/2024 01:59:24
*/
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for item
-- ----------------------------
DROP TABLE IF EXISTS `item`;
CREATE TABLE `item` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '商品id',
`name` varchar(200) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NOT NULL COMMENT 'SKU名称',
`price` int NOT NULL DEFAULT 0 COMMENT '价格(分)',
`stock` int UNSIGNED NOT NULL COMMENT '库存数量',
`image` varchar(200) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NULL DEFAULT NULL COMMENT '商品图片',
`category` varchar(200) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NULL DEFAULT NULL COMMENT '类目名称',
`brand` varchar(100) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NULL DEFAULT NULL COMMENT '品牌名称',
`spec` varchar(200) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NULL DEFAULT NULL COMMENT '规格',
`sold` int NULL DEFAULT 0 COMMENT '销量',
`comment_count` int NULL DEFAULT 0 COMMENT '评论数',
`isAD` tinyint(1) NULL DEFAULT 0 COMMENT '是否是推广广告,true/false',
`status` int NULL DEFAULT 2 COMMENT '商品状态 1-正常,2-下架,3-删除',
`create_time` datetime NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`creater` bigint NULL DEFAULT NULL COMMENT '创建人',
`updater` bigint NULL DEFAULT NULL COMMENT '修改人',
PRIMARY KEY (`id`) USING BTREE,
INDEX `status`(`status` ASC) USING BTREE,
INDEX `updated`(`update_time` ASC) USING BTREE,
INDEX `category`(`category` ASC) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 100002672305 CHARACTER SET = utf8mb3 COLLATE = utf8mb3_general_ci COMMENT = '商品表' ROW_FORMAT = COMPACT;
SET FOREIGN_KEY_CHECKS = 1;
13.6.2 导入数据
运行 SQL 文件,导入数据,共有 88476 条数据(如果需要 SQL 文件,可以私聊我)
13.6.3 编写实体类
编写 service 类前,需要先导入 MyBatisPlus 的 Maven 依赖
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.7</version>
</dependency>
Item.java
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import java.io.Serial;
import java.io.Serializable;
import java.time.LocalDateTime;
@TableName("item")
public class Item implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 商品id
*/
@TableId(value = "id", type = IdType.AUTO)
private Long id;
/**
* SKU名称
*/
private String name;
/**
* 价格(分)
*/
private Integer price;
/**
* 库存数量
*/
private Integer stock;
/**
* 商品图片
*/
private String image;
/**
* 类目名称
*/
private String category;
/**
* 品牌名称
*/
private String brand;
/**
* 规格
*/
private String spec;
/**
* 销量
*/
private Integer sold;
/**
* 评论数
*/
private Integer commentCount;
/**
* 是否是推广广告,true/false
*/
@TableField("isAD")
private Boolean isAD;
/**
* 商品状态 1-正常,2-下架,3-删除
*/
private Integer status;
/**
* 创建时间
*/
private LocalDateTime createTime;
/**
* 更新时间
*/
private LocalDateTime updateTime;
/**
* 创建人
*/
private Long creater;
/**
* 修改人
*/
private Long updater;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getPrice() {
return price;
}
public void setPrice(Integer price) {
this.price = price;
}
public Integer getStock() {
return stock;
}
public void setStock(Integer stock) {
this.stock = stock;
}
public String getImage() {
return image;
}
public void setImage(String image) {
this.image = image;
}
public String getCategory() {
return category;
}
public void setCategory(String category) {
this.category = category;
}
public String getBrand() {
return brand;
}
public void setBrand(String brand) {
this.brand = brand;
}
public String getSpec() {
return spec;
}
public void setSpec(String spec) {
this.spec = spec;
}
public Integer getSold() {
return sold;
}
public void setSold(Integer sold) {
this.sold = sold;
}
public Integer getCommentCount() {
return commentCount;
}
public void setCommentCount(Integer commentCount) {
this.commentCount = commentCount;
}
public Boolean getIsAD() {
return isAD;
}
public void setIsAD(Boolean AD) {
isAD = AD;
}
public Integer getStatus() {
return status;
}
public void setStatus(Integer status) {
this.status = status;
}
public LocalDateTime getCreateTime() {
return createTime;
}
public void setCreateTime(LocalDateTime createTime) {
this.createTime = createTime;
}
public LocalDateTime getUpdateTime() {
return updateTime;
}
public void setUpdateTime(LocalDateTime updateTime) {
this.updateTime = updateTime;
}
public Long getCreater() {
return creater;
}
public void setCreater(Long creater) {
this.creater = creater;
}
public Long getUpdater() {
return updater;
}
public void setUpdater(Long updater) {
this.updater = updater;
}
@Override
public String toString() {
return "Item{" +
"id=" + id +
", name='" + name + '\'' +
", price=" + price +
", stock=" + stock +
", image='" + image + '\'' +
", category='" + category + '\'' +
", brand='" + brand + '\'' +
", spec='" + spec + '\'' +
", sold=" + sold +
", commentCount=" + commentCount +
", isAD=" + isAD +
", status=" + status +
", createTime=" + createTime +
", updateTime=" + updateTime +
", creater=" + creater +
", updater=" + updater +
'}';
}
}
ItemDocument.java
import java.time.LocalDateTime;
/**
* 索引库实体类
*/
public class ItemDocument {
private Long id;
private String name;
private Integer price;
private Integer stock;
private String image;
private String category;
private String brand;
private Integer sold;
private Integer commentCount;
private Boolean isAD;
private LocalDateTime updateTime;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getPrice() {
return price;
}
public void setPrice(Integer price) {
this.price = price;
}
public Integer getStock() {
return stock;
}
public void setStock(Integer stock) {
this.stock = stock;
}
public String getImage() {
return image;
}
public void setImage(String image) {
this.image = image;
}
public String getCategory() {
return category;
}
public void setCategory(String category) {
this.category = category;
}
public String getBrand() {
return brand;
}
public void setBrand(String brand) {
this.brand = brand;
}
public Integer getSold() {
return sold;
}
public void setSold(Integer sold) {
this.sold = sold;
}
public Integer getCommentCount() {
return commentCount;
}
public void setCommentCount(Integer commentCount) {
this.commentCount = commentCount;
}
public Boolean getIsAD() {
return isAD;
}
public void setIsAD(Boolean AD) {
isAD = AD;
}
public LocalDateTime getUpdateTime() {
return updateTime;
}
public void setUpdateTime(LocalDateTime updateTime) {
this.updateTime = updateTime;
}
@Override
public String toString() {
return "ItemDocument{" +
"id='" + id + '\'' +
", name='" + name + '\'' +
", price=" + price +
", stock=" + stock +
", image='" + image + '\'' +
", category='" + category + '\'' +
", brand='" + brand + '\'' +
", sold=" + sold +
", commentCount=" + commentCount +
", isAD=" + isAD +
", updateTime=" + updateTime +
'}';
}
}
13.6.4 编写配置文件
编写配置文件前,先导入 MySQL 连接驱动的 Maven 依赖
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
application.yaml
spring:
datasource:
url: jdbc:mysql://localhost:3306/blog?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: 123456
编写完配置文件后,在项目的启动类上添加 @MapperScan
注解,指定 Mapper 所在的包
@MapperScan("cn.edu.scau.mapper")
13.6.5 编写 Mapper 类和 Service 类
ItemMapper.java
import cn.edu.scau.pojo.Item;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
public interface ItemMapper extends BaseMapper<Item> {
}
ItemService.java
import cn.edu.scau.pojo.Item;
import com.baomidou.mybatisplus.extension.service.IService;
public interface ItemService extends IService<Item> {
}
ItemServiceImpl.java
import cn.edu.scau.mapper.ItemMapper;
import cn.edu.scau.pojo.Item;
import cn.edu.scau.service.ItemService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;
@Service
public class ItemServiceImpl extends ServiceImpl<ItemMapper, Item> implements ItemService {
}
完成上述工作后,编写一个测试类,检查 ItemServiceImpl 类能否正常工作
import cn.edu.scau.service.ItemService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
public class ItemServiceTests {
@Autowired
private ItemService itemService;
@Test
public void test() {
System.out.println(itemService.getById(317578L));
}
}
13.6.6 批量新增文档
新增文档前,需要先引入 PageHelper 和 fastjson2 的 Maven 依赖
PageHelper
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>2.1.0</version>
<exclusions>
<exclusion>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
</exclusion>
</exclusions>
</dependency>
fastjson2
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>2.0.50</version>
</dependency>
由于数据库中有 87476 条数据,肯定不能一次性全部导入 ElasticSearch ,需要分批次导入
import cn.edu.scau.pojo.Item;
import cn.edu.scau.pojo.ItemDocument;
import cn.edu.scau.service.ItemService;
import com.alibaba.fastjson2.JSON;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.github.pagehelper.PageHelper;
import org.apache.http.HttpHost;
import org.elasticsearch.action.bulk.BulkRequest;
import org.elasticsearch.action.index.IndexRequest;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestClient;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.xcontent.XContentType;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.List;
@SpringBootTest
public class BulkInsertDocumentTests {
private RestHighLevelClient restHighLevelClient;
@Autowired
private ItemService itemService;
@Test
public void testBulkInsertDocument() throws Exception {
int pageNumber = 1;
int pageSize = 500;
while (true) {
// 1.准备文档数据
QueryWrapper<Item> queryWrapper = new QueryWrapper<>();
queryWrapper.lambda().eq(Item::getStatus, 1);
PageHelper.startPage(pageNumber, pageSize);
List<Item> itemList = itemService.list(queryWrapper);
if (itemList == null || itemList.isEmpty()) {
return;
}
// 2.准备 BulkRequest 对象
BulkRequest bulkRequest = new BulkRequest();
// 3.准备请求参数
ItemDocument itemDocument;
for (Item item : itemList) {
itemDocument = new ItemDocument();
BeanUtils.copyProperties(item, itemDocument);
bulkRequest.add(new IndexRequest("shopping_mall")
.id(item.getId().toString())
.source(JSON.toJSONString(itemDocument), XContentType.JSON));
}
// 4.发送请求
restHighLevelClient.bulk(bulkRequest, RequestOptions.DEFAULT);
// 5.翻页
pageNumber++;
}
}
@BeforeEach
public void setUp() {
restHighLevelClient = new RestHighLevelClient(RestClient.builder(
new HttpHost("127.0.0.1", 9200, "http")
));
}
@AfterEach
public void tearDown() throws Exception {
restHighLevelClient.close();
}
}
批量新增文档总耗时 34 秒 2 毫秒(耗时较慢,可以使用多线程 + 线程池的方案进行优化),我们来验证一下数据是否已经全部导入成功
返回的结果
{
"count" : 88476,
"_shards" : {
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
}
}
item 表中共有 88476 条数据,其中有一条数据的 status 字段为 2 ,表示该商品已下架,再加上我们新增的小米手机数据,所以总共是 88476 条数据
13.6.7 可能遇到的问题
如果你在批量新增文档时遇到了以下错误,是因为 ElasticSearch 拒绝了执行该次批量操作请求
{“error”:{“root_cause”:[{“type”:“es_rejected_execution_exception”,“reason”:“rejected execution of coordinating operation [coordinating_and_primary_bytes=0, replica_bytes=0, all_bytes=0, coordinating_operation_bytes=54034916, max_coordinating_and_primary_bytes=53687091]”}],“type”:“es_rejected_execution_exception”,“reason”:“rejected execution of coordinating operation [coordinating_and_primary_bytes=0, replica_bytes=0, all_bytes=0, coordinating_operation_bytes=54034916, max_coordinating_and_primary_bytes=53687091]”},“status”:429}
at org.elasticsearch.client.RestClient.convertResponse(RestClient.java:347)
at org.elasticsearch.client.RestClient.performRequest(RestClient.java:313)
at org.elasticsearch.client.RestClient.performRequest(RestClient.java:288)
at org.elasticsearch.client.RestHighLevelClient.performClientRequest(RestHighLevelClient.java:2699)
at org.elasticsearch.client.RestHighLevelClient.internalPerformRequest(RestHighLevelClient.java:2171)
… 74 more
因为本次批量操作所需的内存大小超过了配置的最大限制,也就是说,ElasticSearch 为了防止内存溢出或资源耗尽,拒绝了该次请求
解决方法:减小分页查询时每一页的数据条数
14. DSL(Domain SpecificLanguage) 查询
前面我们都是根据 id 来查询文档的,这种查询方式满足不了我们的业务要求,比如想京东这样的电商网站,我们搜索商品的时候,搜索条件往往比较复杂,为了实现复杂搜索,我们需要一种新的搜索方式
ElasticSearch 为我们提供了 DSL 查询来实现复杂搜索
14.1 快速入门
DSL:Domain Specific Language,以 JSON 格式来定义查询条件,以下是一个示例
大家看到上面的请求,是不是一下就懵了,怎么这么复杂
不用担心,这是最终形态,我们在学习的时候肯定是一步一步拆分,从易到难进行学习
DSL 查询可以分为两大类:
- 叶子查询(Leaf query clauses):一般是在特定的字段里查询特定值,属于简单查询,很少单独使用
- 复合查询(Compound query clauses):以逻辑方式组合多个叶子查询或者更改叶子查询的行为方式
查询以后,还可以对查询的结果做处理,包括:
- 排序:按照 1 个或多个字段值做排序
- 分页:根据 from 和 size 做分页,类似于 MySQL 的分页
- 高亮:对搜索结果中的关键字添加特殊样式,使其更加醒目
- 聚合:对搜索结果做数据统计以形成报表
基于 DSL 的查询语法如下
我们先在 Dev Tools 中进行一个 DSL 查询
GET /shopping_mall/_search
{
"query": {
"match_all": {}
}
}
查询结果
{
"took" : 0,
"timed_out" : false,
"_shards" : {
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 1,
"relation" : "eq"
},
"max_score" : 1.0,
"hits" : [
{
"_index" : "shopping_mall",
"_type" : "_doc",
"_id" : "1",
"_score" : 1.0,
"_source" : {
"sold" : 102,
"commentCount" : 103
}
}
]
}
}
以下是对查询结果的解释
- Elastic Search 单次查询涉及的数据量默认不能超过一万
- Elastic Search 单次查询默认只会返回 10 条数据
14.2 叶子查询
叶子查询还可以进一步细分,常见的有:
- 全文检索(fulltext)查询:利用分词器对用户输入的内容分词,然后去词条列表中匹配,例如:
- match_query
- multi_match_query
- 精确查询:不对用户输入内容分词,直接精确匹配,一般是查找keyword、数值、日期、布尔等类型,例如:
- ids
- range
- term
- 地理(geo)查询:用于搜索地理位置,搜索方式很多,例如(地理查询可用于实现搜索附近的人、搜索附近的车等功能):
- geo_distance
- geo_bounding_box
14.2.1 全文检索查询
参与全文检索查询的字段最好也是可以分词的(类型为 text 的字段),这样才可以找到与用户搜索内容匹配度更高的文档
14.2.1.1 match 查询
match 查询:全文检索查询的一种,会对用户输入内容分词,然后去倒排索引库检索,语法如下
示例
GET /shopping_mall/_search
{
"query": {
"match": {
"name": "脱脂牛奶"
}
}
}
14.2.1.2 multi_match 查询
multi_match:与 match 查询类似,只不过允许同时查询多个字段,语法如下(参与查询字段越多,查询性能越差)
示例
GET /shopping_mall/_search
{
"query": {
"multi_match": {
"query": "脱脂牛奶",
"fields": ["name"]
}
}
}
14.2.2 精确查询
精确查询,英文是 Term-level query,顾名思义,词条级别的查询,也就是说不会对用户输入的搜索条件再分词,而是将搜索条件作为一个词条,与搜索的字段内容精确值匹配
精确查询适用于查找 keyword、数值、日期、boolean 类型的字段,例如 id、price、城市、地名、人名等作为一个整体才有含义的字段
精确查询主要有三种:
- term 查询
- range 查询
- id 查询
14.2.2.1 term 查询
term 查询的语法如下
示例
GET /shopping_mall/_search
{
"query": {
"term": {
"brand": {
"value": "德亚"
}
}
}
}
14.2.2.2 range 查询
range 查询的语法如下
示例(价格以分为单位)
GET /shopping_mall/_search
{
"query": {
"range": {
"price": {
"gte": 500000,
"lte": 1000000
}
}
}
}
14.2.2.3 id 查询
示例
GET /shopping_mall/_search
{
"query": {
"ids": {
"values": [
"613359",
"613360"
]
}
}
}
14.3 复合查询(布尔查询)
叶子查询是比较简单的单字段查询,在真实的业务场景下,往往都会有比较复杂的组合条件查询,这个时候就需要使用复合查询了
复合查询大致可以分为两类:
- 第一类:基于逻辑运算组合叶子查询,实现组合条件,例如
- bool
- 第二类:基于某种算法修改查询时的文档相关性算分,从而改变文档排名,例如:
- function_score
- dis_max
布尔查询是一个或多个查询子句的组合,子查询的组合方式有:
- must:必须匹配每个子查询,类似与
- should:选择性匹配子查询,类似或
- must_not:必须不匹配,不参与算分,类似非
- filter:必须匹配,不参与算分
布尔查询的语法如下
我们来做一个小案例:搜索"智能手机",但品牌必须是华为,而且价格必须在 [900, 1599] 区间内
GET /shopping_mall/_search
{
"query": {
"bool": {
"must": [
{
"match": {
"name": "智能手机"
}
}
],
"filter": [
{
"term": {
"brand": "华为"
}
},
{
"range": {
"price": {
"gte": 90000,
"lte": 159900
}
}
}
]
}
}
}
14.4 排序和分页
14.4.1 排序
Elastic Search 支持对搜索结果排序,默认是根据相关度算分(_score)来排序,也可以指定字段排序,可以排序的字段类型有:keyword 类型、数值类型、地理坐标类型、日期类型等
排序的语法如下
- 如果有多个字段参与排序,会先按照第一个字段进行排序
- 如果第一个字段相同,再按照第二个字段进行排序,以此类推
我们来做一个小案例:搜索商品,按照销量排序,销量一样则按照价格升序
GET /shopping_mall/_search
{
"query": {
"match_all": {}
},
"sort": [
{
"sold": {
"order": "desc"
}
},
{
"price": {
"order": "asc"
}
}
]
}
14.4.2 分页
Elastic Search 默认情况下只返回 top10 的数据,如果要查询更多数据,需要修改分页参数,Elastic Search 中通过修改 from、size 参数来控制要返回的分页结果:
- from:从第几个文档开始
- size:总共查询几个文档
分页的语法如下
我们来做一个小案例:查询出销量排名前 10 的商品,销量一样时按照价格升序
GET /shopping_mall/_search
{
"query": {
"match_all": {}
},
"from": 0,
"size": 10,
"sort": [
{
"sold": {
"order": "desc"
}
},
{
"price": {
"order": "asc"
}
}
]
}
14.5 深度分页问题
ElasticSearch 的数据一般会采用分片存储,也就是把一个索引中的数据分成 N 份,存储到不同节点上,查询数据时需要汇总各个分片的数据
假如我们要查第 100 页的数据,每页查 10 条,那 ElasticSearch 是如何找到前 1000 名中的最后 10 名的呢?
要找到前 1000 名的最后 10 名,需要先知道前 1000 名是谁,整体的思路有两步:
- 对数据进行排序
- 找出 [991, 1000] 名
那前 1000 个如何找呢(假如现在有四个分片),是不是我每个分片找 250 个就行了呢?实际上不能这么操作,因为数据是混乱地保存在不同的分片上的,不能保证每个分片的前 250 名加起来就是总的前 1000 名
举个通俗的例子,学校有 10 个班级,现在要找到年级前十名,是选出每个班的第一名就可以了吗,显然不是的,因为每个班级中每个学生都有学得好的和学得差的,班级的整体水平不一样,有可能某个班级的第一名在另一个班级中是垫底的
那我要怎么找到年级的前十名呢,最简单的做法就是对所有学生的成绩进行统计,找出前十名,但这种做法的效率可能不是很高,其实我们只需要找出每个班级的前十名,接着将每个班级前十名的学生汇总在一起,再找出这些汇总学生的前十名即可(就算是最极端的情况,即年级前十名都在同一个班级,也能正确地找出年级前十名)
ElasticSearch 的分页也是基于这个思想,要找出前 1000 名的数据,需要从每个分片中取出前 1000 名的数据,汇总后进行排序,找到真正的前 1000 名的数据
其实这种做法在数据量较大的情况下也有一定的问题,比如说我要查第 1 万页的数据,每页查 10 条,也就是要找前 10 万条数据,再找出排名在 [99990, 100000] 之间的数据
根据上述思想,就需要从每个分片中分别取出前 10 万条数据,汇总在一起,再筛选出真正的排名在 [99900, 100000] 之间的数据,这个数据量是非常恐怖的,很有可能导致内存直接炸裂
针对深度分页,Elastic Search 提供了两种解决方案(官方文档:search after):
- search after:分页时需要排序,原理是从上一次的排序值开始,查询下一页数据(官方推荐使用的方式)
- scroll:原理将排序数据形成快照,保存在内存,官方已经不推荐使用
search after 模式
- 优点:没有查询上限,支持深度分页
- 缺点:只能从前往后逐页查询,不能随机翻页
- 应用场景:数据迁移、手机滚动查询
那传统分页的应用场景有哪些呢?
百度搜索就是传统分页的一个典型例子,这个时候,有同学就有疑惑了,像百度这样的搜索引擎,ElasticSearch 中不是存储了数以千万计的数据吗,百度搜索是怎么解决深度分页问题的呢?
实际上,百度搜索对分页做了限制,最多只能跳转到 77 页,也就是说,你最多只能得到几百条数据
事实上,我们平时在用百度搜索的时候,一般只会看前三页的内容,很少会查看后面的数据,所以说,针对深度分页问题,最简单的解决方法就是直接对页码做一个限制,为页码制定一个上限(很多软件都是这么解决深度分页问题的)
ElasticSearch 对传统分页的页码和每页多少条数据也做了一个限制(也就是 from 字段和 size 字段),from 字段和 size 字段组合起来所需要用到的数据不能超过 1 万条,我们可以做一个测试
14.6 高亮显示
高亮显示:在搜索结果中把搜索关键字突出显示
我们在用百度等搜索引擎时,我们的搜索条件会在搜索结果中高亮显示(一般是标为红色),那具体是怎么样实现的呢,其实在搜索结果中,我们的搜索条件会被一个 <em></em>
标签包裹起来
那前端怎么知道我要给哪些内容添加高亮呢,前端工程师最多只能控制页面的排版,不能提前知道数据的内容
其实,添加标签这一步是由 ElasticSearch 完成的,ElasticSearch 为我们提供了高亮显示搜索词的功能
高亮显示的语法如下
示例
GET /shopping_mall/_search
{
"query": {
"match": {
"name": "脱脂牛奶"
}
},
"highlight": {
"fields": {
"name": {
"pre_tags": "<em>",
"post_tags": "</em>"
}
}
}
}
返回的结果如下
如果不指定标签,默认使用
<em></em>
标签
搜索的完整语法
15. JavaRestClient 查询
15.1 快速入门
数据搜索的 Java 代码我们分为两部分:
- 构建请求并发起请求
- 解析查询结果
具体的 Java 代码如下
import cn.edu.scau.pojo.ItemDocument;
import com.alibaba.fastjson2.JSON;
import org.apache.http.HttpHost;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestClient;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.SearchHits;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.io.IOException;
public class SearchTests {
private RestHighLevelClient restHighLevelClient;
@Test
public void testMatchAll() throws IOException {
// 1.创建 SearchRequest 对象
SearchRequest searchRequest = new SearchRequest("shopping_mall");
// 2.配置 request 参数
searchRequest.source().query(QueryBuilders.matchAllQuery());
// 3.发送请求
SearchResponse searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
// 4.解析结果
SearchHits searchHits = searchResponse.getHits();
// 4.1 获取总记录数
long totalHits = searchHits.getTotalHits().value;
System.err.println("总记录数 = " + totalHits);
// 4.2 获取命中的数据
for (SearchHit hit : searchHits) {
// 4.2.1 获取 source 结果
String sourceAsString = hit.getSourceAsString();
// 4.2.2 转换为 ItemDocument 对象
ItemDocument itemDocument = JSON.parseObject(sourceAsString, ItemDocument.class);
System.err.println("itemDocument = " + itemDocument);
}
}
@BeforeEach
public void setUp() {
restHighLevelClient = new RestHighLevelClient(RestClient.builder(
new HttpHost("127.0.0.1", 9200, "http")
));
}
@AfterEach
public void tearDown() throws Exception {
restHighLevelClient.close();
}
}
15.2 构造查询条件
在 JavaRestAPI 中,所有类型的 query 查询条件都是由 QueryBuilders 来构建的
全文检索的查询条件构造的 API 如下:
精确检索的查询条件构造的 API 如下:
布尔查询的查询条件构造 API 如下:
了解如何构造查询条件之后,我们来做一个小案例:利用 JavaRestClient 实现搜索功能,条件如下:
- 搜索关键字为脱脂牛奶
- 品牌必须为德亚
- 价格必须低于 300 元
具体的 Java 代码
import cn.edu.scau.pojo.ItemDocument;
import com.alibaba.fastjson2.JSON;
import org.apache.http.HttpHost;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestClient;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.SearchHits;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.io.IOException;
public class SearchTests {
private RestHighLevelClient restHighLevelClient;
@Test
public void testSearch() throws IOException {
// 1.创建 SearchRequest 对象
SearchRequest searchRequest = new SearchRequest("shopping_mall");
// 2.组织 DSL 参数
searchRequest.source().query(QueryBuilders.boolQuery()
.must(QueryBuilders.matchQuery("name", "脱脂牛奶"))
.filter(QueryBuilders.termQuery("brand", "德亚"))
.filter(QueryBuilders.rangeQuery("price").lt(30000))
);
// 3.发送请求
SearchResponse searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
// 4.解析结果
parseSearchResponse(searchResponse);
}
private static void parseSearchResponse(SearchResponse searchResponse) {
SearchHits searchHits = searchResponse.getHits();
// 4.1 获取总记录数
long totalHits = searchHits.getTotalHits().value;
System.err.println("总记录数 = " + totalHits);
// 4.2 获取命中的数据
for (SearchHit hit : searchHits) {
// 4.2.1 获取 source 结果
String sourceAsString = hit.getSourceAsString();
// 4.2.2 转换为 ItemDocument 对象
ItemDocument itemDocument = JSON.parseObject(sourceAsString, ItemDocument.class);
System.err.println("itemDocument = " + itemDocument);
}
}
@BeforeEach
public void setUp() {
restHighLevelClient = new RestHighLevelClient(RestClient.builder(
new HttpHost("127.0.0.1", 9200, "http")
));
}
@AfterEach
public void tearDown() throws Exception {
restHighLevelClient.close();
}
}
15.3 排序和分页
与 query 类似,排序和分页参数都是基于 request.source() 来设置的
具体的 Java 代码如下
@Test
public void testSortAndPage() throws IOException {
// 0.模拟前端传递过来的分页参数
int pageNumber = 1;
int pageSize = 10;
// 1.创建 SearchRequest 对象
SearchRequest searchRequest = new SearchRequest("shopping_mall");
// 2.组织 DSL 参数
// 2.1 query条件
searchRequest.source().query(QueryBuilders.matchAllQuery());
// 2.2 分页
searchRequest.source().from((pageNumber - 1) * pageSize).size(pageSize);
// 2.3 排序
searchRequest.source().sort("price", SortOrder.DESC);
// 3.发送请求
SearchResponse searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
// 4.解析结果
parseSearchResponse(searchResponse);
}
private static void parseSearchResponse(SearchResponse searchResponse) {
SearchHits searchHits = searchResponse.getHits();
// 4.1 获取总记录数
long totalHits = searchHits.getTotalHits().value;
System.err.println("总记录数 = " + totalHits);
// 4.2 获取命中的数据
for (SearchHit hit : searchHits) {
// 4.2.1 获取 source 结果
String sourceAsString = hit.getSourceAsString();
// 4.2.2 转换为 ItemDocument 对象
ItemDocument itemDocument = JSON.parseObject(sourceAsString, ItemDocument.class);
System.err.println("itemDocument = " + itemDocument);
}
}
15.4 高亮显示
高亮显示的条件构造 API 如下
具体的 Java 代码
@Test
public void testHighlight() throws IOException {
// 1.创建 SearchRequest 对象
SearchRequest searchRequest = new SearchRequest("shopping_mall");
// 2.组织 DSL 参数
// 2.1 query条件
searchRequest.source().query(QueryBuilders.matchQuery("name", "脱脂牛奶"));
// 2.2 高亮条件
searchRequest.source().highlighter(SearchSourceBuilder.highlight()
.field("name")
.preTags("<em>")
.postTags("</em>")
);
// 3.发送请求
SearchResponse searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
// 4.解析结果
parseSearchResponse(searchResponse);
}
private static void parseSearchResponse(SearchResponse searchResponse) {
SearchHits searchHits = searchResponse.getHits();
// 4.1 获取总记录数
long totalHits = searchHits.getTotalHits().value;
System.err.println("总记录数 = " + totalHits);
// 4.2 获取命中的数据
for (SearchHit hit : searchHits) {
// 4.2.1 获取 source 结果
String sourceAsString = hit.getSourceAsString();
// 4.2.2 转换为 ItemDocument 对象
ItemDocument itemDocument = JSON.parseObject(sourceAsString, ItemDocument.class);
// 4.3 处理高亮结果
Map<String, HighlightField> highlightFields = hit.getHighlightFields();
if (highlightFields != null && !highlightFields.isEmpty()) {
HighlightField highlightField = highlightFields.get("name");
String name = highlightField.getFragments()[0].toString();
itemDocument.setName(name);
}
System.err.println("itemDocument = " + itemDocument);
}
}
16. 数据聚合
ElasticSearch 不仅可以做数据的存储和搜索,还可以做海量数据的分析和运算,这种分析和运算的功能,被称为数据聚合
16.1 聚合的分类
聚合(aggregations)可以实现对文档数据的统计、分析、运算,常见的聚合有三类:
- 桶(Bucket)聚合:用来对文档做分组
- TermAggregation:按照文档字段值分组
- Date Histogram:按照日期阶梯分组,例如一周为一组,或者一月为一组
- 度量(Metric)聚合:用以计算一些值,比如最大值、最小值、平均值等
- Avg:求平均值
- Max:求最大值
- Min:求最小值
- Stats:同时求 max、min、avg、sum 等
- 管道(pipeline)聚合:以其它聚合的结果为基础做聚合
注意事项:
参与聚合的字段必须是不能分词的字段,例如 keyword、数值、日期、布尔等类型的字段
16.2 DSL 实现聚合
16.2.1 无条件的聚合
我们来做一个小案例:统计所有商品中共有哪些商品分类,其实就是以分类(category)字段对数据分组,category 值一样的放在同一组,属于 Bucket 聚合中的 Term 聚合
DSL 的语法如下
示例
GET /shopping_mall/_search
{
"query": {
"match_all": {}
},
"size": 0,
"aggs": {
"categoryAggregation": {
"terms": {
"field": "category",
"size": 20
}
}
}
}
返回的结果
{
"took" : 38,
"timed_out" : false,
"_shards" : {
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 10000,
"relation" : "gte"
},
"max_score" : null,
"hits" : [ ]
},
"aggregations" : {
"categoryAggregation" : {
"doc_count_error_upper_bound" : 0,
"sum_other_doc_count" : 0,
"buckets" : [
{
"key" : "休闲鞋",
"doc_count" : 20612
},
{
"key" : "牛仔裤",
"doc_count" : 19611
},
{
"key" : "老花镜",
"doc_count" : 16222
},
{
"key" : "拉杆箱",
"doc_count" : 14347
},
{
"key" : "手机",
"doc_count" : 10101
},
{
"key" : "真皮包",
"doc_count" : 3064
},
{
"key" : "拉拉裤",
"doc_count" : 1706
},
{
"key" : "牛奶",
"doc_count" : 1296
},
{
"key" : "曲面电视",
"doc_count" : 1219
},
{
"key" : "硬盘",
"doc_count" : 298
}
]
}
}
}
16.2.2 有条件的聚合
默认情况下,Bucket 聚合是对索引库的所有文档做聚合,我们可以限定要聚合的文档范围,只要添加 query 条件即可
例如,我想知道价格高于 3000 元的手机品牌有哪些
示例
GET /shopping_mall/_search
{
"query": {
"bool": {
"filter": [
{
"term": {
"category": "手机"
}
},
{
"range": {
"price": {
"gt": 300000
}
}
}
]
}
},
"size": 0,
"aggs": {
"brandAggregation": {
"terms": {
"field": "brand",
"size": 20
}
}
}
}
16.2.3 度量聚合
除了对数据分组(Bucket)以外,我们还可以对每个 Bucket 内的数据进一步做数据计算和统计
例如:我想知道手机有哪些品牌,每个品牌的价格最小值、最大值、平均值
GET /shopping_mall/_search
{
"query": {
"term": {
"category": "手机"
}
},
"size": 0,
"aggs": {
"brandAggregation": {
"terms": {
"field": "brand",
"size": 20
},
"aggs": {
"priceStatistics": {
"stats": {
"field": "price"
}
}
}
}
}
}
16.3 Java 客户端实现聚合
聚合三要素:
- 聚合类型
- 聚合名称
- 聚合字段
我们以查询品牌为例
具体的 Java 代码
Terms 类所在的包为
org.elasticsearch.search.aggregations.bucket.terms.Terms
@Test
public void testAggregation() throws IOException {
// 1.创建 SearchRequest 对象
SearchRequest searchRequest = new SearchRequest("shopping_mall");
// 2.组织 DSL 参数
// 2.1 分页
searchRequest.source().size(0);
// 2.2 聚合条件
String brandAggregationName = "brandAggregationName";
searchRequest.source().aggregation(
AggregationBuilders.terms(brandAggregationName).field("brand").size(20)
);
// 3.发送请求
SearchResponse searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
// 4.解析结果
Aggregations aggregations = searchResponse.getAggregations();
// 4.1 根据聚合名称获取对应的聚合结果
Terms brandTerms = aggregations.get(brandAggregationName);
// 4.2 获取buckets
List<? extends Terms.Bucket> buckets = brandTerms.getBuckets();
// 4.3 遍历获取每一个bucket
for (Terms.Bucket bucket : buckets) {
System.out.println("brand = " + bucket.getKeyAsString());
System.out.println("docCount = " + bucket.getDocCount());
}
}
注意事项:
- aggregations.get(brandAggregationName) 方法返回的是顶层的 Aggregation 接口,由于 aggregations.get(String name) 是一个统一的 API ,也就是说,设计者在设计这个 API 的时候不知道使用者会使用哪种聚合,所以使用顶层的 Aggregation 接口来接收
- Aggregation 接口有很多的子子孙孙,代表不同类型的聚合,我们使用的 Terms 接口实现了 Aggregation 接口,Terms 接口中有获取桶的方法
- 不同类型的聚合,结果也大不相同,我们使用的是 term 分组聚合,所以才能得到桶,如果不是使用 term 分组聚合,结果中不会有桶,所以顶层接口中不会有获取桶的方法