搜索项和属性拆开 —— 搜索标签的诞生
需求背景
随着搜索维度越来越多,产品侧开始反馈:搜索栏太乱了,用户找不到想要的条件。
具体有三个问题:
一、搜索项太多,一屏显示不下。 用户体验很差,需要滚动才能看到所有选项。
二、归类不清。 【作者朝代】、【出版年代】、【特殊历史时期】(文艺复兴、工业革命、一战、二战)这些搜索项分散在各处,业务希望把它们聚合到同一个分类下,统称【历史时期】。
三、无法表达组合规则。 业务想上线一个搜索项叫【好评如潮】,条件是"4星且评论数大于3000"。这是一个多属性组合的规则,当前架构里搜索项和属性是一一对应的,根本表达不了。
核心矛盾
当前架构的根本问题是:搜索项和实体属性是强绑定的,一个搜索项对应一个属性字段,没有中间层。
这导致搜索项既不能分类组织,也不能组合多个属性,更不能灵活配置。每次新增一个搜索项,都要改后端接口、改前端页面、联调上线,耗时 2-3 天。
解决方案的方向很清晰:在搜索项和属性之间加一层抽象,让搜索项变成一个独立的配置对象,可以自由组合属性规则。
方案设计
核心设计思路:搜索项在后端眼里,就是一套实体属性的布尔运算规则,对应一段 ES 查询 DSL。
新增两张配置表:
搜索标签配置表(每个搜索项对应一条记录):
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'
)
搜索分类配置表(控制搜索项的分组和多选逻辑):
create table search_category_config (
category_id bigint primary key comment '搜索项分类ID',
category_name varchar(255) comment '搜索项分类名称',
allow_multi_choice int comment '是否允许多选,1=是,2=否',
union_type int comment '多选时的聚合类型,1=AND,2=OR'
)
前后端交互
前端拿到的是分类 + 搜索项的树形结构:
{
"searchCategoryList": [
{
"categoryName": "历史时期",
"allowMultiChoice": 1,
"unionType": 2,
"searchTagList": [
{ "tagName": "作者朝代", "tagId": 1 },
{ "tagName": "出版年代", "tagId": 2 },
{ "tagName": "文艺复兴", "tagId": 3 }
]
},
{
"categoryName": "图书质量",
"allowMultiChoice": 0,
"unionType": 1,
"searchTagList": [
{ "tagName": "好评如潮", "tagId": 10 }
]
}
]
}
用户勾选后,前端只传 tagId 列表,后端根据配置自动组装 ES 查询 DSL:
public QueryBuilder buildSearchQuery(List<Long> tagIds) {
List<SearchTagConfig> tagConfigs = searchTagRepository.findByTagIdIn(tagIds);
Map<Long, List<SearchTagConfig>> categoryGroups =
tagConfigs.stream().collect(Collectors.groupingBy(SearchTagConfig::getCategoryId));
BoolQueryBuilder rootQuery = QueryBuilders.boolQuery();
for (Map.Entry<Long, List<SearchTagConfig>> entry : categoryGroups.entrySet()) {
SearchCategoryConfig categoryConfig =
searchCategoryRepository.findById(entry.getKey());
BoolQueryBuilder categoryQuery = QueryBuilders.boolQuery();
for (SearchTagConfig tagConfig : entry.getValue()) {
QueryBuilder tagQuery = parseDsl(tagConfig.getSearchTagDsl());
if (categoryConfig.getUnionType() == 1) {
categoryQuery.must(tagQuery); // AND
} else {
categoryQuery.should(tagQuery); // OR
}
}
rootQuery.must(categoryQuery);
}
return rootQuery;
}
DSL 示例
简单属性搜索项(作者朝代 = 唐朝):
{ "term": { "author_dynasty": "唐朝" } }
组合属性搜索项(好评如潮 = 4星 AND 评论数>3000):
{
"bool": {
"must": [
{ "term": { "star_level.level": "4星" } },
{ "range": { "star_level.comment_count": { "gt": 3000 } } }
]
}
}
新增搜索项的成本对比
优化前:改后端接口 → 改前端页面 → 联调测试 → 上线部署,耗时 2-3 天。
优化后:写 ES DSL → 插入配置表,耗时 2 小时,无需上线。
| 维度 | 优化前 | 优化后 |
|---|---|---|
| 新增搜索项周期 | 2-3 天 | 2 小时 |
| 前端改动 | 每次必须 | 无需 |
| 属性组合支持 | 不支持 | 完全支持 |
| 分类组织 | 代码固化 | 配置化 |
这一步解决了什么
搜索项从此变成了可配置、可管理的独立对象。它不再和某个具体属性绑定,而是对应一套灵活的查询规则。新增搜索项只需要写 DSL 配置,无需代码开发。
但用户很快提出了新需求:搜索结果列表里,能不能显示每本书命中了哪些搜索条件?
这个需求看起来只是个展示问题,但它引出了一个更深层的设计——搜索项和标签,其实是同一件事。
这引出了下一篇:标签系统的诞生。