# SpringBoot集成Elasticsearch企业级实战教程
# 1. 环境准备
# 1.1 开发环境确认
确保本地已配置以下环境:
- IDEA 2023 Ultimate 版
- JDK 17+:SpringBoot 3.5.10(博主前几篇还是用的3.5.9,最近更新了3.5.10,索性就用最新版了)
- Maven 3.9.4:使用 IDEA 自带版本,仅需配置阿里云镜像加速依赖下载
- Elasticsearch 8.15.0:远程服务地址
http://localhost:9200,账号kelsen,密码admin123456
# 1.2 Maven 镜像配置
修改 IDEA 自带 Maven 的 settings.xml(路径:IDEA安装目录/plugins/maven/lib/maven3/conf/settings.xml),添加阿里云镜像:
<mirrors>
<mirror>
<id>aliyunmaven</id>
<mirrorOf>central</mirrorOf>
<url>https://maven.aliyun.com/repository/public</url>
</mirror>
</mirrors>
2
3
4
5
6
7
# 2. 新建 SpringBoot 项目
# 2.1 项目创建步骤

# 2.2 完善 pom.xml 依赖
SpringBoot 3.5.10 已内置与 ES 8.15.0 匹配的依赖版本,无需手动指定 ES 版本,完整 pom.xml 如下:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.5.10</version>
<relativePath/>
</parent>
<groupId>cc.hep</groupId>
<artifactId>SpringBoot-Elasticsearch</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>SpringBoot-Elasticsearch</name>
<description>SpringBoot 3.5.10 集成 Elasticsearch 8.15.0 企业级项目</description>
<properties>
<java.version>17</java.version>
<elasticsearch.version>8.15.0</elasticsearch.version>
</properties>
<dependencies>
<!-- Spring Web 核心依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring Data Elasticsearch 集成依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>
<!-- Lombok 简化实体类编写 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- SpringBoot 测试依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.0.0-M5</version> <!-- 使用最新版本 -->
<configuration>
<skipTests>true</skipTests>
</configuration>
</plugin>
</plugins>
</build>
</project>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
# 3. 核心配置
# 3.1 application.yml 配置
在 src/main/resources 下创建 application.yml(替代默认的 properties),配置 ES 连接信息和项目基础参数:
server:
port: 8080
servlet:
context-path: /es-demo
spring:
elasticsearch:
uris: http://localhost:9200
username: kelsen
password: admin123456
2
3
4
5
6
7
8
9
10
# 3.2 Elasticsearch 客户端配置类
企业级项目需自定义 ES 客户端配置,保证连接稳定性和可扩展性,在 cc.hep.config 包下创建 ElasticsearchConfig.java:
package cc.hep.config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.elasticsearch.client.ClientConfiguration;
import org.springframework.data.elasticsearch.client.elc.ElasticsearchConfiguration;
import org.springframework.data.elasticsearch.repository.config.EnableElasticsearchRepositories;
import java.net.URI;
import java.time.Duration;
import java.util.Arrays;
@Configuration
@EnableElasticsearchRepositories(basePackages = "cc.hep.repository")
public class ElasticsearchConfig extends ElasticsearchConfiguration {
@Value("${spring.elasticsearch.uris:http://localhost:9200}")
private String esUris;
@Value("${spring.elasticsearch.username:kelsen}")
private String username;
@Value("${spring.elasticsearch.password:admin123456}")
private String password;
@Override
public ClientConfiguration clientConfiguration() {
// 解析多个 URI(支持逗号分隔的多个地址)
String[] hostAndPorts = Arrays.stream(esUris.split(","))
.map(uri -> {
URI parsedUri = URI.create(uri);
return parsedUri.getHost() + ":" + parsedUri.getPort();
})
.toArray(String[]::new);
return ClientConfiguration.builder()
.connectedTo(hostAndPorts)
.withBasicAuth(username, password)
.withConnectTimeout(Duration.ofSeconds(10))
.withSocketTimeout(Duration.ofSeconds(30))
.build();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
# 4. 企业级工程结构搭建
# 4.1 标准包结构
遵循企业级分层架构,创建以下包结构(cc.hep 下):
cc.hep
├── config // 配置类(ES、全局配置等)
├── controller // 控制层(HTTP 接口)
├── entity // 实体类(映射 ES 文档)
├── exception // 异常处理(自定义异常、全局处理器)
├── repository // ES 数据访问层(Repository)
├── service // 业务层(接口 + 实现)
│ └── impl // 业务层实现
├── util // 工具类(返回结果、常量等)
└── SpringBootElasticsearchApplication.java // 启动类
2
3
4
5
6
7
8
9
10
# 4.2 核心实体类设计(示例:商品)
在 cc.hep.entity 包下创建 Product.java,映射 ES 索引文档(企业级字段设计):
package cc.hep.entity;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.data.annotation.Id;
import org.springframework.data.elasticsearch.annotations.*;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* 商品实体类
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Document(indexName = "product_index")
public class Product {
@Id
private String id;
//如果你安装了ICU分词器,也可以使用ICU分词。
// 在 Elasticsearch 服务器上执行
//cd /usr/share/elasticsearch
// 安装 ICU 分词器
//sudo bin/elasticsearch-plugin install analysis-icu
@Field(type = FieldType.Text, analyzer = "icu_analyzer", searchAnalyzer = "icu_analyzer")
//@Field(type = FieldType.Text, analyzer = "standard", searchAnalyzer = "standard")
private String productName;
@Field(type = FieldType.Keyword)
private String category;
@Field(type = FieldType.Double)
private BigDecimal price;
@Field(type = FieldType.Integer)
private Integer stock;
@Field(type = FieldType.Boolean)
private Boolean isOnSale;
@Field(type = FieldType.Date, format = DateFormat.date_hour_minute_second)
private LocalDateTime createTime;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
# 4.3 统一返回结果工具类
企业级项目需统一接口返回格式,在 cc.hep.util 包下创建 Result.java:
package cc.hep.util;
import lombok.AllArgsConstructor;
import lombok.Data;
/**
* 统一返回结果类(企业级规范)
*/
@Data
@AllArgsConstructor
public class Result<T> {
/**
* 响应码(200成功,500失败)
*/
private Integer code;
/**
* 响应消息
*/
private String msg;
/**
* 响应数据
*/
private T data;
// 成功响应(无数据)
public static <T> Result<T> success() {
return new Result<>(200, "操作成功", null);
}
// 成功响应(有数据)
public static <T> Result<T> success(T data) {
return new Result<>(200, "操作成功", data);
}
// 失败响应
public static <T> Result<T> error(String msg) {
return new Result<>(500, msg, null);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
# 5. 核心功能实现
# 5.1 索引管理(创建/删除/判断存在)
企业级项目需手动管理索引(避免自动创建的索引字段类型不规范),在 cc.hep.service 下创建 IndexService.java 接口:
package cc.hep.service;
/**
* 索引管理服务接口
*/
public interface IndexService {
/**
* 创建索引
*/
boolean createIndex(String indexName);
/**
* 创建商品索引(硬编码mapping)
*/
boolean createProductIndex();
/**
* 删除索引
*/
boolean deleteIndex(String indexName);
/**
* 判断索引是否存在
*/
boolean existsIndex(String indexName);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
在 cc.hep.service.impl 下创建实现类 IndexServiceImpl.java:
package cc.hep.service.impl;
import cc.hep.service.IndexService;
import co.elastic.clients.elasticsearch.ElasticsearchClient;
import co.elastic.clients.elasticsearch.indices.CreateIndexResponse;
import co.elastic.clients.elasticsearch.indices.DeleteIndexResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.io.IOException;
/**
* 简化版索引管理实现
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class IndexServiceImpl implements IndexService {
private final ElasticsearchClient elasticsearchClient;
@Override
public boolean createIndex(String indexName) {
try {
// 1. 判断索引是否存在
if (existsIndex(indexName)) {
log.warn("索引 {} 已存在,无需重复创建", indexName);
return false;
}
// 2. 创建简单索引
CreateIndexResponse response = elasticsearchClient.indices().create(builder ->
builder
.index(indexName)
.settings(s -> s
.numberOfShards("1")
.numberOfReplicas("0")
)
);
boolean acknowledged = response.acknowledged();
log.info("索引 {} 创建{}", indexName, acknowledged ? "成功" : "失败");
return acknowledged;
} catch (IOException e) {
log.error("创建索引 {} 失败", indexName, e);
throw new RuntimeException("创建索引失败:" + e.getMessage(), e);
}
}
@Override
public boolean deleteIndex(String indexName) {
try {
if (!existsIndex(indexName)) {
log.warn("索引 {} 不存在,无需删除", indexName);
return false;
}
DeleteIndexResponse response = elasticsearchClient.indices().delete(builder ->
builder.index(indexName)
);
boolean acknowledged = response.acknowledged();
log.info("索引 {} 删除{}", indexName, acknowledged ? "成功" : "失败");
return acknowledged;
} catch (IOException e) {
log.error("删除索引 {} 失败", indexName, e);
throw new RuntimeException("删除索引失败:" + e.getMessage(), e);
}
}
@Override
public boolean existsIndex(String indexName) {
try {
return elasticsearchClient.indices().exists(builder ->
builder.index(indexName)
).value();
} catch (IOException e) {
log.error("判断索引 {} 是否存在失败", indexName, e);
throw new RuntimeException("判断索引存在性失败:" + e.getMessage(), e);
}
}
@Override
public boolean createProductIndex() {
String indexName = "product_index";
try {
if (existsIndex(indexName)) {
log.warn("索引 {} 已存在,无需重复创建", indexName);
return false;
}
// 创建商品索引并设置mapping
CreateIndexResponse response = elasticsearchClient.indices().create(builder ->
builder
.index(indexName)
.settings(s -> s
.numberOfShards("1")
.numberOfReplicas("0")
)
.mappings(m -> m
.properties("id", p -> p.keyword(k -> k))
.properties("product_name", p -> p.text(t -> t
.analyzer("ik_max_word")
.searchAnalyzer("ik_smart")
))
.properties("category", p -> p.keyword(k -> k))
.properties("price", p -> p.double_(d -> d))
.properties("stock", p -> p.integer(i -> i))
.properties("is_on_sale", p -> p.boolean_(b -> b))
.properties("create_time", p -> p.date(d -> d.format("yyyy-MM-dd HH:mm:ss")))
)
);
boolean acknowledged = response.acknowledged();
log.info("商品索引 {} 创建{}", indexName, acknowledged ? "成功" : "失败");
return acknowledged;
} catch (IOException e) {
log.error("创建商品索引 {} 失败", indexName, e);
throw new RuntimeException("创建索引失败:" + e.getMessage(), e);
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
# 5.2 文档 CRUD 操作(核心业务)
# 5.2.1 数据访问层(Repository)
在 cc.hep.repository 下创建 ProductRepository.java(继承 Spring Data ES 接口,简化 CRUD):
package cc.hep.repository;
import cc.hep.entity.Product;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface ProductRepository extends ElasticsearchRepository<Product, String> {
/**
* 根据商品名称查询(模糊匹配)
*/
Page<Product> findByProductName(String productName, Pageable pageable);
/**
* 根据分类查询
*/
List<Product> findByCategory(String category);
/**
* 根据价格范围查询
*/
List<Product> findByPriceBetween(Double minPrice, Double maxPrice);
/**
* 根据是否上架查询
*/
List<Product> findByIsOnSale(Boolean isOnSale);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# 5.2.2 业务层(Service)
创建 ProductService.java 接口:
package cc.hep.service;
import cc.hep.entity.Product;
import org.springframework.data.domain.Page;
import java.util.List;
/**
* 商品业务服务接口
*/
public interface ProductService {
Product saveProduct(Product product);
Product getProductById(String id);
Product updateProduct(Product product);
void deleteProductById(String id);
Iterable<Product> batchSaveProducts(List<Product> products);
void batchDeleteProducts(List<String> ids);
Page<Product> queryProductsByCondition(String keyword, String category, Integer pageNum, Integer pageSize);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
创建实现类 ProductServiceImpl.java:
package cc.hep.service.impl;
import cc.hep.entity.Product;
import cc.hep.repository.ProductRepository;
import cc.hep.service.ProductService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import java.util.List;
import java.util.Optional;
/**
* 商品业务服务实现类
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class ProductServiceImpl implements ProductService {
private final ProductRepository productRepository;
@Override
public Product saveProduct(Product product) {
if (product.getCreateTime() == null) {
product.setCreateTime(java.time.LocalDateTime.now());
}
if (product.getIsOnSale() == null) {
product.setIsOnSale(true);
}
Product saved = productRepository.save(product);
log.info("新增商品成功,ID:{}", saved.getId());
return saved;
}
@Override
public Product getProductById(String id) {
Optional<Product> optional = productRepository.findById(id);
return optional.orElse(null);
}
@Override
public Product updateProduct(Product product) {
Product existProduct = getProductById(product.getId());
if (existProduct == null) {
throw new RuntimeException("商品不存在,ID:" + product.getId());
}
// 只更新非空字段
if (StringUtils.hasText(product.getProductName())) {
existProduct.setProductName(product.getProductName());
}
if (product.getCategory() != null) {
existProduct.setCategory(product.getCategory());
}
if (product.getPrice() != null) {
existProduct.setPrice(product.getPrice());
}
if (product.getStock() != null) {
existProduct.setStock(product.getStock());
}
if (product.getIsOnSale() != null) {
existProduct.setIsOnSale(product.getIsOnSale());
}
Product updated = productRepository.save(existProduct);
log.info("更新商品成功,ID:{}", updated.getId());
return updated;
}
@Override
public void deleteProductById(String id) {
if (!productRepository.existsById(id)) {
throw new RuntimeException("商品不存在,ID:" + id);
}
productRepository.deleteById(id);
log.info("删除商品成功,ID:{}", id);
}
@Override
public Iterable<Product> batchSaveProducts(List<Product> products) {
if (products == null || products.isEmpty()) {
throw new RuntimeException("批量新增的商品列表不能为空");
}
products.forEach(product -> {
if (product.getCreateTime() == null) {
product.setCreateTime(java.time.LocalDateTime.now());
}
if (product.getIsOnSale() == null) {
product.setIsOnSale(true);
}
});
Iterable<Product> saved = productRepository.saveAll(products);
log.info("批量新增商品成功,数量:{}", products.size());
return saved;
}
@Override
public void batchDeleteProducts(List<String> ids) {
if (ids == null || ids.isEmpty()) {
throw new RuntimeException("批量删除的商品ID列表不能为空");
}
ids.forEach(id -> {
if (productRepository.existsById(id)) {
productRepository.deleteById(id);
}
});
log.info("批量删除商品成功,数量:{}", ids.size());
}
@Override
public Page<Product> queryProductsByCondition(String keyword, String category, Integer pageNum, Integer pageSize) {
// 使用Repository的简单查询方法
// 更复杂的查询建议在Repository中定义自定义方法
Pageable pageable = PageRequest.of(
Math.max(pageNum - 1, 0), // 确保页码非负
Math.max(pageSize, 1), // 确保页大小大于0
Sort.by(Sort.Direction.DESC, "createTime")
);
// 简单实现:查询所有,实际项目应该根据keyword和category进行过滤
return productRepository.findAll(pageable);
}
/**
* 根据商品名称搜索
*/
public Page<Product> searchByProductName(String productName, Pageable pageable) {
return productRepository.findByProductName(productName, pageable);
}
/**
* 根据分类查询
*/
public List<Product> findByCategory(String category) {
return productRepository.findByCategory(category);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
# 5.3 控制层(HTTP 接口)
在 cc.hep.controller 下创建 ProductController.java,提供 RESTful 接口:
package cc.hep.controller;
import cc.hep.entity.Product;
import cc.hep.service.IndexService;
import cc.hep.service.ProductService;
import cc.hep.util.Result;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/product")
@RequiredArgsConstructor
public class ProductController {
private final ProductService productService;
private final IndexService indexService;
/**
* 创建商品索引
*/
@PostMapping("/initIndex")
public Result<Boolean> initIndex() {
try {
boolean success = indexService.createProductIndex();
return Result.success(success);
} catch (Exception e) {
return Result.error("初始化商品索引失败: " + e.getMessage());
}
}
/**
* 新增商品
*/
@PostMapping("/save")
public Result<Product> saveProduct(@RequestBody Product product) {
try {
Product saved = productService.saveProduct(product);
return Result.success(saved);
} catch (Exception e) {
return Result.error("新增商品失败: " + e.getMessage());
}
}
/**
* 根据ID查询商品
*/
@GetMapping("/{id}")
public Result<Product> getProduct(@PathVariable String id) {
try {
Product product = productService.getProductById(id);
return product != null ? Result.success(product) : Result.error("商品不存在");
} catch (Exception e) {
return Result.error("查询商品失败: " + e.getMessage());
}
}
/**
* 批量新增商品
*/
@PostMapping("/batchSave")
public Result<List<Product>> batchSaveProducts(@RequestBody List<Product> products) {
try {
Iterable<Product> saved = productService.batchSaveProducts(products);
return Result.success((List<Product>) saved);
} catch (Exception e) {
return Result.error("批量新增商品失败: " + e.getMessage());
}
}
/**
* 分页查询商品
*/
@GetMapping("/page")
public Result<Page<Product>> pageProducts(
@RequestParam(defaultValue = "1") Integer pageNum,
@RequestParam(defaultValue = "10") Integer pageSize) {
try {
Page<Product> page = productService.queryProductsByCondition(null, null, pageNum, pageSize);
return Result.success(page);
} catch (Exception e) {
return Result.error("分页查询商品失败: " + e.getMessage());
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
# 5.4 控制层
在 cc.hep.controller 下创建 IndexController.java,提供 RESTful 接口:
package cc.hep.controller;
import cc.hep.service.IndexService;
import cc.hep.util.Result;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/index")
@RequiredArgsConstructor
public class IndexController {
private final IndexService indexService;
@PostMapping("/create")
public Result<Boolean> createIndex(@RequestParam String indexName) {
try {
boolean success = indexService.createIndex(indexName);
return Result.success(success);
} catch (Exception e) {
return Result.error("创建索引失败: " + e.getMessage());
}
}
@PostMapping("/createProductIndex")
public Result<Boolean> createProductIndex() {
try {
boolean success = indexService.createProductIndex();
return Result.success(success);
} catch (Exception e) {
return Result.error("创建商品索引失败: " + e.getMessage());
}
}
@DeleteMapping("/delete")
public Result<Boolean> deleteIndex(@RequestParam String indexName) {
try {
boolean success = indexService.deleteIndex(indexName);
return Result.success(success);
} catch (Exception e) {
return Result.error("删除索引失败: " + e.getMessage());
}
}
@GetMapping("/exists")
public Result<Boolean> existsIndex(@RequestParam String indexName) {
try {
boolean exists = indexService.existsIndex(indexName);
return Result.success(exists);
} catch (Exception e) {
return Result.error("检查索引失败: " + e.getMessage());
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
# 6. 统一异常处理(企业级必备)
# 6.1 自定义异常类
在 cc.hep.exception 下创建 EsBusinessException.java:
package cc.hep.exception;
/**
* ES 业务自定义异常
*/
public class EsBusinessException extends RuntimeException {
public EsBusinessException(String message) {
super(message);
}
public EsBusinessException(String message, Throwable cause) {
super(message, cause);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 6.2 全局异常处理器
在 cc.hep.exception 下创建 GlobalExceptionHandler.java:
package cc.hep.exception;
import cc.hep.util.Result;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
/**
* 全局异常处理器(企业级)
*/
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
/**
* 处理自定义业务异常
*/
@ExceptionHandler(EsBusinessException.class)
public Result<?> handleEsBusinessException(EsBusinessException e) {
log.error("业务异常:", e);
return Result.error(e.getMessage());
}
/**
* 处理运行时异常
*/
@ExceptionHandler(RuntimeException.class)
public Result<?> handleRuntimeException(RuntimeException e) {
log.error("运行时异常:", e);
return Result.error("系统异常:" + e.getMessage());
}
/**
* 处理所有异常
*/
@ExceptionHandler(Exception.class)
public Result<?> handleException(Exception e) {
log.error("系统异常:", e);
return Result.error("服务器内部错误,请联系管理员");
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
# 7. Windows CMD测试
# 1. 创建商品索引
curl -X POST "http://localhost:8080/es-demo/api/index/createProductIndex"
# 2. 新增单个商品
curl -X POST "http://localhost:8080/es-demo/api/product/save" ^
-H "Content-Type: application/json" ^
-d "{\"productName\":\"华为Mate70 Pro\",\"category\":\"手机\",\"price\":6999.00,\"stock\":1000,\"isOnSale\":true}"
# 3. 获取ID并查询(假设返回的ID是:abc123def456)
curl -X GET "http://localhost:8080/es-demo/api/product/abc123def456"
# 4. 分页查询
curl -X GET "http://localhost:8080/es-demo/api/product/page?pageNum=1&pageSize=10"
# 5. 批量新增
curl -X POST "http://localhost:8080/es-demo/api/product/batchSave" ^
-H "Content-Type: application/json" ^
-d "[{\"productName\":\"iPhone 15\",\"category\":\"手机\",\"price\":5999.00,\"stock\":500,\"isOnSale\":true},{\"productName\":\"小米14\",\"category\":\"手机\",\"price\":3999.00,\"stock\":800,\"isOnSale\":true}]"
# 6. 检查索引是否存在
curl -X GET "http://localhost:8080/es-demo/api/index/exists?indexName=product_index"
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 8. 企业级优化建议
# 8.1 连接池优化
- 配置 ES 客户端连接池大小,避免连接数过多导致服务崩溃;
- 增加连接重试机制,提升服务稳定性。
# 8.2 索引设计优化
- 索引名规范:小写 + 下划线(如
product_index); - 分片和副本:根据数据量设置(建议分片数 = 节点数 × 1.5~3);
- 字段类型:避免使用
text类型做排序/聚合,优先使用keyword或新增子字段。
# 8.3 性能调优
- 批量操作:每次批量处理 1000~5000 条数据,避免单次操作过多;
- 查询优化:使用过滤查询(Filter)替代普通查询,利用缓存提升性能;
- 避免深度分页:使用
search_after替代from + size实现深分页。
# 8.4 监控与日志
- 集成 Elasticsearch 监控工具(如 Cerebro、Kibana);
- 记录 ES 操作日志,包含请求参数、响应时间、执行结果,便于问题排查。
# 总结
- 本次教程基于 SpringBoot 3.5.10 + Elasticsearch 8.15.0 搭建了企业级项目,核心包含索引管理、文档 CRUD、批量操作、高级查询等功能;
- 项目遵循分层架构设计,包含统一返回结果、全局异常处理等企业级规范,可直接复用至生产环境;
- 关键优化点:索引分片/副本配置、批量操作大小控制、查询条件优化,是提升 ES 性能的核心手段。