Spring Boot 3 整合 Elasticsearch 8

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 提供的 ElasticsearchRepositoryElasticsearchOperations

本文主要采用:

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。

相关推荐
雪隐1 小时前
个人电脑玩AI01-让5060 Ti给你打工——Whisper语音识别篇(上)
人工智能·后端
我是一颗柠檬1 小时前
【Redis】主从复制Day9
java·数据库·redis·后端
侯盛鑫1 小时前
理解 RocksDB IngestExternalFile
数据库·后端
可乐ea1 小时前
【知识获取与分享社区项目 | 项目日记第 20 天】search_after 游标分页:解决 Elasticsearch 深分页稳定性问题
java·大数据·elasticsearch·搜索引擎·全文检索
Mr.朱鹏2 小时前
基于 postgres_fdw 的跨库查询方案
java·数据库·spring boot·sql·spring·postgresql
1368木林森2 小时前
【Spring源码17·完结篇】SpringBoot核心注解+高频坑点+失效场景万字全集!收官Spring全家桶源码系列
java·spring boot·后端
南山十一少2 小时前
基于 Quartz 组件在 Spring Boot 框架下的周期任务调度实验
java·spring boot·spring
武子康2 小时前
Java-15 深入浅出MyBatis 分页与通用 Mapper 实战:PageHelper + tk.mybatis 从配置到分页查询
java·后端