在当今的电子商务环境中,一个快速、准确、功能丰富的搜索功能不再是锦上添花,而是必不可少的核心体验。在我的开源商城项目 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 查看项目源码,并提出宝贵的意见和贡献!