mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4
914 words
2 minutes
Elasticsearch 从 0 到 1(四):Spring Boot 集成与商品搜索实战
2025-06-07

Elasticsearch 从 0 到 1(四):Spring Boot 集成与商品搜索实战#

前面三篇已经讲完基础:

这一篇开始接到 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#

一个简化配置:

@Configuration
public 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 里更常见的是把搜索需要的字段拍平成一个文档。

写入商品文档#

@Service
public 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_desc
price_asc
price_desc
newest

不要直接把前端传来的字段名拼进排序里,避免暴露内部字段和错误排序。

构建搜索查询#

核心思路:

  • 关键词走 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:

Elasticsearch 从 0 到 1(五):MySQL 数据同步与一致性

参考#

Share

If this article helped you, please share it with others!

Elasticsearch 从 0 到 1(四):Spring Boot 集成与商品搜索实战
https://mizuki.mysqil.com/posts/elasticsearch-04-springboot-practice/
Author
梦幻晨风
Published at
2025-06-07
License
CC BY-NC-SA 4.0

Some information may be outdated

Table of Contents