spring cloud alibaba微服务电商elastic搜索功能简单实现
一、整体架构设计
电商平台常见的搜索场景包括:商品搜索(关键词、分类、价格区间、排序)、订单搜索(订单号、商品名、状态)等。由于数据来源于多个微服务(如商品服务、订单服务),搜索服务需要从这些服务同步数据到 Elasticsearch,优化查询性能并且避免直接跨库查询。
推荐架构:
-
搜索微服务:提供
/search/products、/search/orders等接口。 -
数据同步:通过消息队列(RocketMQ)或定时任务将业务数据变更同步到 ES。
-
服务注册与发现:使用 Nacos 实现微服务注册与配置管理。
-
调用方式:网关(Spring Cloud Gateway)统一路由,前端请求 → 网关 → 搜索微服务。
二、技术栈
| 组件 | 说明 |
|---|---|
| Spring Boot | 基础框架 |
| Spring Cloud Alibaba | 服务发现(Nacos)、配置管理(Nacos)、RPC(Dubbo 或 Feign) |
| Elasticsearch | 搜索引擎(7.x 或 8.x) |
| Spring Data Elasticsearch | 简化 ES 操作(也可用 RestHighLevelClient) |
| RocketMQ | 数据同步(可选) |
| Lombok | 简化代码 |
三、实现步骤
1. 创建搜索微服务
使用 Spring Initializr 创建项目,引入以下核心依赖:
<dependencies>
<!-- Spring Boot Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring Cloud Alibaba Nacos Discovery -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!-- Spring Data Elasticsearch -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>
<!-- Feign(调用其他微服务同步数据) -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!-- 可选:RocketMQ 用于异步同步 -->
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-spring-boot-starter</artifactId>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
2. 配置 Elasticsearch 和rocketmq连接
在 application.yml 中配置 ES 地址:
spring:
elasticsearch:
uris: http://localhost:9200
# 如果启用安全认证
# username: elastic
# password: your_password
rocketmq:
name-server: localhost:9876
consumer:
group: search-consumer
max-reconsume-times: 3 # 最大重试次数(默认16次)
consume-thread-max: 20
3. 定义实体类与索引映射
以商品搜索为例,创建 ProductDocument 类,使用 @Document 注解映射索引。
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;
@Data
@Document(indexName = "products")
public class ProductDocument {
@Id
private Long id; // 商品ID
@Field(type = FieldType.Text, analyzer = "ik_max_word", searchAnalyzer = "ik_smart")
private String name; // 商品名称(支持中文分词)
@Field(type = FieldType.Text, analyzer = "ik_max_word", searchAnalyzer = "ik_smart")
private String description; // 商品描述
@Field(type = FieldType.Keyword)
private String category; // 分类(精确匹配)
@Field(type = FieldType.Double)
private BigDecimal price; // 价格
@Field(type = FieldType.Integer)
private Integer stock; // 库存
@Field(type = FieldType.Date)
private Date createTime; // 创建时间
@Field(type = FieldType.Boolean)
private Boolean status; // 上架状态
}
说明:
analyzer和searchAnalyzer指定了 IK 分词器,需要在 ES 中安装analysis-ik插件。
4. 编写 Repository
继承 ElasticsearchRepository 即可获得基础 CRUD 和分页查询能力。
java
import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;
import java.util.List;
public interface ProductRepository extends ElasticsearchRepository<ProductDocument, Long> {
// 自定义方法(按分类查询)
List<ProductDocument> findByCategory(String category);
}
5. 实现搜索服务层
利用 ElasticsearchRestTemplate 或 NativeSearchQueryBuilder 构建复杂查询,支持关键词、过滤、分页、排序、高亮。
java
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
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.NativeSearchQueryBuilder;
import org.springframework.data.elasticsearch.core.query.Query;
import org.springframework.stereotype.Service;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.search.sort.SortBuilders;
import org.elasticsearch.search.sort.SortOrder;
import java.util.List;
import java.util.stream.Collectors;
@Service
public class ProductSearchService {
@Autowired
private ElasticsearchRestTemplate elasticsearchRestTemplate;
public Page<ProductDocument> searchProducts(String keyword, String category,
Double minPrice, Double maxPrice,
String sortField, String sortOrder,
int page, int size) {
// 1. 构建 bool 查询
BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
// 关键词搜索(匹配 name 和 description)
if (keyword != null && !keyword.isEmpty()) {
boolQuery.must(QueryBuilders.multiMatchQuery(keyword, "name", "description"));
}
// 分类过滤
if (category != null && !category.isEmpty()) {
boolQuery.filter(QueryBuilders.termQuery("category", category));
}
// 价格范围过滤
if (minPrice != null || maxPrice != null) {
RangeQueryBuilder rangeQuery = QueryBuilders.rangeQuery("price");
if (minPrice != null) rangeQuery.gte(minPrice);
if (maxPrice != null) rangeQuery.lte(maxPrice);
boolQuery.filter(rangeQuery);
}
// 2. 构建排序
SortOrder order = "desc".equalsIgnoreCase(sortOrder) ? SortOrder.DESC : SortOrder.ASC;
FieldSortBuilder sortBuilder = SortBuilders.fieldSort(sortField).order(order);
// 3. 构建查询对象
NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder()
.withQuery(boolQuery)
.withSort(sortBuilder)
.withPageable(PageRequest.of(page, size));
// 4. 添加高亮(可选)
queryBuilder.withHighlightFields(
new HighlightBuilder.Field("name").preTags("<em>").postTags("</em>"),
new HighlightBuilder.Field("description").preTags("<em>").postTags("</em>")
);
Query query = queryBuilder.build();
// 5. 执行搜索
SearchHits<ProductDocument> searchHits = elasticsearchRestTemplate.search(query, ProductDocument.class);
// 6. 处理高亮结果(将高亮片段替换到原文档中)
List<ProductDocument> content = searchHits.getSearchHits().stream().map(hit -> {
ProductDocument doc = hit.getContent();
if (hit.getHighlightFields().containsKey("name")) {
doc.setName(hit.getHighlightFields().get("name").get(0));
}
if (hit.getHighlightFields().containsKey("description")) {
doc.setDescription(hit.getHighlightFields().get("description").get(0));
}
return doc;
}).collect(Collectors.toList());
// 7. 返回分页对象
return new PageImpl<>(content, PageRequest.of(page, size), searchHits.getTotalHits());
}
}
6. 暴露 REST API
java
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/search/products")
public class ProductSearchController {
@Autowired
private ProductSearchService productSearchService;
@GetMapping
public Page<ProductDocument> search(
@RequestParam(required = false) String keyword,
@RequestParam(required = false) String category,
@RequestParam(required = false) Double minPrice,
@RequestParam(required = false) Double maxPrice,
@RequestParam(defaultValue = "createTime") String sortField,
@RequestParam(defaultValue = "desc") String sortOrder,
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size) {
return productSearchService.searchProducts(keyword, category, minPrice, maxPrice,
sortField, sortOrder, page, size);
}
}
8. 微服务调用(Feign)
如果其他微服务需要调用搜索功能,可以通过 Feign 声明式客户端。
java
@FeignClient(name = "search-service", path = "/search")
public interface SearchFeignClient {
@GetMapping("/products")
Page<ProductDocument> searchProducts(@RequestParam Map<String, Object> params);
}
7. 数据同步(关键)
为了保证 ES 中的数据与业务数据库一致,需要建立同步机制。常见方案:
方案一:使用 RocketMQ 消息队列
商品服务在商品新增、更新、删除时发送消息。
搜索服务监听消息,实时更新 ES 索引。
java
// 商品服务发送消息
@Autowired
private RocketMQTemplate rocketMQTemplate;
public void updateProduct(Product product) {
// 更新数据库...
rocketMQTemplate.convertAndSend("product-topic", product);
}
// 搜索服务监听消息
@RocketMQMessageListener(topic = "product-topic", consumerGroup = "search-consumer")
public class ProductMessageListener implements RocketMQListener<Product> {
@Autowired
private ProductRepository productRepository;
@Override
public void onMessage(Product product) {
// 转换为 ProductDocument 并保存到 ES
ProductDocument doc = convertToDocument(product);
productRepository.save(doc);
}
}
8. 手动创建索引(推荐用于生产环境)
通过 Kibana Dev Tools 或直接调用 Elasticsearch REST API 创建索引。这种方式可以精确控制分片数、副本数、映射细节、分析器配置等,避免程序自动创建时可能产生的映射错误。
优点:
-
完全可控,适合复杂业务场景。
-
便于版本管理和迁移。
-
避免动态映射带来的字段类型错误。
缺点:需要运维人员手动执行,与 CI/CD 集成略麻烦,但可通过初始化脚本自动化。
10. 程序自动创建索引(Spring Data Elasticsearch)
Spring Data Elasticsearch 支持在应用启动时根据 @Document 注解自动创建索引和映射。只需配置:
yaml
spring:
elasticsearch:
uris: http://localhost:9200
restclient:
create-index: true # 默认为 true
然后实体类使用 @Document、@Field 等注解:
java
@Data
@Document(indexName = "products")
public class ProductDocument {
@Id
private Long id;
@Field(type = FieldType.Text, analyzer = "ik_max_word", searchAnalyzer = "ik_smart")
private String name;
// ...
}
启动应用时,如果索引不存在,Spring Data Elasticsearch 会根据注解创建索引。但这种方式不支持自定义分析器配置(如自定义分词器、过滤器),也无法设置分片数、副本数等高级参数。不过可以通过实现 ElasticsearchConfiguration 并重写 getCreateIndex() 等方法部分覆盖。
优点:开发环境快速搭建,无需额外操作。
缺点:生产环境控制力弱,映射修改困难,且无法管理索引版本。
更多推荐

所有评论(0)