在当今的电子商务环境中,一个快速、准确、功能丰富的搜索功能不再是锦上添花,而是必不可少的核心体验。在我的开源商城项目 Shoptnt 中,我们选择了 Elasticsearch 作为搜索引擎,来应对海量商品数据下的复杂查询与聚合需求。
本文将深入剖析 Shoptnt 是如何设计商品索引结构、实现数据的同步与维护,并构建出一个支持关键词、分类、品牌、属性、价格等多维度检索的商品搜索系统。
项目地址: https://gitee.com/bbc-se/shoptnt
官方地址: www.shoptnt.cn
一、核心索引结构设计 (Mapping)
一切搜索的基石在于良好的索引设计。我们定义了 GoodsIndex
类来映射 Elasticsearch 中的文档结构。
java
@Document(indexName = "#{esConfig.indexName}_"+ EsSettings.GOODS_INDEX_NAME)
@Data
public class GoodsIndex {
@JestId
private Long goodsId; // 商品ID
@Field(type = FieldType.Text, analyzer = EsSettings.IK_MAX_WORD)
private String name; // 商品名称(使用IK分词)
// ... 其他字段(价格、销量、商家等)
@Field(type = FieldType.Nested, index = true, store = true)
private List<IndexParam> params; // 商品参数(Nested类型!)
}
设计要点:
-
动态索引名:
#{esConfig.indexName}_goods
支持多环境隔离。 -
分词策略: 对商品名
name
字段使用ik_max_word
分词器,力求最细粒度拆分,保证召回率。 -
Nested 类型: 商品参数
params
是一个列表,内含参数名和值。将其定义为nested
类型是关键决策。这确保了参数数组中的每个对象是独立索引和查询的,避免了传统对象类型导致的"交叉匹配"问题。 -
多数据类型: 精确匹配的字段(如 ID、状态码)使用
keyword
或long
,需搜索的文本使用text
,数值范围过滤使用integer
/double
。
二、数据同步:保证 ES 与 DB 的最终一致性
数据库是源 of truth,而 ES 是搜索专用视图。如何高效、可靠地将数据变更同步到 ES 是关键。
我们采用了 事件驱动 的模式,通过 RabbitMQ 进行解耦。
1. 消息生产者: 任何导致商品信息变更的业务操作(增删改、上下架、审核通过等),都会发送一个 GoodsChangeMsg
消息事件。
2. 消息消费者: GoodsChangeIndexConsumer
监听这些消息。
java
@Service
public class GoodsChangeIndexConsumer implements GoodsChangeEvent {
@Autowired
private GoodsIndexClient goodsIndexClient;
@Override
public void goodsChange(GoodsChangeMsg goodsChangeMsg) {
Long[] goodsIds = goodsChangeMsg.getGoodsIds();
int operationType = goodsChangeMsg.getOperationType();
if (isUpdateOperation(operationType)) {
// 新增或更新:重新导入整个商品文档
goodsIndexClient.saveByGoodsIds(Arrays.asList(goodsIds));
} else if (operationType == GoodsChangeMsg.DEL_OPERATION) {
// 删除:从索引中移除文档
goodsIndexClient.deleteBatchByGoodsIds(Arrays.asList(goodsIds));
}
}
}
同步逻辑:
-
更新/新增: 收到消息后,根据商品 ID 从数据库查询最新数据,重新构建
GoodsIndex
对象并index
到 ES。这是一个 替换(Repace) 操作,而非部分更新,简化了逻辑,保证了数据一致性。 -
删除: 直接从 ES 中删除对应文档。
这种模式的优势在于异步和解耦,避免了在业务事务中直接操作 ES 带来的性能瓶颈和复杂性,保证了最终的数据一致性。
三、搜索与聚合:构建丰富查询功能
搜索的核心实现位于 GoodsSearchManagerImpl
类中。
1. 构建复杂查询 (BoolQuery):
createQuery
方法动态地构建一个 BoolQueryBuilder
,将各种筛选条件以 must
子句的形式组合起来,形成最终的查询。
java
BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
// 关键词查询(IK分词,AND操作符)
if (!StringUtil.isEmpty(keyword)) {
QueryStringQueryBuilder queryString = new QueryStringQueryBuilder(keyword)
.field("name").analyzer(EsSettings.IK_SMART).defaultOperator(Operator.AND);
boolQueryBuilder.must(queryString);
}
// 分类查询(利用Path的Wildcard查询)
if (cat != null) {
boolQueryBuilder.must(QueryBuilders.wildcardQuery("categoryPath", encodedCatPath + "*"));
}
// Nested 参数查询!
if (!StringUtil.isEmpty(prop)) {
// ... 解析出参数名和值
boolQueryBuilder.must(QueryBuilders.nestedQuery("params",
QueryBuilders.boolQuery()
.must(QueryBuilders.termQuery("params.name", name))
.must(QueryBuilders.termQuery("params.value", value)),
ScoreMode.None));
}
// 范围查询(价格)
if (!StringUtil.isEmpty(price)) {
boolQueryBuilder.must(QueryBuilders.rangeQuery("price").from(min).to(max));
}
// 固定条件:上架、审核通过、未删除
boolQueryBuilder.must(QueryBuilders.termQuery("isAuth", "1"));
boolQueryBuilder.must(QueryBuilders.termQuery("marketEnable", "1"));
boolQueryBuilder.must(QueryBuilders.termQuery("disabled", "1"));
2. 聚合 (Aggregation) 生成筛选器:
为了生成左侧的"分类"、"品牌"、"参数"等筛选项,我们使用了 ES 的聚合功能。
java
private void setAggregationQuery(SearchSourceBuilder searchSourceBuilder) {
// 分类聚合
AggregationBuilder categoryAgg = AggregationBuilders.terms("categoryAgg").field("categoryId").size(Integer.MAX_VALUE);
// 品牌聚合
AggregationBuilder brandAgg = AggregationBuilders.terms("brandAgg").field("brand").size(Integer.MAX_VALUE);
// 参数聚合是嵌套的:先按参数名聚合,再按参数值聚合
AggregationBuilder valuesAgg = AggregationBuilders.terms("valueAgg").field("params.value");
AggregationBuilder nameAgg = AggregationBuilders.terms("nameAgg").field("params.name").subAggregation(valuesAgg);
AggregationBuilder paramsAgg = AggregationBuilders.nested("paramsAgg", "params").subAggregation(nameAgg);
searchSourceBuilder.aggregation(categoryAgg);
searchSourceBuilder.aggregation(brandAgg);
searchSourceBuilder.aggregation(paramsAgg);
}
一次搜索请求既返回了商品结果,也返回了所有的聚合结果,前端可以据此渲染出丰富的筛选界面。
四、管理功能:索引与词库的维护
1. 全量索引重建: GoodsIndexManagerImpl.initAll()
这是一个后台管理功能,用于首次部署或索引结构变更时重建整个商品索引。它会分页扫描数据库中的所有商品,批量生成索引,并显示进度条,体验非常好。
2. 搜索词库:
系统会通过 analyzer
方法提取所有商品名称的分词,并将其存入一个单独的词库(可能是 ES 或 DB),用于实现搜索提示(AutoComplete)功能。updateGoodsWords
方法用于全量更新这个词库。
总结与最佳实践
通过 Shoptnt 的 ES 实现,我们可以总结出一些电商搜索的通用最佳实践:
-
精心设计 Mapping: 特别是
nested
类型的使用,是处理多值属性的关键。 -
事件驱动同步: 采用 MQ 实现异步解耦,保证最终一致性,对业务性能影响最小。
-
查询与聚合结合: 一次请求完成搜索和筛选面板数据的获取,高效且减少网络开销。
-
中文分词: 选择合适的分词器(如 IK),并根据场景选择
ik_smart
或ik_max_word
。 -
后台管理工具: 提供全量索引、词库更新的工具,便于运维。
Elasticsearch 为 Shoptnt 提供了强大的搜索能力,使其能够应对复杂的电商查询场景。这套实现方案稳定、高效且功能完备,希望能为正在构建类似功能的开发者提供一些参考和思路。
欢迎访问 Gitee 查看项目源码,并提出宝贵的意见和贡献!