Spring Boot 3 整合 Elasticsearch 8:从环境搭建到增删改查与条件查询
前言
在实际项目开发中,MySQL、PostgreSQL 等关系型数据库更适合处理结构化数据和事务场景,但是当系统需要实现全文检索、关键词高亮、模糊查询、分词搜索、多条件筛选时,传统数据库的 LIKE '%keyword%' 查询往往性能较差,也不够灵活。
Elasticsearch,简称 ES,是一个基于 Lucene 的分布式搜索与分析引擎,常用于日志检索、商品搜索、文章搜索、用户行为分析等场景。
本文将从零开始介绍如何在 Spring Boot 项目中适配 Elasticsearch,并完成索引创建、文档新增、修改、删除、根据 ID 查询、关键词搜索等常见操作。
一、技术选型说明
本文采用的技术栈如下:
| 技术 | 版本建议 |
|---|---|
| JDK | 17+ |
| Spring Boot | 3.x |
| Elasticsearch | 8.x |
| Spring Data Elasticsearch | 随 Spring Boot 版本自动管理 |
| Maven | 3.8+ |
| Docker | 可选,用于快速启动 ES |
需要注意的是,Spring Boot 2.x 时代常见的 RestHighLevelClient 在新版本中已经不推荐继续使用。Spring Boot 3.x 项目更建议使用 Spring Data Elasticsearch 提供的 ElasticsearchRepository 或 ElasticsearchOperations。
本文主要采用:
text
ElasticsearchRepository:适合基础 CRUD
ElasticsearchOperations:适合复杂查询、索引管理、条件构造
二、Elasticsearch 环境搭建
1. 使用 Docker 启动 Elasticsearch
如果只是本地学习,可以使用 Docker 快速启动单节点 Elasticsearch。
bash
docker run -d \
--name elasticsearch \
-p 9200:9200 \
-p 9300:9300 \
-e "discovery.type=single-node" \
-e "xpack.security.enabled=false" \
elasticsearch:8.12.2
参数说明:
| 参数 | 说明 |
|---|---|
9200 |
HTTP 访问端口 |
9300 |
节点通信端口 |
discovery.type=single-node |
单节点模式 |
xpack.security.enabled=false |
关闭安全认证,便于本地测试 |
启动完成后,在浏览器访问:
text
http://localhost:9200
如果看到类似下面的返回,说明 Elasticsearch 启动成功:
json
{
"name": "xxxx",
"cluster_name": "docker-cluster",
"version": {
"number": "8.12.2"
},
"tagline": "You Know, for Search"
}
三、创建 Spring Boot 项目
1. Maven 依赖
在 pom.xml 中引入 Spring Data Elasticsearch 依赖:
xml
<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>
<!-- 测试依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
如果使用 Spring Boot 父工程管理版本,一般不需要手动指定 Elasticsearch 相关依赖版本,交给 Spring Boot 自动管理即可。
四、配置 Elasticsearch 连接
在 application.yml 中添加 ES 连接配置:
yaml
server:
port: 8080
spring:
application:
name: springboot-es-demo
elasticsearch:
uris: http://localhost:9200
connection-timeout: 3s
socket-timeout: 30s
如果你的 Elasticsearch 开启了账号密码认证,可以改成下面这种形式:
yaml
spring:
elasticsearch:
uris: http://localhost:9200
username: elastic
password: your_password
本地学习阶段建议先关闭认证,降低环境配置复杂度。等项目上线时,再开启用户认证、HTTPS、证书校验等安全配置。
五、创建 ES 文档实体类
这里以"博客文章搜索"为例,创建一个 ArticleDocument 实体类。
java
package com.example.esdemo.document;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.data.annotation.Id;
import org.springframework.data.elasticsearch.annotations.*;
import java.time.LocalDateTime;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Document(indexName = "blog_article")
public class ArticleDocument {
@Id
private Long id;
@Field(type = FieldType.Text, analyzer = "standard")
private String title;
@Field(type = FieldType.Text, analyzer = "standard")
private String content;
@Field(type = FieldType.Keyword)
private String author;
@Field(type = FieldType.Integer)
private Integer viewCount;
@Field(type = FieldType.Date, format = DateFormat.date_hour_minute_second)
private LocalDateTime createTime;
}
注解说明
| 注解 | 作用 |
|---|---|
@Document |
指定当前类对应 ES 中的索引 |
@Id |
指定文档 ID |
@Field |
指定字段类型、分词器等信息 |
FieldType.Text |
可分词,适合全文检索 |
FieldType.Keyword |
不分词,适合精确匹配 |
FieldType.Integer |
整数类型 |
FieldType.Date |
日期类型 |
需要特别注意:
text
Text:会分词,适合标题、正文、描述等字段
Keyword:不会分词,适合用户名、状态、分类、标签等字段
例如:
java
@Field(type = FieldType.Text)
private String title;
适合搜索"Spring Boot 整合 Elasticsearch"这类文本。
而:
java
@Field(type = FieldType.Keyword)
private String author;
适合精确匹配作者名。
六、创建 Repository 接口
Spring Data Elasticsearch 提供了类似 JPA 的 Repository 写法。
java
package com.example.esdemo.repository;
import com.example.esdemo.document.ArticleDocument;
import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;
import java.util.List;
public interface ArticleRepository extends ElasticsearchRepository<ArticleDocument, Long> {
List<ArticleDocument> findByAuthor(String author);
List<ArticleDocument> findByTitleContaining(String keyword);
}
这里继承:
java
ElasticsearchRepository<ArticleDocument, Long>
第一个泛型是文档实体类,第二个泛型是主键 ID 类型。
七、编写 Service 层
1. 基础增删改查
java
package com.example.esdemo.service;
import com.example.esdemo.document.ArticleDocument;
import com.example.esdemo.repository.ArticleRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.util.Optional;
@Service
@RequiredArgsConstructor
public class ArticleService {
private final ArticleRepository articleRepository;
public ArticleDocument saveArticle(ArticleDocument article) {
if (article.getCreateTime() == null) {
article.setCreateTime(LocalDateTime.now());
}
return articleRepository.save(article);
}
public Optional<ArticleDocument> findById(Long id) {
return articleRepository.findById(id);
}
public Iterable<ArticleDocument> findAll() {
return articleRepository.findAll();
}
public void deleteById(Long id) {
articleRepository.deleteById(id);
}
}
2. 根据作者查询
java
public Iterable<ArticleDocument> findByAuthor(String author) {
return articleRepository.findByAuthor(author);
}
3. 根据标题模糊查询
java
public Iterable<ArticleDocument> searchByTitle(String keyword) {
return articleRepository.findByTitleContaining(keyword);
}
这种 Repository 方法命名查询适合简单场景。如果需要更复杂的条件组合,建议使用 ElasticsearchOperations。
八、使用 ElasticsearchOperations 实现复杂查询
在实际开发中,经常会遇到如下需求:
text
1. 标题或正文包含关键词
2. 作者精确匹配
3. 阅读量大于某个值
4. 按创建时间倒序排序
5. 分页查询
这类查询更适合使用 ElasticsearchOperations。
java
package com.example.esdemo.service;
import com.example.esdemo.document.ArticleDocument;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.data.elasticsearch.client.elc.NativeQuery;
import org.springframework.data.elasticsearch.core.ElasticsearchOperations;
import org.springframework.data.elasticsearch.core.SearchHits;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class ArticleSearchService {
private final ElasticsearchOperations elasticsearchOperations;
public SearchHits<ArticleDocument> search(String keyword, int page, int size) {
NativeQuery query = NativeQuery.builder()
.withQuery(q -> q
.multiMatch(m -> m
.query(keyword)
.fields("title", "content")
)
)
.withPageable(PageRequest.of(page, size))
.withSort(Sort.by(Sort.Direction.DESC, "createTime"))
.build();
return elasticsearchOperations.search(query, ArticleDocument.class);
}
}
这里使用了 multiMatch 查询,表示在多个字段中搜索同一个关键词。
java
.fields("title", "content")
表示同时在标题和正文中搜索。
九、编写 Controller 测试接口
java
package com.example.esdemo.controller;
import com.example.esdemo.document.ArticleDocument;
import com.example.esdemo.service.ArticleSearchService;
import com.example.esdemo.service.ArticleService;
import lombok.RequiredArgsConstructor;
import org.springframework.data.elasticsearch.core.SearchHit;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;
@RestController
@RequestMapping("/articles")
@RequiredArgsConstructor
public class ArticleController {
private final ArticleService articleService;
private final ArticleSearchService articleSearchService;
@PostMapping
public ArticleDocument save(@RequestBody ArticleDocument article) {
return articleService.saveArticle(article);
}
@GetMapping("/{id}")
public ArticleDocument findById(@PathVariable Long id) {
return articleService.findById(id).orElse(null);
}
@GetMapping
public List<ArticleDocument> findAll() {
return StreamSupport.stream(articleService.findAll().spliterator(), false)
.collect(Collectors.toList());
}
@DeleteMapping("/{id}")
public String deleteById(@PathVariable Long id) {
articleService.deleteById(id);
return "删除成功";
}
@GetMapping("/author/{author}")
public Iterable<ArticleDocument> findByAuthor(@PathVariable String author) {
return articleService.findByAuthor(author);
}
@GetMapping("/search")
public List<ArticleDocument> search(@RequestParam String keyword,
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size) {
return articleSearchService.search(keyword, page, size)
.getSearchHits()
.stream()
.map(SearchHit::getContent)
.collect(Collectors.toList());
}
}
十、接口测试
1. 新增文章
请求地址:
http
POST http://localhost:8080/articles
请求体:
json
{
"id": 1,
"title": "Spring Boot 整合 Elasticsearch 实战",
"content": "本文介绍如何在 Spring Boot 项目中使用 Elasticsearch 实现全文检索。",
"author": "张三",
"viewCount": 100
}
2. 根据 ID 查询
http
GET http://localhost:8080/articles/1
3. 查询全部文章
http
GET http://localhost:8080/articles
4. 根据作者查询
http
GET http://localhost:8080/articles/author/张三
5. 关键词搜索
http
GET http://localhost:8080/articles/search?keyword=Spring&page=0&size=10
6. 删除文章
http
DELETE http://localhost:8080/articles/1
十一、索引和数据库表的区别
很多刚接触 Elasticsearch 的同学容易把 ES 索引和 MySQL 表混为一谈。可以简单类比,但不能完全等同。
| MySQL | Elasticsearch |
|---|---|
| Database | Cluster |
| Table | Index |
| Row | Document |
| Column | Field |
| SQL | DSL Query |
例如在 MySQL 中,一条文章记录是一行数据;在 Elasticsearch 中,一篇文章就是一个 Document。
十二、常见问题
1. Spring Boot 连接不上 Elasticsearch
常见报错:
text
Connection refused
原因通常是:
text
1. Elasticsearch 没有启动
2. 端口不是 9200
3. application.yml 中 uris 配置错误
4. Docker 容器没有正确映射端口
可以先访问:
text
http://localhost:9200
确认 ES 是否正常运行。
2. 报错:index_not_found_exception
这个错误表示索引不存在。
解决方式有两种:
第一种:通过 Repository 自动创建索引。
第二种:使用 ElasticsearchOperations 手动创建索引。
java
@PostConstruct
public void createIndex() {
IndexOperations indexOperations = elasticsearchOperations.indexOps(ArticleDocument.class);
if (!indexOperations.exists()) {
indexOperations.create();
indexOperations.putMapping();
}
}
完整写法如下:
java
package com.example.esdemo.config;
import com.example.esdemo.document.ArticleDocument;
import jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.elasticsearch.core.ElasticsearchOperations;
import org.springframework.data.elasticsearch.core.IndexOperations;
@Configuration
@RequiredArgsConstructor
public class ElasticsearchIndexConfig {
private final ElasticsearchOperations elasticsearchOperations;
@PostConstruct
public void createIndex() {
IndexOperations indexOperations = elasticsearchOperations.indexOps(ArticleDocument.class);
if (!indexOperations.exists()) {
indexOperations.create();
indexOperations.putMapping();
}
}
}
3. 中文搜索效果不好
如果直接使用默认分词器,中文分词效果通常比较一般。例如搜索"搜索引擎",默认分词器可能无法达到理想效果。
实际项目中可以安装 IK 分词器,例如:
text
ik_smart:粗粒度分词
ik_max_word:细粒度分词
实体类可以这样配置:
java
@Field(type = FieldType.Text, analyzer = "ik_max_word", searchAnalyzer = "ik_smart")
private String title;
注意:使用 IK 分词器之前,需要先在 Elasticsearch 中安装对应版本的 IK 插件,否则启动或创建索引时会报错。
4. Text 和 Keyword 用错导致查不到数据
如果字段定义为:
java
@Field(type = FieldType.Text)
private String author;
那么作者名可能会被分词,精确查询时容易出现问题。
如果字段用于精确匹配,建议使用:
java
@Field(type = FieldType.Keyword)
private String author;
简单记忆:
text
需要全文搜索:Text
需要精确匹配:Keyword
5. 日期排序失败
如果需要按照时间排序,字段类型必须正确设置为 Date:
java
@Field(type = FieldType.Date, format = DateFormat.date_hour_minute_second)
private LocalDateTime createTime;
同时,插入数据时必须保证 createTime 有值,否则排序结果可能不符合预期。
十三、Repository 和 ElasticsearchOperations 怎么选?
| 使用方式 | 适用场景 |
|---|---|
| ElasticsearchRepository | 简单 CRUD、简单条件查询 |
| ElasticsearchOperations | 复杂查询、分页排序、索引管理、DSL 查询 |
| Java API Client | 更底层、更灵活,适合复杂 ES 原生 API 操作 |
一般项目可以这样选择:
text
简单业务:Repository 足够
复杂搜索:ElasticsearchOperations 更合适
高级定制:Java API Client
对于大多数 Spring Boot 业务系统来说,ElasticsearchRepository + ElasticsearchOperations 的组合已经可以覆盖大部分需求。
十四、完整项目结构
推荐项目结构如下:
text
springboot-es-demo
├── src
│ └── main
│ ├── java
│ │ └── com.example.esdemo
│ │ ├── controller
│ │ │ └── ArticleController.java
│ │ ├── document
│ │ │ └── ArticleDocument.java
│ │ ├── repository
│ │ │ └── ArticleRepository.java
│ │ ├── service
│ │ │ ├── ArticleService.java
│ │ │ └── ArticleSearchService.java
│ │ ├── config
│ │ │ └── ElasticsearchIndexConfig.java
│ │ └── EsDemoApplication.java
│ └── resources
│ └── application.yml
└── pom.xml
十五、项目开发中的最佳实践
1. 不要把 ES 当成主数据库
Elasticsearch 适合搜索和分析,但不适合作为强事务数据库。实际项目中通常采用:
text
MySQL / PostgreSQL:存储核心业务数据
Elasticsearch:存储搜索副本
也就是说,数据库负责"准确存储",Elasticsearch 负责"快速搜索"。
2. 保证数据库和 ES 数据一致
常见同步方式有三种:
text
1. 业务代码双写:保存数据库后同步写入 ES
2. 消息队列异步同步:MySQL -> MQ -> ES
3. Binlog 监听同步:MySQL Binlog -> Canal -> ES
小项目可以使用业务代码双写;中大型项目建议使用 MQ 或 Binlog 方案。
3. 索引 Mapping 不要频繁修改
ES 的字段类型一旦确定,后续不能随意改变。例如一个字段一开始是 Text,后面想改成 Keyword,通常需要重新创建索引并迁移数据。
因此,在项目初期就要设计好字段类型。
4. 大数据量查询要避免深分页
Elasticsearch 默认不适合非常深的分页,例如查询第 10000 页。深分页会带来较高的性能开销。
常见优化方式:
text
1. 限制最大分页页数
2. 使用 search_after
3. 使用滚动查询 scroll 处理批量导出
普通后台管理系统可以使用 from + size 分页;大规模数据检索场景建议使用 search_after。
十六、总结
本文完成了 Spring Boot 3 整合 Elasticsearch 8 的基本流程,主要包括:
text
1. 使用 Docker 启动 Elasticsearch
2. 在 Spring Boot 中引入 spring-boot-starter-data-elasticsearch
3. 配置 ES 连接地址
4. 创建 ES 文档实体类
5. 使用 ElasticsearchRepository 实现基础 CRUD
6. 使用 ElasticsearchOperations 实现复杂查询
7. 总结 Text、Keyword、索引、Mapping 等常见问题
整体来看,Spring Boot 整合 Elasticsearch 的核心并不复杂,真正需要注意的是版本匹配、字段类型设计、中文分词、数据同步和分页性能问题。
如果只是实现简单搜索功能,可以先使用 ElasticsearchRepository 快速完成开发;如果后续需要复杂查询、排序、高亮、聚合分析等功能,则建议逐步切换到 ElasticsearchOperations 或 Elasticsearch Java API Client。