Elasticsearch 从 0 到 1(四):Spring Boot 集成与商品搜索实战
前面三篇已经讲完基础:
- Elasticsearch 从 0 到 1(一):ES 是什么,为什么需要它
- Elasticsearch 从 0 到 1(二):索引、Mapping 和分词器
- Elasticsearch 从 0 到 1(三):DSL 查询入门
这一篇开始接到 Java 项目里。
目标是做一个商品搜索接口:
输入关键词、品牌、分类、价格区间返回分页商品列表支持按销量、价格排序支持标题高亮依赖选择
现在更推荐使用官方 Java API Client,而不是老的 High Level REST Client。
Maven 依赖示例:
<dependency> <groupId>co.elastic.clients</groupId> <artifactId>elasticsearch-java</artifactId> <version>8.14.0</version></dependency>版本要和你的 ES 服务版本尽量保持一致。
如果项目用 Spring Boot,也可以结合 Spring Data Elasticsearch。但刚开始学 ES,我更建议先理解官方 Client 的请求结构,这样更接近原始 DSL。
配置 ElasticsearchClient
一个简化配置:
@Configurationpublic class ElasticsearchConfig {
@Bean public ElasticsearchClient elasticsearchClient() { RestClient restClient = RestClient.builder( new HttpHost("localhost", 9200) ).build();
ElasticsearchTransport transport = new RestClientTransport( restClient, new JacksonJsonpMapper() );
return new ElasticsearchClient(transport); }}真实生产环境还要配置:
- 用户名密码;
- HTTPS;
- 连接超时;
- Socket 超时;
- 多节点地址;
- 连接池;
- 认证证书。
入门阶段先跑通最小链路即可。
商品文档对象
ES 里的商品文档可以这样定义:
public class ProductDocument {
private Long productId; private String title; private String brand; private String category; private Integer price; private Integer sales; private Integer stock; private String status; private LocalDateTime createdAt;
// getter setter}注意:ES 文档通常是为了搜索设计的,不一定和 MySQL 表结构完全一致。
MySQL 可以有很多张表,ES 里更常见的是把搜索需要的字段拍平成一个文档。
写入商品文档
@Servicepublic class ProductSearchService {
private static final String INDEX = "products";
private final ElasticsearchClient client;
public ProductSearchService(ElasticsearchClient client) { this.client = client; }
public void save(ProductDocument document) throws IOException { client.index(i -> i .index(INDEX) .id(String.valueOf(document.getProductId())) .document(document) ); }}这里用商品 ID 作为 ES 文档 ID。
好处是:
- 更新时可以覆盖同一条文档;
- 删除时可以直接按 ID 删除;
- MySQL 和 ES 的关系更清楚。
删除商品文档
public void delete(Long productId) throws IOException { client.delete(d -> d .index(INDEX) .id(String.valueOf(productId)) );}比如商品下架后,可以删除 ES 文档,也可以把 status 改成 OFF_SALE。
如果业务需要保留搜索记录或后台查询,建议改状态;如果完全不希望被搜到,可以删除。
搜索请求对象
前端请求可以设计成:
public class ProductSearchRequest {
private String keyword; private String brand; private String category; private Integer minPrice; private Integer maxPrice; private String sortBy; private Integer pageNo = 1; private Integer pageSize = 10;
// getter setter}sortBy 可以约定几个值:
sales_descprice_ascprice_descnewest不要直接把前端传来的字段名拼进排序里,避免暴露内部字段和错误排序。
构建搜索查询
核心思路:
- 关键词走
match; - 品牌、分类、状态走
term; - 价格走
range; - 排序根据参数选择;
- 高亮标题字段。
示例:
public SearchResponse<ProductDocument> search(ProductSearchRequest request) throws IOException { int from = (request.getPageNo() - 1) * request.getPageSize();
return client.search(s -> s .index(INDEX) .from(from) .size(request.getPageSize()) .query(q -> q.bool(b -> { if (hasText(request.getKeyword())) { b.must(m -> m.match(mm -> mm .field("title") .query(request.getKeyword()) )); } b.filter(f -> f.term(t -> t.field("status").value("ON_SALE"))); if (hasText(request.getBrand())) { b.filter(f -> f.term(t -> t.field("brand").value(request.getBrand()))); } if (hasText(request.getCategory())) { b.filter(f -> f.term(t -> t.field("category").value(request.getCategory()))); } if (request.getMinPrice() != null || request.getMaxPrice() != null) { b.filter(f -> f.range(r -> { r.field("price"); if (request.getMinPrice() != null) { r.gte(JsonData.of(request.getMinPrice())); } if (request.getMaxPrice() != null) { r.lte(JsonData.of(request.getMaxPrice())); } return r; })); } return b; })) .highlight(h -> h .fields("title", hf -> hf .preTags("<em>") .postTags("</em>") ) ), ProductDocument.class );}这段代码只是示例,真实项目可以拆成多个私有方法,让查询构建更清晰。
处理高亮
搜索结果里的命中数据在 hit.source(),高亮在 hit.highlight()。
List<ProductSearchVO> list = new ArrayList<>();
for (Hit<ProductDocument> hit : response.hits().hits()) { ProductDocument doc = hit.source(); ProductSearchVO vo = ProductSearchVO.from(doc);
List<String> titleHighlights = hit.highlight().get("title"); if (titleHighlights != null && !titleHighlights.isEmpty()) { vo.setTitle(titleHighlights.get(0)); }
list.add(vo);}前端展示时注意 em 标签样式。
如果担心 XSS,要对可控内容做安全处理。
返回分页结果
long total = response.hits().total() == null ? 0 : response.hits().total().value();
return new PageResult<>(total, request.getPageNo(), request.getPageSize(), list);一个搜索接口最终返回:
{ "total": 120, "pageNo": 1, "pageSize": 10, "list": [ { "productId": 1001, "title": "无线<em>蓝牙</em>耳机", "brand": "SoundGo", "price": 199 } ]}Controller 示例
@RestController@RequestMapping("/api/products/search")public class ProductSearchController {
private final ProductSearchService productSearchService;
public ProductSearchController(ProductSearchService productSearchService) { this.productSearchService = productSearchService; }
@GetMapping public PageResult<ProductSearchVO> search(ProductSearchRequest request) throws IOException { return productSearchService.searchProducts(request); }}实际项目里建议:
- 参数加校验;
- pageSize 限制最大值;
- 异常统一处理;
- 搜索慢查询打日志;
- 搜索参数记录埋点。
这一篇先记住什么
- Java 项目推荐使用官方 Java API Client;
- ES 文档对象是面向搜索设计的,不一定等于数据库实体;
- 商品 ID 可以作为 ES 文档 ID;
- 搜索接口一般由关键词、过滤条件、排序、分页、高亮组成;
- 高亮结果在
hit.highlight(); - 不要把前端传入字段直接拼到排序逻辑里;
- 真实项目要考虑异常、参数校验和慢查询日志。
下一篇讲 MySQL 数据怎么同步到 ES:
参考
If this article helped you, please share it with others!
Some information may be outdated






