标签嵌套标签 —— 系统中的妥协与代价
需求背景
业务希望推出一套分级搜索标签体系:
- 【十分推荐】:满足【经典名著】,或者同时满足【好评如潮】AND【线上热销】
- 【优质图书】:满足(【最近热搜】OR【最近热议】OR【现代优秀作家】)AND 非【十分推荐】
- 【潜力图书】:满足【今年出版】AND【优质作家】AND【搜索飙升】AND 非【优质图书】
这套规则有一个显著特点:标签的判断依赖其他标签的结果。
核心矛盾
当前架构里,标签规则是用 SpEL 表达式写的,表达式的输入是实体的属性值,比如 starLevel == 4 && commentCount > 3000。
但【十分推荐】的规则需要判断"这本书是否已经被打上了【经典名著】标签"。问题在于:标签列表本身就是一个需要计算的结果,它不是实体的原始属性,没法直接作为 SpEL 的输入。
这是一个架构层面的限制,不是改改代码就能解决的。
临时方案:硬编码
在架构支持之前,只能先硬编码实现:
public List<String> calculateLabels(BookInfoSearchEntity book) {
List<String> labels = new ArrayList<>();
// 第一步:计算基础标签
for(labelConfg : laberConfigService.getConfigs()){
if(labelConfig.determineMeetCondition(book)){
labels.add(labelConfig.getLabel())
}
}
// ... 其他基础标签
// 第二步:基于基础标签计算组合标签
if (labels.contains("经典名著") ||
(labels.contains("好评如潮") && labels.contains("线上热销"))) {
labels.add("十分推荐");
}
// 第三步:排除已有标签后计算下一级
if (!labels.contains("十分推荐") &&
(labels.contains("最近热搜") || labels.contains("最近热议"))) {
labels.add("优质图书");
}
// ...
return labels;
}
这段代码能跑,但问题很明显:
每次新增一个组合标签,都要改这个方法,加新的判断逻辑。标签之间的依赖关系全部隐藏在代码里,没有任何可视化。改一个标签的规则,需要找到对应的代码位置,改完还要担心影响其他标签。每次调整都要走完整的开发上线流程,耗时 3-5 天。
规律总结:标签开发流程的固化
经过多个版本的迭代,标签相关需求的开发流程已经相当固定了:
步骤一:确定属性来源
属性大致分三类:实体模型自有属性(如 name、publish_time);外部离线计算、日更新、本地冗余存储的属性(如 star_level、comment_count);实时外部接口、监听变更 MQ、本地不冗余存储的属性(如 view_count、stock_quantity)。
步骤二:确定标签规则
规则分两类:基于实体属性的规则(SpEL 可以处理);基于其他标签的嵌套规则(只能硬编码)。
步骤三:配置上线
写 ES DSL、写 SpEL、配置入库,如果有嵌套规则则额外写 Java 代码上线。
这套流程里,嵌套规则是唯一一个无法配置化的环节,也是最高频的痛点。
系统走到了转折点
此时系统的状态:
- 数据规模已经达到亿级,
book_base表已经分库分表,ES 也需要扩容 - 标签数量越来越多,嵌套规则越来越复杂,硬编码代码越堆越高
- 每次迭代都像在拆炸弹,改一个地方不知道会影响哪里
系统的熵增已经到了难以维护的临界点。
这是一个典型的"最小成本原则"和"持续控制熵增"之间的权衡时刻。前面几篇我们一直在用最小成本解决当前问题,但现在,技术债的利息已经超过了继续拖延的收益。
是时候做一次大迭代了。
大迭代的四个方向
痛定思痛,我们规划了四个方向的重构:
方向一:离线属性计算解耦。每次新增离线计算属性都要写 Java 代码,能不能改成写 HiveSQL?
方向二:实时外部属性接入标准化。每接入一个外部系统都要重复开发,能不能有统一的接入框架?
方向三:ES 异构流程重构。异构代码散落各处,每次接入新系统都要改,能不能用责任链模式统一管理?
方向四:标签嵌套规则配置化。这是最核心的问题,需要设计一套自定义的简单 DSL,让嵌套规则也能配置化,以便于后续对业务透明,对标签管理,以及配置可视化更友好。
接下来的四篇,分别对应这四个方向。