一、整体架构设计

电商平台常见的搜索场景包括:商品搜索(关键词、分类、价格区间、排序)、订单搜索(订单号、商品名、状态)等。由于数据来源于多个微服务(如商品服务、订单服务),搜索服务需要从这些服务同步数据到 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() 等方法部分覆盖。

优点:开发环境快速搭建,无需额外操作。
缺点:生产环境控制力弱,映射修改困难,且无法管理索引版本。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐