Elasticsearch 实战系列(二):SpringBoot 集成 Elasticsearch,从 0 到 1 实现商品搜索系统
本文为 Elasticsearch 实战系列第二篇,承接上一篇《Elasticsearch 实战系列(一):基础入门》的核心知识点,从零实现 Spring Boot 与 Elasticsearch 的无缝集成,基于 Spring Data Elasticsearch 搭建一套可直接落地的商品搜索系统,覆盖环境搭建、分层开发、基础 CRUD、复杂全文检索、聚合统计、高亮搜索全流程。
前言
在上一篇中,我们已经掌握了 ES 的核心概念、DSL 语法、索引与文档的基础操作。但在实际的 Java 后端开发中,我们很少直接通过 curl 命令操作 ES,更多是在 Spring Boot 项目中集成 ES,实现业务化的检索能力。
读完本文,你将掌握以下核心内容:
- ES 主流 Java 客户端的选型逻辑,避开已废弃的过时方案
- Spring Boot 与 ES 的版本匹配规则,解决 90% 的集成启动报错
- 基于 Spring Data Elasticsearch 的分层开发规范
- 无需手写 DSL,通过方法命名实现基础查询
- 基于 ElasticsearchRestTemplate 实现复杂组合查询、聚合统计、高亮搜索
- 新手集成 ES 的高频踩坑点与完整解决方案
一、ES Java 客户端选型:选对工具少走弯路
ES 官方提供了多种 Java 客户端,不同版本的适配性、维护状态差异极大,选对客户端是避免后续返工的第一步。
1.1 主流客户端对比
| 客户端类型 | 核心说明 | 推荐状态 | 适配版本 |
|---|---|---|---|
| TransportClient | 基于 TCP 协议的传统客户端 | ❌ 已废弃 | ES 7.x 之前版本 |
| RestHighLevelClient | 基于 HTTP 的高级 REST 客户端 | ⚠️ 即将废弃 | ES 7.x 全系列 |
| Elasticsearch Java API Client | 官方新一代 Java 客户端 | ✅ 官方推荐 | ES 8.x+ |
| Spring Data Elasticsearch | Spring 生态封装的 ES 操作框架 | ✅ 全场景推荐 | 兼容 7.x、8.x 全系列 |
1.2 本文选型说明
本文最终选用Spring Data Elasticsearch作为核心开发框架,核心原因如下:
- 与 Spring Boot 无缝集成,自动配置、开箱即用,无需手动管理客户端连接
- 提供 Repository 模式,无需手写 DSL 即可实现绝大多数基础查询,大幅简化开发
- 同时兼容 ES 7.x 和 8.x 版本,后续版本升级成本极低
- 底层封装了 RestHighLevelClient,既支持极简开发,也保留了原生 DSL 复杂查询的能力
- 与 Spring 生态的其他组件(Spring MVC、Spring Cloud 等)完美适配,符合 Java 后端开发规范
整体集成架构如下:
Controller层(REST接口) → Service层(业务逻辑) → Repository层/ElasticsearchRestTemplate → Elasticsearch集群
全程通过 HTTP REST 协议与 ES 通信,无语言绑定,兼容性极强。
二、环境准备:版本匹配是第一要务
Spring Boot、Spring Data Elasticsearch、ES 三者之间有严格的版本对应关系,版本不匹配会出现类找不到、方法不兼容、启动报错等问题,这是新手集成 ES 的第一大踩坑点。
2.1 严格的版本对应关系
本文采用官方推荐的稳定兼容版本组合,适配绝大多数企业级生产环境:
表格
| Spring Boot 版本 | Spring Data Elasticsearch 版本 | Elasticsearch 版本 |
|---|---|---|
| 2.7.x | 4.4.x | 7.17.x |
| 3.0.x | 5.0.x | 8.x |
本文最终使用版本:
- Spring Boot:2.7.14
- Spring Data Elasticsearch:4.4.14(随 Spring Boot 自动引入)
- Elasticsearch:7.17.10(与上一篇环境保持一致)
2.2 项目核心依赖引入
在 Maven 的 pom.xml 中引入以下核心依赖,无需手动指定 Spring Data Elasticsearch 的版本,由 Spring Boot 父工程统一管理:
<dependencies>
<!-- Spring Boot 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>
<!-- JSON序列化与反序列化 -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
</dependencies>
2.3 配置文件编写
在 application.yml(或 application.properties)中添加 ES 相关配置,Spring Boot 会自动读取配置,完成客户端的自动初始化:
spring:
elasticsearch:
# ES服务地址,集群环境填写多个节点,用逗号分隔
uris: http://localhost:9200
# 若ES开启了用户名密码认证,填写以下配置
# username: elastic
# password: 123456
# 连接超时时间,默认5s,网络环境差可适当调大
connection-timeout: 5s
# socket通信超时时间,默认30s,大数据量查询可适当调大
socket-timeout: 30s
# 日志配置:开启ES相关操作的DEBUG日志,方便调试排查问题
logging:
level:
org.springframework.data.elasticsearch: DEBUG
三、实体类映射:Java 对象与 ES 索引的绑定
实体类是 Java 业务对象与 ES 索引文档的映射桥梁,通过注解即可完成索引名称、分片配置、字段类型、分词规则的全定义,无需手动在 ES 中创建索引和 Mapping。
3.1 完整商品实体类
我们以电商商品搜索场景为例,创建完整的 Product 实体类,覆盖 ES 常用的字段类型与配置:
package com.example.demo.entity;
import lombok.Data;
import org.springframework.data.annotation.Id;
import org.springframework.data.elasticsearch.annotations.Document;
import org.springframework.data.elasticsearch.annotations.Field;
import org.springframework.data.elasticsearch.annotations.FieldType;
import java.math.BigDecimal;
import java.util.Date;
import java.util.List;
/**
* 商品实体类,与ES的product_index索引绑定
*/
@Data
@Document(
indexName = "product_index", // 绑定的索引名称,必须全小写
shards = 3, // 主分片数量,创建后不可修改
replicas = 1, // 副本分片数量,可动态调整
createIndex = true // 项目启动时自动创建索引(若不存在)
)
public class Product {
/**
* 文档主键,对应ES中的_id字段
*/
@Id
private String id;
/**
* 商品标题:全文检索字段,使用ik_max_word分词器实现中文分词
*/
@Field(type = FieldType.Text, analyzer = "ik_max_word")
private String title;
/**
* 商品分类:精确匹配字段,不分词,用于分类过滤、聚合统计
*/
@Field(type = FieldType.Keyword)
private String category;
/**
* 商品价格:浮点类型,用于范围查询、排序
*/
@Field(type = FieldType.Double)
private BigDecimal price;
/**
* 商品库存:整数类型,用于库存过滤
*/
@Field(type = FieldType.Integer)
private Integer stock;
/**
* 商品销量:整数类型,用于排序、统计
*/
@Field(type = FieldType.Integer)
private Integer sales;
/**
* 商品品牌:精确匹配字段,用于品牌过滤、聚合
*/
@Field(type = FieldType.Keyword)
private String brand;
/**
* 商品标签:数组类型,Keyword类型,用于标签过滤
*/
@Field(type = FieldType.Keyword)
private List<String> tags;
/**
* 商品描述:全文检索字段,支持中文分词模糊搜索
*/
@Field(type = FieldType.Text, analyzer = "ik_max_word")
private String description;
/**
* 创建时间:日期类型,支持多种日期格式,用于时间范围查询、排序
*/
@Field(type = FieldType.Date, pattern = "yyyy-MM-dd HH:mm:ss||yyyy-MM-dd||epoch_millis")
private Date createTime;
}
3.2 核心注解详解
1. @Document 注解(类级别)
用于绑定 Java 类与 ES 索引,核心参数说明:
indexName:必填,绑定的索引名称,必须符合 ES 索引命名规则(全小写,不能以特殊字符开头)shards:主分片数量,默认 1,创建索引后不可修改replicas:副本分片数量,默认 1,可动态调整createIndex:是否在项目启动时自动创建索引,默认 true,生产环境建议开启
2. @Id 注解(字段级别)
用于标记文档主键,对应 ES 中的_id字段,支持手动赋值,也可由 ES 自动生成。
3. @Field 注解(字段级别)
用于定义字段的存储与索引规则,核心参数说明:
type:必填,字段类型(Text、Keyword、Integer、Date 等),直接决定字段的检索能力analyzer:text 类型字段的分词器,中文场景推荐使用 ik_max_word(细粒度分词)searchAnalyzer:搜索时使用的分词器,默认与 analyzer 保持一致index:是否开启索引,默认 true,关闭后该字段无法被检索store:是否单独存储,默认 false,默认存储在_source 中
3.3 Java 与 ES 字段类型对应规则
| Java 数据类型 | ES 字段类型 | 核心适用场景 |
|---|---|---|
| String | Text | 全文检索场景(商品标题、文章内容、描述等),会分词 |
| String | Keyword | 精确匹配场景(分类、品牌、状态、ID、标签等),不分词 |
| Integer/Long | Integer/Long | 整数数据(年龄、库存、销量、数量等) |
| Float/Double/BigDecimal | Float/Double | 浮点数据(价格、评分、折扣率等) |
| Boolean | Boolean | 二值状态(是否上架、是否删除等) |
| Date | Date | 时间数据(创建时间、更新时间、订单时间等) |
| List/Set | Array | 数组数据(标签列表、图片地址列表等) |
| 自定义对象 | Object | 单嵌套对象(收货地址、规格信息等) |
踩坑提示:Text 类型字段不支持排序和聚合,Keyword 类型字段不支持全文分词检索,一定要根据业务场景选择正确的字段类型,否则会出现查询不到结果、排序报错的问题。
四、Repository 数据访问层:极简实现基础 CRUD
Spring Data Elasticsearch 提供了 Repository 抽象,继承ElasticsearchRepository接口即可自动获得基础的 CRUD、分页、排序能力,无需手动编写实现类,甚至无需手写 DSL 语句,通过方法命名即可自动生成查询逻辑。
4.1 Repository 继承体系
CrudRepository → PagingAndSortingRepository → ElasticsearchRepository → 自定义Repository接口
- CrudRepository:提供基础的增删改查方法
- PagingAndSortingRepository:扩展了分页和排序能力
- ElasticsearchRepository:扩展了 ES 专属的搜索能力
- 自定义接口:实现业务专属的查询方法
4.2 自定义 ProductRepository
创建 ProductRepository 接口,继承 ElasticsearchRepository,泛型参数为 <实体类类型,主键类型>:
package com.example.demo.repository;
import com.example.demo.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.math.BigDecimal;
import java.util.List;
/**
* 商品数据访问层,自动获得基础CRUD能力
*/
@Repository
public interface ProductRepository extends ElasticsearchRepository<Product, String> {
// ==================== 单条件精确查询 ====================
/**
* 根据商品标题查询(全文检索)
*/
List<Product> findByTitle(String title);
/**
* 根据商品分类查询(精确匹配)
*/
List<Product> findByCategory(String category);
/**
* 根据品牌查询(精确匹配)
*/
List<Product> findByBrand(String brand);
// ==================== 范围查询 ====================
/**
* 价格区间查询
*/
List<Product> findByPriceBetween(BigDecimal minPrice, BigDecimal maxPrice);
/**
* 查询价格大于指定值的商品
*/
List<Product> findByPriceGreaterThan(BigDecimal price);
/**
* 查询价格小于指定值的商品
*/
List<Product> findByPriceLessThan(BigDecimal price);
// ==================== 模糊查询 ====================
/**
* 标题模糊查询
*/
List<Product> findByTitleLike(String title);
// ==================== 组合条件查询 ====================
/**
* 组合查询:分类 + 价格区间
*/
List<Product> findByCategoryAndPriceBetween(String category, BigDecimal minPrice, BigDecimal maxPrice);
/**
* 组合查询:品牌或分类
*/
List<Product> findByBrandOrCategory(String brand, String category);
// ==================== 分页与排序查询 ====================
/**
* 根据分类分页查询
*/
Page<Product> findByCategory(String category, Pageable pageable);
/**
* 根据分类查询,按销量倒序排序
*/
List<Product> findByCategoryOrderBySalesDesc(String category);
}
4.3 方法命名规则详解
Spring Data Elasticsearch 会根据方法名自动解析生成对应的 DSL 查询语句,核心关键字与 ES 查询的对应关系如下:
| 方法关键字 | 示例方法 | 对应 ES DSL 查询 | 逻辑说明 |
|---|---|---|---|
| findBy | findByTitle | match/term 查询 | 根据字段查询 |
| And | findByTitleAndCategory | bool.must | 多个条件必须同时满足(AND) |
| Or | findByBrandOrCategory | bool.should | 多个条件满足其一即可(OR) |
| Between | findByPriceBetween | range | 范围查询,包含上下限 |
| GreaterThan | findByPriceGreaterThan | range.gt | 大于指定值 |
| LessThan | findByPriceLessThan | range.lt | 小于指定值 |
| Like | findByTitleLike | wildcard | 模糊查询 |
| OrderBy | findByCategoryOrderBySalesDesc | sort | 按指定字段排序 |
最佳实践:简单单条件、组合条件查询优先使用方法命名实现,开发效率极高;复杂的多条件动态查询、聚合查询、高亮搜索,使用 ElasticsearchRestTemplate 实现。
五、Service 业务层:覆盖基础操作与复杂查询
Service 层负责封装业务逻辑,分为两部分:基于 Repository 实现基础 CRUD,基于 ElasticsearchRestTemplate 实现复杂动态查询。
5.1 完整 Service 层实现
package com.example.demo.service;
import com.example.demo.entity.Product;
import com.example.demo.repository.ProductRepository;
import lombok.extern.slf4j.Slf4j;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.index.query.BoolQueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.search.aggregations.AggregationBuilders;
import org.elasticsearch.search.aggregations.Aggregations;
import org.elasticsearch.search.aggregations.bucket.terms.Terms;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightBuilder;
import org.elasticsearch.search.sort.SortBuilders;
import org.elasticsearch.search.sort.SortOrder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.elasticsearch.core.ElasticsearchRestTemplate;
import org.springframework.data.elasticsearch.core.SearchHit;
import org.springframework.data.elasticsearch.core.SearchHits;
import org.springframework.data.elasticsearch.core.query.NativeSearchQuery;
import org.springframework.data.elasticsearch.core.query.NativeSearchQueryBuilder;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.util.*;
import java.util.stream.Collectors;
/**
* 商品业务层实现
*/
@Slf4j
@Service
public class ProductService {
@Autowired
private ProductRepository productRepository;
@Autowired
private ElasticsearchRestTemplate elasticsearchTemplate;
// ==================== 基础CRUD操作 ====================
/**
* 新增/更新商品
* 若ID已存在则执行全量更新,不存在则执行新增
*/
public Product save(Product product) {
return productRepository.save(product);
}
/**
* 批量保存商品
*/
public void saveAll(List<Product> products) {
productRepository.saveAll(products);
}
/**
* 根据ID查询商品
*/
public Product findById(String id) {
Optional<Product> optional = productRepository.findById(id);
return optional.orElse(null);
}
/**
* 根据ID删除商品
*/
public void deleteById(String id) {
productRepository.deleteById(id);
}
/**
* 查询所有商品
*/
public List<Product> findAll() {
Iterable<Product> iterable = productRepository.findAll();
List<Product> productList = new ArrayList<>();
iterable.forEach(productList::add);
return productList;
}
// ==================== 基于Repository的业务查询 ====================
/**
* 根据分类分页查询商品
* @param category 分类名称
* @param page 页码,从0开始
* @param size 每页条数
*/
public Page<Product> findByCategory(String category, int page, int size) {
Pageable pageable = PageRequest.of(page, size);
return productRepository.findByCategory(category, pageable);
}
/**
* 价格区间查询商品
*/
public List<Product> findByPriceRange(BigDecimal minPrice, BigDecimal maxPrice) {
return productRepository.findByPriceBetween(minPrice, maxPrice);
}
// ==================== 基于RestTemplate的复杂查询 ====================
/**
* 全文检索:多字段匹配关键字(标题+描述)
*/
public List<Product> fullTextSearch(String keyword) {
// 构建多字段匹配查询:同时匹配title和description字段
NativeSearchQuery searchQuery = new NativeSearchQueryBuilder()
.withQuery(QueryBuilders.multiMatchQuery(keyword, "title", "description"))
.build();
// 执行查询
SearchHits<Product> searchHits = elasticsearchTemplate.search(searchQuery, Product.class);
// 解析结果,返回商品列表
return searchHits.stream()
.map(SearchHit::getContent)
.collect(Collectors.toList());
}
/**
* 动态组合查询:分类、价格区间、关键字、排序
* 支持参数动态为空,自动忽略空条件
*/
public List<Product> complexSearch(String category, BigDecimal minPrice, BigDecimal maxPrice, String keyword) {
// 构建布尔查询
BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
// 分类过滤:参数不为空时才添加条件
if (category != null && !category.isEmpty()) {
boolQuery.filter(QueryBuilders.termQuery("category", category));
}
// 价格区间过滤:参数不为空时才添加条件
if (minPrice != null || maxPrice != null) {
boolQuery.filter(QueryBuilders.rangeQuery("price")
.gte(minPrice != null ? minPrice : 0)
.lte(maxPrice != null ? maxPrice : Integer.MAX_VALUE));
}
// 关键字全文检索:参数不为空时才添加条件
if (keyword != null && !keyword.isEmpty()) {
boolQuery.must(QueryBuilders.multiMatchQuery(keyword, "title", "description"));
}
// 构建查询:按销量倒序排序
NativeSearchQuery searchQuery = new NativeSearchQueryBuilder()
.withQuery(boolQuery)
.withSort(SortBuilders.fieldSort("sales").order(SortOrder.DESC))
.build();
// 执行查询并解析结果
SearchHits<Product> searchHits = elasticsearchTemplate.search(searchQuery, Product.class);
return searchHits.stream()
.map(SearchHit::getContent)
.collect(Collectors.toList());
}
// ==================== 高级功能实现 ====================
/**
* 聚合查询:按商品分类统计商品数量
*/
public Map<String, Long> countByCategory() {
// 构建聚合查询:按category字段分桶统计文档数量
NativeSearchQuery searchQuery = new NativeSearchQueryBuilder()
.addAggregation(AggregationBuilders.terms("category_count").field("category"))
.build();
// 执行查询
SearchHits<Product> searchHits = elasticsearchTemplate.search(searchQuery, Product.class);
// 解析聚合结果
Aggregations aggregations = searchHits.getAggregations();
Terms categoryAgg = aggregations.get("category_count");
// 封装结果:key=分类名称,value=商品数量
Map<String, Long> result = new HashMap<>();
for (Terms.Bucket bucket : categoryAgg.getBuckets()) {
result.put(bucket.getKeyAsString(), bucket.getDocCount());
}
return result;
}
/**
* 高亮搜索:匹配的关键字添加高亮标签,优化前端展示
*/
public List<Product> searchWithHighlight(String keyword) {
// 构建高亮配置:匹配title和description字段,用<em>标签包裹匹配的关键字
HighlightBuilder highlightBuilder = new HighlightBuilder()
.field("title")
.field("description")
.preTags("<em>")
.postTags("</em>");
// 构建查询
NativeSearchQuery searchQuery = new NativeSearchQueryBuilder()
.withQuery(QueryBuilders.multiMatchQuery(keyword, "title", "description"))
.withHighlightBuilder(highlightBuilder)
.build();
// 执行查询
SearchHits<Product> searchHits = elasticsearchTemplate.search(searchQuery, Product.class);
// 解析结果:将高亮内容替换到商品实体中
return searchHits.stream()
.map(hit -> {
Product product = hit.getContent();
// 获取高亮字段
Map<String, List<String>> highlightFields = hit.getHighlightFields();
// 替换标题的高亮内容
if (highlightFields.containsKey("title")) {
product.setTitle(highlightFields.get("title").get(0));
}
// 替换描述的高亮内容
if (highlightFields.containsKey("description")) {
product.setDescription(highlightFields.get("description").get(0));
}
return product;
})
.collect(Collectors.toList());
}
}
5.2 核心实现说明
- 基础 CRUD:直接调用 Repository 提供的内置方法,无需手动实现,极简开发
- 动态组合查询:使用 BoolQueryBuilder 构建动态条件,参数为空时自动忽略,适配前端多条件筛选的业务场景
- 过滤与查询分离:过滤条件统一放在 filter 子句中,不计算相关性分数,ES 会自动缓存过滤结果,查询性能远高于 must 子句
- 聚合查询:使用 Terms 聚合实现分桶统计,适用于分类统计、品牌统计等电商常见场景
- 高亮搜索:自定义高亮标签,将匹配的关键字替换到实体类中,前端可直接渲染高亮效果,无需额外处理
六、Controller 层:RESTful API 接口设计
基于 Spring MVC 设计 RESTful 风格的 API 接口,对外提供商品的增删改查、检索能力,所有接口均配套测试命令,可直接调用验证。
6.1 统一返回结果类
首先创建通用的返回结果类Result,统一接口返回格式:
package com.example.demo.common;
import lombok.Data;
/**
* 全局统一返回结果类
*/
@Data
public class Result<T> {
/**
* 响应码:200成功,其他失败
*/
private Integer code;
/**
* 响应消息
*/
private String message;
/**
* 响应数据
*/
private T data;
public static <T> Result<T> success(T data) {
Result<T> result = new Result<>();
result.setCode(200);
result.setMessage("操作成功");
result.setData(data);
return result;
}
public static <T> Result<T> success(String message, T data) {
Result<T> result = new Result<>();
result.setCode(200);
result.setMessage(message);
result.setData(data);
return result;
}
public static <T> Result<T> error(String message) {
Result<T> result = new Result<>();
result.setCode(500);
result.setMessage(message);
result.setData(null);
return result;
}
}
6.2 完整 Controller 接口实现
package com.example.demo.controller;
import com.example.demo.common.Result;
import com.example.demo.entity.Product;
import com.example.demo.service.ProductService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.web.bind.annotation.*;
import java.math.BigDecimal;
import java.util.List;
import java.util.Map;
/**
* 商品管理REST接口
*/
@RestController
@RequestMapping("/api/products")
public class ProductController {
@Autowired
private ProductService productService;
// ==================== 基础CRUD接口 ====================
/**
* 创建商品
*/
@PostMapping
public Result<Product> create(@RequestBody Product product) {
Product savedProduct = productService.save(product);
return Result.success(savedProduct);
}
/**
* 批量创建商品
*/
@PostMapping("/batch")
public Result<Void> batchCreate(@RequestBody List<Product> products) {
productService.saveAll(products);
return Result.success("批量创建成功", null);
}
/**
* 根据ID查询商品详情
*/
@GetMapping("/{id}")
public Result<Product> getById(@PathVariable String id) {
Product product = productService.findById(id);
return Result.success(product);
}
/**
* 查询所有商品
*/
@GetMapping
public Result<List<Product>> getAll() {
List<Product> products = productService.findAll();
return Result.success(products);
}
/**
* 根据ID删除商品
*/
@DeleteMapping("/{id}")
public Result<Void> delete(@PathVariable String id) {
productService.deleteById(id);
return Result.success("删除成功", null);
}
// ==================== 检索查询接口 ====================
/**
* 根据分类分页查询商品
* @param category 分类名称
* @param page 页码,默认0(第一页)
* @param size 每页条数,默认10
*/
@GetMapping("/category/{category}")
public Result<Page<Product>> getByCategory(
@PathVariable String category,
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size) {
Page<Product> productPage = productService.findByCategory(category, page, size);
return Result.success(productPage);
}
/**
* 价格区间查询商品
*/
@GetMapping("/price-range")
public Result<List<Product>> getByPriceRange(
@RequestParam BigDecimal minPrice,
@RequestParam BigDecimal maxPrice) {
List<Product> products = productService.findByPriceRange(minPrice, maxPrice);
return Result.success(products);
}
/**
* 全文关键字搜索
*/
@GetMapping("/search")
public Result<List<Product>> fullTextSearch(@RequestParam String keyword) {
List<Product> products = productService.fullTextSearch(keyword);
return Result.success(products);
}
/**
* 多条件复杂搜索
* 所有参数均为非必填,支持动态组合
*/
@GetMapping("/complex-search")
public Result<List<Product>> complexSearch(
@RequestParam(required = false) String category,
@RequestParam(required = false) BigDecimal minPrice,
@RequestParam(required = false) BigDecimal maxPrice,
@RequestParam(required = false) String keyword) {
List<Product> products = productService.complexSearch(category, minPrice, maxPrice, keyword);
return Result.success(products);
}
/**
* 带高亮的关键字搜索
*/
@GetMapping("/search-highlight")
public Result<List<Product>> searchWithHighlight(@RequestParam String keyword) {
List<Product> products = productService.searchWithHighlight(keyword);
return Result.success(products);
}
/**
* 按分类统计商品数量
*/
@GetMapping("/count-by-category")
public Result<Map<String, Long>> countByCategory() {
Map<String, Long> countResult = productService.countByCategory();
return Result.success(countResult);
}
}
6.3 接口测试 curl 命令
项目启动后,可直接通过以下 curl 命令测试所有接口,也可通过 Postman、Apifox 等工具调用:
# 1. 查询所有商品
curl http://localhost:8080/api/products
# 2. 根据ID查询商品详情
curl http://localhost:8080/api/products/1
# 3. 根据分类分页查询商品
curl http://localhost:8080/api/products/category/手机
# 4. 价格区间查询商品
curl "http://localhost:8080/api/products/price-range?minPrice=3000&maxPrice=8000"
# 5. 全文关键字搜索
curl "http://localhost:8080/api/products/search?keyword=华为"
# 6. 多条件复杂搜索
curl "http://localhost:8080/api/products/complex-search?category=手机&minPrice=4000&maxPrice=7000&keyword=5G"
# 7. 带高亮的关键字搜索
curl "http://localhost:8080/api/products/search-highlight?keyword=旗舰"
# 8. 按分类统计商品数量
curl http://localhost:8080/api/products/count-by-category
# 9. 创建单个商品
curl -X POST http://localhost:8080/api/products \
-H "Content-Type: application/json" \
-d '{
"title": "iPhone 15 Pro",
"category": "手机",
"price": 7999,
"stock": 150,
"sales": 300,
"brand": "苹果",
"tags": ["5G", "高端", "iOS"],
"description": "苹果最新旗舰手机,搭载A17 Pro芯片"
}'
# 10. 根据ID删除商品
curl -X DELETE http://localhost:8080/api/products/1
七、测试数据初始化:启动自动导入测试数据
为了方便测试,我们实现CommandLineRunner接口,在项目启动完成后自动初始化测试数据,无需手动调用接口造数:
package com.example.demo;
import com.example.demo.entity.Product;
import com.example.demo.service.ProductService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
import java.math.BigDecimal;
import java.util.Arrays;
import java.util.Date;
/**
* 项目启动时自动初始化测试数据
*/
@Component
public class DataInitializer implements CommandLineRunner {
@Autowired
private ProductService productService;
@Override
public void run(String... args) throws Exception {
// 构建测试商品数据
Product p1 = new Product();
p1.setId("1");
p1.setTitle("华为Mate 60 Pro 5G手机");
p1.setCategory("手机");
p1.setPrice(new BigDecimal("6999"));
p1.setStock(100);
p1.setSales(500);
p1.setBrand("华为");
p1.setTags(Arrays.asList("5G", "高端", "国产"));
p1.setDescription("华为最新旗舰手机,搭载麒麟9000S芯片,支持卫星通信");
p1.setCreateTime(new Date());
Product p2 = new Product();
p2.setId("2");
p2.setTitle("小米14 Pro 智能手机");
p2.setCategory("手机");
p2.setPrice(new BigDecimal("4999"));
p2.setStock(200);
p2.setSales(800);
p2.setBrand("小米");
p2.setTags(Arrays.asList("5G", "性价比", "拍照"));
p2.setDescription("小米旗舰手机,搭载骁龙8 Gen3芯片,徕卡影像系统");
p2.setCreateTime(new Date());
Product p3 = new Product();
p3.setId("3");
p3.setTitle("MacBook Pro 14英寸笔记本电脑");
p3.setCategory("电脑");
p3.setPrice(new BigDecimal("14999"));
p3.setStock(50);
p3.setSales(200);
p3.setBrand("苹果");
p3.setTags(Arrays.asList("M3芯片", "高性能", "轻薄"));
p3.setDescription("苹果M3芯片笔记本,专业级性能,适合设计、开发场景");
p3.setCreateTime(new Date());
// 批量保存数据
productService.saveAll(Arrays.asList(p1, p2, p3));
System.out.println("===== 测试商品数据初始化完成 =====");
}
}
八、新手高频踩坑与解决方案
8.1 报错:no such index [product_index]
问题原因:ES 中不存在对应的索引,可能是自动创建索引失败,或者手动删除了索引。解决方案:手动创建索引与映射,添加以下方法,项目启动时执行即可:
@Autowired
private ElasticsearchRestTemplate elasticsearchTemplate;
/**
* 手动创建索引与映射
*/
public void createIndex() {
IndexOperations indexOps = elasticsearchTemplate.indexOps(Product.class);
// 若索引不存在,则创建索引并写入映射
if (!indexOps.exists()) {
indexOps.create();
indexOps.putMapping(indexOps.createMapping());
}
}
8.2 ES 服务连接超时
问题原因:网络环境差、ES 服务不在本地、超时时间配置过短,导致连接超时。解决方案:调整配置文件中的超时时间,适当放大连接超时和 socket 超时:
spring:
elasticsearch:
uris: http://localhost:9200
connection-timeout: 10s
socket-timeout: 60s
同时检查 ES 服务是否正常启动、端口是否开放、防火墙是否拦截了请求。
8.3 中文分词不生效
问题原因:ES 中未安装 IK 中文分词器,导致 text 类型的中文字段无法正常分词,全文检索失效。解决方案:安装与 ES 版本完全一致的 IK 中文分词器,Docker 环境安装步骤如下:
# 1. 进入ES容器
docker exec -it elasticsearch bash
# 2. 安装对应版本的IK分词器(本文ES版本为7.17.10,必须对应)
./bin/elasticsearch-plugin install https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v7.17.10/elasticsearch-analysis-ik-7.17.10.zip
# 3. 退出容器
exit
# 4. 重启ES容器,使分词器生效
docker restart elasticsearch
注意:IK 分词器的版本必须与 ES 版本完全一致,否则会导致 ES 启动失败。
九、总结与系列预告
本文总结
本文完整覆盖了 Spring Boot 集成 Elasticsearch 的全流程,从客户端选型、环境搭建、分层开发,到复杂查询、高级功能、踩坑解决方案,实现了一套可直接落地的商品搜索系统。核心内容如下:
- 明确了 ES Java 客户端的选型逻辑,避开了已废弃的 TransportClient 方案
- 梳理了 Spring Boot 与 ES 的版本对应规则,解决了集成的第一大踩坑点
- 掌握了基于注解的实体类与 ES 索引映射规则,明确了字段类型的选型逻辑
- 学会了基于 Repository 的极简开发模式,通过方法命名实现基础查询
- 掌握了基于 ElasticsearchRestTemplate 的复杂动态查询、聚合统计、高亮搜索实现
- 解决了新手集成 ES 的 3 个高频问题,提供了完整的解决方案
系列预告
在下一篇文章中,我们将深入讲解 Elasticsearch 的高级查询技巧与生产环境实战场景,包括:
- 深度分页的 3 种解决方案与性能对比
- 嵌套对象与 nested 类型的查询实战
- 中文分词的高级配置与自定义词典
- ES 集群的生产环境部署规范与性能优化
- 海量数据的同步方案与最佳实践