用户搜"差旅报销标准",知识库返回空。系统显示"未找到相关内容"——用户于是去问同事,而实际答案就在库里的《费用报销制度》第 4 章,只是那章标题写的是"差旅费用"而正文没有"标准"这个词。
"空结果"不是终点,而是一个需要分类的信号:可能是真的没有、可能是表达不匹配、可能是过滤条件太严、也可能是索引没同步。这里的成本不对称:一次"没搜到"的体验损失,远大于十次"搜到了"的收益。因为用户对"找不到"的记忆更深,而且它会直接影响"下次是否还来这里搜"。
而它的处理方式决定了用户是"再试一次"还是"再也不来"。
1. 五种"空"的原因
| 原因 | 特征 |
|---|---|
| 真的没有 | 任何表达都搜不到 |
| 表达不匹配 | 换词能搜到 |
| 过滤太严 | 去掉条件能搜到 |
| 索引缺失 | 数据存在但索引里没有 |
| 权限过滤 | 有数据但当前用户不可见 |
前三项是"检索问题",后两项是"数据/权限问题"。前两项最容易混淆:"真的没有"与"表达不匹配"在系统里都表现为空,区分方式是"用同义词再搜一次"——如果能搜到,就是后者。这个检查很便宜,应当自动化。
而系统对它们的正确反应完全不同:前者应当引导改写,后者应当说明原因。
2. 空结果的四种应对
| 应对 | 适用 |
|---|---|
| 提示改写 | 表达不匹配 |
| 放宽条件(自动) | 过滤太严 |
| 展示相近结果 | 真的没有但有相关 |
| 说明并转人工 | 权限/索引问题 |
第一项要给具体的改写方向,而不是"请换个说法":"你搜的是'报销标准',可以试试'报销比例'或'费用报销'"。给出候选词比空泛建议有效得多。
第三项要标注"这些不是你要的":直接展示相近结果而不说明,会让用户误以为系统答错了。第三项的实现要点是"区分标题匹配与正文匹配":相近结果通常来自标题匹配,而用户的目标可能在正文。所以展示时可以标注"标题相关",让他知道为什么出现。
// 空结果页的三种文案(按原因)
表达不匹配:没有找到"差旅报销标准"。试试:差旅费用 / 报销比例 / 费用制度
确有资料但不可见:该内容存在,但你的账号无查看权限,可申请开通
确实没有:库中没有相关资料。可提交需求(表单链接),或咨询财务共享中心
// 关键:三种文案都不一样,而"未找到相关内容"是最没用的一种
3. 自动放宽的边界
| 放宽方式 | 风险 |
|---|---|
| 去掉一个过滤条件 | 结果可能超出范围 |
| 降低相关度阈值 | 噪声增加 |
| 同义词扩展 | 语义漂移 |
| 跨语言/跨库检索 | 结果不可信 |
第一项要让用户知道"系统放宽了条件":否则用户以为看到的是精确结果。所以"已放宽条件"应当是可见标签。
第四项(跨语言/跨库)要特别谨慎:跨库检索的"相关"往往靠翻译或向量近似,准确率显著低于同库。如果不标注来源,用户会把不可靠的结果当成权威。
第二项要有下限:阈值降到 0.2 时,返回的是"任何东西",而用户会认为系统在胡说。
// 分步放宽:每一档都标注"放宽了什么"
def search(q, filters):
r = do_search(q, filters)
if r: return r, None
r = do_search(q, filters.without("year")) # 去掉年份
if r: return r, "已忽略年份筛选"
r = do_search(q, filters.without("year","dept")) # 再去部门
if r: return r, "已忽略年份与部门筛选"
return [], "未找到"
// 关键:返回"放宽说明",随结果一起展示
4. 引导改写的三种手段
| 手段 | 效果 |
|---|---|
| 从语料里提候选词 | 最准 |
| 拼写纠正 | 处理错字 |
| 同义词表 | 覆盖行业说法 |
第一项最有效:**从索引里取出"与查询共享字词的高频词"**第三项(同义词表)的维护成本最低:几十对词就能覆盖大部分行业术语差异,而它的收益立竿见影。代价是需要有人持续收集(从零结果日志里)。
,作为"你可能想搜"的候选。它用的是你的语料,不是通用词典。
第三项在专业领域很实用:"报销"与"费用"、"考勤"与"打卡"——这类词在通用词典里不同义,在内部语料里同义。
// 候选词生成的两种来源
语料侧:取"包含查询任一词"的文档标题,抽高频词(如"差旅费用""费用报销")
用户侧:取历史上"零结果后用户改写成的词"(最有价值,但需要日志)
// 前者可离线算,后者需要埋点:零结果 → 下一次查询的配对
空结果还有一个"同义"的形态:非空但无关。检索返回了 5 条,看起来"有结果",而用户实际没找到。它比"空"更糟——因为空会触发引导,无关结果不会,用户只能在噪声里翻。
// 三种结果状态的判定(都要有对应引导)
空: 结果数 = 0 → 给候选词/说明
弱相关: 最大相关分 < 阈值 → 提示"结果可能不相关"
正常: 最大相关分 ≥ 阈值 → 直接展示
// 反例:只看"结果数 > 0"就展示 → 弱相关被当成命中
5. 零结果日志怎么用
| 指标 | 用途 |
|---|---|
| 零结果率 | 判断"表达不匹配"的规模 |
| 零结果后改写率 | 用户是否愿意再试 |
| 改写后成功率 | 引导是否有效 |
| 零结果高频词 | 补词典/补内容 |
第三项(改写后成功率)是"引导质量"的唯一客观指标:如果给了候选词而用户点了仍搜不到,说明候选词是错的——这类问题只能靠这个指标发现。
第四项是最直接的行动项:高频零结果词就是"应该建同义词"或"应该补文档"的清单。
第二项反映体验:改写率低于 30% 说明用户已经放弃,此时"优化文案"意义不大,要先解决"搜不到"本身。
-- 零结果词 + 用户的下一次查询(最有价值的诊断数据)
WITH z AS (
SELECT user_id, q, ts FROM search_log WHERE result_count = 0
), n AS (
SELECT user_id, q, ts FROM search_log WHERE result_count > 0
)
SELECT z.q AS zero_q, n.q AS next_q, COUNT(*) c
FROM z JOIN n ON n.user_id = z.user_id AND n.ts > z.ts AND n.ts < z.ts + 60
GROUP BY 1, 2 ORDER BY c DESC LIMIT 50;
-- 输出:哪些零结果词被改写成什么 → 直接生成同义词表
落地上还有一个"埋点"的最小集合:每次搜索记录"查询词、结果数、最大相关分、是否点击"。四个字段就够支撑上面所有分析,而它们几乎不增加成本。
// 一次搜索的埋点(四字段 + 用户)
{"q":"差旅报销标准","n":0,"max_score":0.41,"clicked":false,
"uid":123,"ts":"2026-11-10T19:00:00Z"}
// 用途:零结果率、改写率、引导有效性、候选词生成
6. 排错:五种"空结果处理不当"
| 现象 | 成因 | 处理方向 |
|---|---|---|
| 用户重复搜同一词 | 无改写引导 | 给候选词 |
| 用户改一次就放弃 | 候选词质量差 | 用日志学改写 |
| 放宽后结果离题 | 无标注 | 显示"已放宽" |
| 有数据却搜不到 | 索引缺失 | 对账 |
| 认可内容但无权限 | 提示含混 | 说明并给出申请路径 |
第五项最伤体验:用户知道"应该有",而系统说"没有"第五项还有一个"合规"角度:"存在但不可见"本身可能是敏感信息(如"这个人的工资单存在")。所以提示的粒度要控制:说明"该内容存在但无权限",不说明"它叫什么"。
,于是他对整个系统的信任下降。正确做法是"存在但不可见"要明确说出来。
最后一条与"内容"有关的边界:空结果处理不能替代"补内容"。如果 30% 的查询是零结果,那正确动作是"补文档/补同义词",而不是"把提示写得更温柔"。提示只能修补体验,不能修补缺失。
// 两种原因的区分(看零结果率与改写成功率)
零结果率 30% + 改写成功率 40% → 表达问题(补同义词、改标题)
零结果率 30% + 改写成功率 5% → 内容缺失(补文档)
// 混淆这两者会导致"文案越改越好,用户还是搜不到"
7. 边界:什么情况不必处理空结果
| 场景 | 为什么 | 做法 |
|---|---|---|
| 内部工具(用户即作者) | 自己能补内容 | 简单提示 |
| 搜索是辅助入口 | 另有主路径 | 不处理 |
| 极低频查询 | 不值得 | 简单提示 |
| 已有强兜底(转人工) | 有人接 | 直接转 |
最后一条与"成本"有关的边界:空结果处理是"低投入高感知"的一类改动。加三种文案 + 一个候选词列表,成本可能是一天,而它直接改变"用户觉得这个系统好不好用"的判断。
判断口径一句话:用户搜不到之后会不会"再试一次"。会,就必须给改写引导;不会(直接转人工),把转人工做得顺畅更重要。
8. 自测
- 五种"空"的原因里,哪三项是检索问题、哪两项是数据/权限问题?为什么反应要不同?
- 为什么"请换个说法"是坏提示?"给出候选词"为什么更有效?
- 自动放宽条件时为什么必须标注"放宽了什么"?
- "从语料里提候选词"为什么比通用词典更准?
- 零结果日志的四个指标里,哪一个最直接指向"该补同义词"?改写率低说明什么?
- 判断"是否需要处理空结果"的那个问题是什么?