搜索出来还需要打标 —— 标签系统的诞生
需求背景
用户同时勾选了多个搜索项,系统返回了一个列表。但产品提出了一个新问题:
"这个列表里,用户怎么知道每本书是因为什么条件被搜出来的?"
比如用户同时勾选了【好评如潮】和【历史时期-唐朝】,返回的列表里有些书两个条件都满足,有些只满足其中一个。如果能在列表项上直接显示标签,用户一眼就能看出来。
这个需求看起来只是个展示问题,但仔细想想,它和搜索项的底层逻辑完全一样。
核心矛盾
标签的本质是什么?是判断一个实体是否满足某套属性规则,满足就打上这个标签。
搜索项的本质是什么?也是判断一个实体是否满足某套属性规则,满足就被搜索出来。
规则相同,用途不同。 用于过滤时叫搜索项,用于展示时叫标签。
如果为标签单独建一套系统,就是在重复造轮子。更合理的做法是:扩展现有的搜索标签配置表,让同一条配置既能作为搜索项,也能作为标签。
方案设计
在 search_tag_config 表上新增几个字段:
create table search_tag_config (
tag_id bigint primary key comment '搜索标签 ID',
tag_name varchar(255) comment '搜索项名称',
category_id bigint comment '搜索项分类ID',
search_tag_dsl text comment '搜索项映射的 ES 搜索 DSL',
valid_status int comment '标签状态,1=预发灰度,2=发布,3=下线',
-- 新增:标签相关字段
as_search_term boolean comment '是否作为搜索项展示',
as_label boolean comment '是否作为标签展示',
label_spel text comment '标签规则的 SpEL 表达式',
label_show_config text comment '标签样式配置(文案、颜色等)JSON'
)
一个搜索标签可以同时具备两种身份,通过 as_search_term 和 as_label 字段控制:
as_search_term = true:在搜索栏展示,用户勾选后作为过滤条件,使用search_tag_dsl生成 ES 查询as_label = true:在列表中展示,标记实体特征,使用label_spel进行规则判断
为什么用 SpEL 做标签判断
ES DSL 是给 Elasticsearch 用的查询语言,没法直接拿来判断一个 Java 对象是否满足条件。标签打标的场景是:拿到一个实体对象,判断它是否命中某条规则,返回 true/false。
SpEL(Spring Expression Language)正好适合这个场景:语法直观,支持布尔运算、属性访问、方法调用,Spring 自带无需额外依赖。
好评如潮:starLevel == 4 && commentCount > 3000
最近出版:publishTime > T(java.time.LocalDateTime).now().minusDays(7)
短篇: wordCount <= 100000
中篇: wordCount > 100000 && wordCount <= 200000
标签计算流程
用户搜索后,返回 ID 列表,从 MySQL 反查实体数据,然后对每条数据计算标签:
- 查询所有
as_label = true且状态为发布的标签配置 - 对每条实体数据,遍历所有标签配置,用 SpEL 逐一判断是否命中
- 命中的标签 ID 列表随实体数据一起返回给前端
- 前端根据
label_show_config中的样式配置渲染标签
成本对比
优化前(新增标签【好评如潮】):编写 ES DSL + 编写标签判断逻辑 + 前端开发 + 联调上线,耗时 1 天。
优化后:写 ES DSL + 写 SpEL 表达式 + 配置入库,耗时 2 小时,无需上线。
统一术语:搜索标签
从这篇开始,我们不再单独称"搜索项",而是统一叫搜索标签——它既可以做搜索项,也可以做标签,是同一个概念的两种用途。
这一步解决了什么
搜索标签实现了真正的配置化:新增一个搜索标签,只需要写 DSL 和 SpEL,配置入库即可生效,无需代码开发,无需上线。
但业务继续发展,产品提出了一个新需求:分级搜索标签。
比如【十分推荐】的规则是:满足【经典名著】,或者同时满足【好评如潮】和【线上热销】。这是一套用标签来定义标签的规则,当前的 SpEL 方案完全支持不了——因为标签列表本身就是一个需要计算的结果,不能作为 SpEL 的输入。
这个问题,是后续大迭代的导火索之一。但在那之前,还有一个更紧迫的问题需要先解决。
这引出了下一篇:EAV 异构的优化。