Spring Boot 整合 Elasticsearch 是企业级开发中常见的需求,用于实现高效的全文检索、日志分析等功能。以下是整合的核心步骤和常用方法大全,涵盖从基础配置到高级操作的完整流程。
Spring Boot 版本 | Spring Data Elasticsearch 版本 | Elasticsearch 版本 |
---|---|---|
2.7.x | 4.4.x | 7.10 - 8.6 |
3.0.x 及以上 | 5.0.x | 8.0+ |
在 pom.xml
中添加核心依赖(以 Spring Boot 3.x + Elasticsearch 8.x 为例):
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-data-elasticsearch
org.elasticsearch.client
elasticsearch-rest-high-level-client
org.springframework.boot
spring-boot-starter-test
test
在 application.yml
或 application.properties
中配置 ES 节点地址、认证信息(如有)等:
spring:
elasticsearch:
uris: http://localhost:9200 # ES 服务地址(集群用逗号分隔)
username: elastic # 可选(若 ES 开启了安全认证)
password: your-password # 可选
connect-timeout: 5000 # 连接超时时间(ms)
socket-timeout: 30000 # Socket 超时时间(ms)
若需要更细粒度控制(如连接池、线程池),可自定义 ElasticsearchClient
或 RestHighLevelClient
:
@Configuration
public class ElasticsearchConfig {
@Bean
public RestHighLevelClient restHighLevelClient(RestClientBuilder builder) {
return new RestHighLevelClient(builder);
}
// 或直接使用 Spring Data 提供的 ElasticsearchRestTemplate(已过时,新版推荐 ElasticsearchClient)
// @Bean
// public ElasticsearchRestTemplate elasticsearchRestTemplate(RestHighLevelClient client) {
// return new ElasticsearchRestTemplate(client);
// }
}
通过注解将 Java 对象映射为 ES 文档,关键注解包括:
注解 | 说明 |
---|---|
@Document |
标记类为 ES 文档,indexName 指定索引名,shards /replicas 分片配置。 |
@Id |
标记主键字段(自动生成或手动指定)。 |
@Field |
标记字段,type 指定 ES 数据类型(如 Text 、Keyword 、Date 等)。 |
@CreateDate |
自动填充文档创建时间(需配合 @Field(type = Date) )。 |
@UpdateDate |
自动填充文档更新时间。 |
@Data
@Document(indexName = "user_index", shards = 3, replicas = 1) // 索引名、分片、副本
public class User {
@Id
private String id; // ES 文档 ID(自动生成时可为 null)
@Field(type = FieldType.Text, analyzer = "ik_max_word") // 使用 IK 分词器
private String name;
@Field(type = FieldType.Keyword) // 精确匹配字段(不分词)
private String email;
@Field(type = FieldType.Date, format = DateFormat.custom, pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createTime;
@Field(type = FieldType.Integer)
private Integer age;
}
通过 ElasticsearchRestTemplate
或 ElasticsearchClient
管理索引(创建、删除、查看等)。
@Document
)。@Service
public class IndexService {
@Autowired
private ElasticsearchRestTemplate restTemplate;
// 手动创建索引(带自定义映射)
public boolean createIndex(String indexName) {
// 定义索引设置(分片、副本)
Settings settings = Settings.builder()
.put("number_of_shards", 3)
.put("number_of_replicas", 1)
.build();
// 定义映射(字段类型、分词器等)
XContentBuilder mapping = XContentFactory.jsonBuilder()
.startObject()
.startObject("properties")
.startObject("name")
.field("type", "text")
.field("analyzer", "ik_max_word")
.endObject()
.startObject("email")
.field("type", "keyword")
.endObject()
.endObject()
.endObject();
CreateIndexRequest request = new CreateIndexRequest(indexName)
.settings(settings)
.mapping(mapping);
try {
return restTemplate.getClusterClient().indices().create(request, RequestOptions.DEFAULT).isAcknowledged();
} catch (IOException e) {
throw new RuntimeException("创建索引失败", e);
}
}
// 删除索引
public boolean deleteIndex(String indexName) {
DeleteIndexRequest request = new DeleteIndexRequest(indexName);
try {
return restTemplate.getClusterClient().indices().delete(request, RequestOptions.DEFAULT).isAcknowledged();
} catch (IOException e) {
throw new RuntimeException("删除索引失败", e);
}
}
// 查看索引是否存在
public boolean existsIndex(String indexName) {
GetIndexRequest request = new GetIndexRequest(indexName);
try {
return restTemplate.getClusterClient().indices().exists(request, RequestOptions.DEFAULT);
} catch (IOException e) {
throw new RuntimeException("检查索引失败", e);
}
}
}
通过 ElasticsearchRestTemplate
或 ElasticsearchClient
实现文档的增删改查。
@Service
public class UserService {
@Autowired
private ElasticsearchRestTemplate restTemplate;
// 保存单个文档(自动生成 ID,若已存在则覆盖)
public User saveUser(User user) {
return restTemplate.save(user); // 若 user.getId() 为 null,ES 自动生成 UUID
}
// 保存多个文档
public List saveAllUsers(List users) {
return restTemplate.saveAll(users);
}
}
// 根据 ID 查询单个文档
public User getUserById(String id) {
return restTemplate.findById(id, User.class);
}
// 批量查询(根据 ID 列表)
public List getUsersByIds(List ids) {
MultiGetQueryRequest request = new MultiGetQueryRequest()
.addIds("user_index", ids.toArray(new String[0]));
MultiGetResponse response = restTemplate.getClusterClient()
.mget(request, RequestOptions.DEFAULT);
return Arrays.stream(response.getResponses())
.map(multiGetItemResponse -> {
if (!multiGetItemResponse.isFailed() && multiGetItemResponse.getResponse() != null) {
return multiGetItemResponse.getResponse().getSourceAsMap(); // 需转换为 User 对象
}
return null;
})
.filter(Objects::nonNull)
.collect(Collectors.toList());
}
// 方式1:全量更新(替换整个文档)
public User updateUser(User user) {
return restTemplate.save(user); // 直接覆盖原文档
}
// 方式2:部分更新(使用 UpdateRequest)
public User partialUpdateUser(String id, Map updates) {
UpdateRequest request = new UpdateRequest("user_index", id)
.doc(updates) // 部分更新(仅修改指定字段)
.detectNoop(false); // 强制更新(即使内容未变)
try {
UpdateResponse response = restTemplate.getClusterClient().update(request, RequestOptions.DEFAULT);
return restTemplate.mapResponse(response, User.class); // 将响应映射为 User 对象
} catch (IOException e) {
throw new RuntimeException("更新文档失败", e);
}
}
// 根据 ID 删除单个文档
public boolean deleteUser(String id) {
DeleteRequest request = new DeleteRequest("user_index", id);
try {
DeleteResponse response = restTemplate.getClusterClient().delete(request, RequestOptions.DEFAULT);
return response.getResult() == DocWriteResponse.Result.DELETED;
} catch (IOException e) {
throw new RuntimeException("删除文档失败", e);
}
}
// 批量删除
public void deleteUsersByIds(List ids) {
BulkRequest bulkRequest = new BulkRequest();
ids.forEach(id -> bulkRequest.add(new DeleteRequest("user_index", id)));
try {
restTemplate.getClusterClient().bulk(bulkRequest, RequestOptions.DEFAULT);
} catch (IOException e) {
throw new RuntimeException("批量删除失败", e);
}
}
ES 的查询非常灵活,支持全文检索、过滤、聚合等。Spring Data 提供了 Query
接口和 NativeSearchQuery
来构建查询。
// 构建查询(使用 QueryBuilders)
public List searchUsers(String keyword) {
// 1. 构建 Bool 查询(must 表示必须满足的条件)
BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
// 2. 添加全文检索条件(对 name 字段分词后匹配)
boolQuery.must(QueryBuilders.matchQuery("name", keyword));
// 3. 添加过滤条件(精确匹配 age > 18)
boolQuery.filter(QueryBuilders.rangeQuery("age").gt(18));
// 4. 构建 NativeSearchQuery
NativeSearchQuery query = new NativeSearchQueryBuilder()
.withQuery(boolQuery)
.build();
// 5. 执行查询
SearchHits searchHits = restTemplate.search(query, User.class);
return searchHits.getSearchHits().stream()
.map(SearchHit::getContent)
.collect(Collectors.toList());
}
// 模糊查询(fuzzy) + 范围查询(range) + 高亮显示(highlight)
public List
ES 支持指标聚合(如求和、平均值)、桶聚合(如分组统计)等。
示例:统计各年龄段的用户数量
public Map countUsersByAgeGroup() {
// 构建桶聚合(按年龄分段:20-29, 30-39, 40+)
TermsAggregationBuilder ageGroup = AggregationBuilders.terms("age_group")
.field("age")
.script(new Script("doc['age'].value.int / 10 * 10 + '0-' + (doc['age'].value.int / 10 * 10 + 10)"))
.order(BucketOrder.key(true));
// 构建查询(无过滤条件,仅聚合)
NativeSearchQuery query = new NativeSearchQueryBuilder()
.withAggregations(ageGroup)
.build();
// 执行查询并解析聚合结果
SearchHits searchHits = restTemplate.search(query, User.class);
Aggregations aggregations = searchHits.getAggregations();
// 解析桶聚合结果
Terms ageTerms = aggregations.get("age_group");
Map result = new HashMap<>();
for (Terms.Bucket bucket : ageTerms.getBuckets()) {
String key = bucket.getKeyAsString(); // 年龄段(如 "20-30")
long count = bucket.getDocCount(); // 数量
result.put(key, count);
}
return result;
}
批量操作(如批量导入、更新)可显著提升性能,使用 BulkRequest
实现。
// 批量插入/更新文档
public void bulkInsertOrUpdate(List users) {
BulkRequest bulkRequest = new BulkRequest();
users.forEach(user -> {
IndexRequest indexRequest = new IndexRequest("user_index")
.id(user.getId())
.source(JSON.toJSONString(user), XContentType.JSON); // 手动构造请求
bulkRequest.add(indexRequest);
});
// 设置批量操作参数(可选)
bulkRequest.timeout(TimeValue.timeValueSeconds(10)); // 超时时间
bulkRequest.maxRetries(3); // 最大重试次数
try {
// 执行批量操作
BulkResponse bulkResponse = restTemplate.getClusterClient().bulk(bulkRequest, RequestOptions.DEFAULT);
if (bulkResponse.hasFailures()) {
// 处理失败项
bulkResponse.forEach(response -> {
if (response.isFailed()) {
log.error("批量操作失败:{}", response.getFailureMessage());
}
});
}
} catch (IOException e) {
throw new RuntimeException("批量操作失败", e);
}
}
若需支持拼音搜索(如搜索“张三”时匹配“zhangsan”),可使用 ik-analyzer-pinyin
插件:
@Field(type = FieldType.Text, analyzer = "ik_max_word", searchAnalyzer = "ik_pinyin_analyzer")
private String name;
若需基于地理位置搜索(如查找附近的商店),可使用 geo_point
类型:
// 实体类中添加地理位置字段
@Field(type = FieldType.GeoPoint)
private GeoPoint location; // { "lat": 30.123, "lon": 120.456 }
// 查询附近 5km 内的文档
public List searchNearby(double lat, double lon, double distance) {
BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
boolQuery.filter(QueryBuilders.geoDistanceQuery("location")
.point(lat, lon)
.distance(distance, DistanceUnit.KILOMETERS));
NativeSearchQuery query = new NativeSearchQueryBuilder()
.withQuery(boolQuery)
.build();
SearchHits searchHits = restTemplate.search(query, User.class);
return searchHits.getSearchHits().stream()
.map(SearchHit::getContent)
.collect(Collectors.toList());
}
整合过程中可能遇到以下异常,需针对性处理:
@Field
注解的 type
是否与 ES 索引中的字段类型一致。全局异常处理示例:
@RestControllerAdvice
public class ElasticsearchExceptionHandler {
@ExceptionHandler(ElasticsearchStatusException.class)
public ResponseEntity handleElasticsearchException(ElasticsearchStatusException e) {
return ResponseEntity.status(e.status()).body("ES 操作失败:" + e.getMessage());
}
@ExceptionHandler(IOException.class)
public ResponseEntity handleIoException(IOException e) {
return ResponseEntity.status(500).body("网络连接失败:" + e.getMessage());
}
}
使用 @SpringBootTest
集成测试,验证核心功能:
@SpringBootTest
public class UserServiceTest {
@Autowired
private UserService userService;
@Test
void testSaveAndQuery() {
User user = new User();
user.setName("张三");
user.setEmail("[email protected]");
user.setAge(25);
user.setCreateTime(LocalDateTime.now());
// 保存文档
User savedUser = userService.saveUser(user);
assertNotNull(savedUser.getId());
// 查询文档
User foundUser = userService.getUserById(savedUser.getId());
assertEquals("张三", foundUser.getName());
}
}
通过 Kibana 的 Dev Tools 控制台直接执行 ES DSL,验证查询逻辑是否正确:
// 示例:查询 name 包含 "张三" 的文档
GET user_index/_search
{
"query": {
"match": {
"name": "张三"
}
}
}
索引设计:
text
(分词)和 keyword
(精确匹配)类型。date
类型,便于范围查询和聚合。性能优化:
_source
过滤(仅返回需要的字段,减少网络传输)。版本兼容:
启动时报错:Connection refused
原因:ES 服务未启动或地址配置错误。
解决:检查 application.yml
中的 spring.elasticsearch.uris
是否正确,确保 ES 服务运行在对应地址。
字段映射冲突
原因:实体类字段类型与 ES 索引中已有字段类型不一致。
解决:删除旧索引(或重建)后重新启动应用,或通过 _reindex
API 迁移数据。
查询结果为空
原因:可能是分词器不匹配(如中文未使用 IK 分词器)或查询条件错误。
解决:通过 Kibana 检查索引的映射(GET user_index/_mapping
),确认分词器配置;打印生成的 DSL 查询语句(开启调试日志)验证条件。
通过以上步骤,可全面掌握 Spring Boot 整合 Elasticsearch 的核心操作,覆盖从基础配置到高级查询的全场景需求。实际开发中需根据业务需求调整索引设计和查询逻辑,确保性能与功能的平衡。