一个商品搜索支持五个筛选项(品牌、价格、颜色、尺寸、库存)。用户勾了三个,结果只剩 2 条——而"只勾两个"时有 200 条。而分面设计的问题在于:每个分面的可选值,是"在当前结果集上"统计的——于是"勾得越多,可选值越少",直到"全部变灰".
分面(facet)的核心是"统计的口径":按"当前已选条件"统计(联动),还是"按全部数据"统计(不联动)。而这两种口径的体验差别,比"筛选项多少"大得多.
"排除自身"的写法在 DSL 里很直观,而它常被"一把 must 全加上"破坏:
// 统计"品牌"分面时:其它条件在 filter,品牌条件"不在"
{"size": 0, "query": {"bool": {"filter": [{"term": {"color": "red"}}]}},
"aggs": {"brands": {"terms": {"field": "brand.keyword", "size": 20}}}}
// 判据:品牌条件"不进 filter",才能"看到换品牌还有多少"
1. 两种"分面口径"
| 口径 | 效果 |
|---|---|
| 联动(含自身条件) | 勾选后"自己的其它值"变灰 |
| 只含"其它分面" | 自己那一栏的选项保持可见 |
| 两种混用 | 最容易出错 |
第二项是"电商常用"的正解:统计"品牌"这一栏时,排除"品牌"自己的勾选,但保留其它分面的条件第二项的"名字"叫"post-filter 分面":先按"其它分面"过滤,再对"本分面"做聚合(不带它自己的条件)——**于是"每个分面都显示'换成别的值有多少'".
——**于是"你能看到'换成另一个品牌还有多少'".
2. "筛没了"的三种成因
| 成因 | 表现 |
|---|---|
| 口径错(含自身) | 勾了 A 品牌,其它品牌全灰 |
| 数据稀(长尾组合) | 三条件交集为空 |
| 语义重叠(颜色 vs 款式) | 勾两个互相排斥 |
第一项是"设计问题",第二项是"数据问题",第三项是"建模问题"——而它们的"修法完全不同".
"0 条"要"区分"是"真没有"还是"被其它条件挤掉":
// 两步判定(一条查询 + 一次对照)
1 去掉"本分面"条件后,该值仍有 0 条 → 数据里真没有
2 去掉"其它分面"后,该值有 N 条 → 是"当前条件组合"挤掉的
// 判据:第 1 种"置灰并折叠",第 2 种"显示并提示'当前条件下为 0'"
3. "分面为 0"的三种处理
| 处理 | 体验 |
|---|---|
| 隐藏为 0 的选项 | 干净,但"看不到还能选什么" |
| 显示但置灰(带 0) | 最透明(推荐) |
| 显示 + "点它会发生什么" | 最好(如"放宽到 2 个条件") |
第二项是"最少争议"的:**用户看到"这个选项 0 条",就知道"是数据没有,不是我漏点了".
"建议放宽哪个"要"算",而算法只有三步:
// 三步"放宽建议"
1 当前条件集 S → 命中 0
2 对每个分面 f:S \ {f} → 命中 c(f)
3 取 c(f) 最大的 f → "放宽 f 后有 c 条"
// 判据:给"一条具体建议",而不是"请减少筛选条件"
4. "长尾组合为空"的三种应对
| 应对 | 说明 |
|---|---|
| 提示"当前条件过严" | 一句话(成本最低) |
| 提供"最接近的条件组合" | 需要"放宽计算" |
| 允许"部分匹配"排序 | 复杂(相关性下降) |
第一项的"实现"要用"分面计数":"把每个分面的条件依次去掉,算一次命中数"——取"命中数增加最多"的那个,作为"建议放宽的维度".
第一项是"最实用"的:"当前 5 个条件下没有结果,放宽'颜色'后有 12 条"**——而"放宽哪一个"要用"分面计数"算出来.
"预计算"要"能更新",否则"数字越来越旧":
// 三种"更新"策略
1 定时全量重算(每小时)→ 简单,但"新上架"不出现
2 增量更新(按变更文档)→ 复杂,但"准"
3 混合:单分面预计算 + 0 条时"实时确认" → 推荐
// 判据:数字"旧 1 小时"能否接受——能,就用 1
5. "分面统计"的性能代价
| 口径 | 成本 |
|---|---|
| 每次请求实时算 | 高(多一次聚合) |
| 预计算(按组合) | 组合爆炸 |
| 预计算(按单分面) | 中(近似) |
第三项是"常见折中":"每个分面单独预计算",统计时"忽略其它分面"第三项的"数字偏差"方向是"偏高":"忽略其它条件"会让"计数 ≥ 实际"——**于是"置灰判断"不能只看它,还要"用精确计数确认 0 条".
——于是"数字不准"(略高),但"性能可控";而"不准"通常"不影响决策".
6. 排错:五种"分面问题"
| 现象 | 成因 | 处理方向 |
|---|---|---|
| 勾选后自己变灰 | 口径含自身 | 排除自身条件 |
| 数字与实际不符 | 预计算近似 | 标注"约" |
| 某分面永远 0 | 字段未索引/映射错 | 查 mapping |
| 组合一多就没结果 | 长尾 | 提示放宽 |
| 分面慢 | 实时聚合 | 预计算/缓存 |
"分面数量"也有上限,太多会让"选择成本"超过"帮助":
// 三个"够用"的经验值
1 分面 3–6 个(超过 8 个,用户"看不过来")
2 每个分面"默认显示前 10–20 个有值项"(其余折叠)
3 默认"收起"低频分面(如"材质"),点击展开
// 判据:首屏"筛选项"不超过"一屏"
7. 边界:什么情况"不必做分面"
| 场景 | 为什么 | 做法 |
|---|---|---|
| 结果集很小(< 50) | 用户能翻完 | 不必 |
| 只有一个筛选维度 | 无"分面"可言 | 下拉即可 |
| 数据量小、更新频繁 | 预计算不划算 | 简单筛选 |
| 用户目标明确(按 ID 查) | 不需要浏览 | 不必 |
最后一条与"数据"有关的边界:"分面依赖字段可聚合":"文本字段若没开 fielddata/keyword 子字段,就无法聚合"——于是"分面全 0"的第一嫌疑,是"mapping 没给可聚合类型",而不是"没数据".
判断口径一句话:"用户会不会"在结果里"逐步收窄""**。会(电商、文档库、招聘),分面是"必需";不会(精确查找、单一入口),它只是"多一层复杂度".
再补一个"体验"细节:"分面数字"要"能对上结果数"——"品牌 A 显示 12 条",而"点进去只剩 9 条",会让用户"觉得系统不准"。而它的成因通常是"预计算口径"与"实际查询"不一致:一个"忽略了其它分面",另一个"包含全部条件"。修法有两种:要么"点进去时用同一口径"(改查询),要么"显示时标注'约 12 条'(改文案)——而"两处口径不同还不标注",是分面最容易被投诉的地方。
还有一个"多选"的坑:"同分面多选"的语义是"或"还是"且"——"品牌选 A 和 B"通常是"或"(A 或 B),而"颜色选红和蓝"也是"或",但"同一分面里'且'是不可能的"——于是"多选"必须"每个分面内用 should、分面间用 must"。而"写错一处",用户就会看到"勾了两个品牌,结果反而更少"。这类 bug 在"筛选项超过 3 个分面"之后极易出现,而"测试用例"通常只覆盖"单选"。
而"分面内 should、分面间 must"这一条,值得写成"一条测试":"勾两个品牌"的结果数,应当 ≥ "勾一个品牌"的结果数——**小于,就说明"分面内用了 must".
而这条测试,只需要"两条查询 + 一个断言"。
它比"人工点十个组合"可靠得多。
而"可靠"这件事,是分面能被长期使用的底线。
8. 自测
- 两种"分面口径"的差别是什么?为什么"排除自身"是电商的正解?
- "筛没了"的三种成因分别属于"设计/数据/建模"哪一类?
- "显示但置灰(带 0)"为什么"最少争议"?
- "长尾组合为空"时,怎么知道"放宽哪一个"最有效?
- "按单分面预计算"的代价是什么?为什么"数字略高"通常可接受?
- 判断"是否需要分面"的那个问题是什么?