Memoria 开发记录 26:自然语言搜索不是加权求和——结构化查询与 AND 约束
前言
相册搜索看起来可以用一个简单公式解决:把地点、时间、语义、标签都转成分数,再按总分排序。但真实使用后会发现,这种做法很容易产生反直觉结果。
例如用户搜索“济南的猫”,地点命中很高的济南照片可能排在前面,即使画面里根本没有猫。搜索“夜晚的大明湖”,如果夜晚语义分数高,其他城市的夜景也可能混进来。这不是排序微调能解决的问题,而是检索模型本身错了。
自然语言检索中的多个条件,很多时候不是加权 OR,而是分层 AND。
为什么加权分数会出错
加权求和默认每个条件都可以互相补偿:
总分 = 地点分 + 时间分 + 语义分 + 标签分
这种模型适合推荐,不适合精确搜索。用户明确说了地点、时间和主体时,每个条件都应该有自己的边界。地点分不能补偿主体缺失,语义分也不能补偿时间不匹配。
“济南的猫”的真实语义是:
地点 = 济南
AND
视觉主体 = 猫
如果没有猫,结果就不是完全匹配。它最多可以被放到“可能相关”里,甚至在用户期望很明确时不应该展示。
查询解析要先拆结构
Memoria 当前将自然语言查询拆成几个层次:
时间日期:绝对日期 / 年度日期 / 当地时间段 / 星期
地点:国家 / 省 / 市 / 区 / POI / 景区 / 校区 / 商圈
视觉主体:用于 MobileCLIP 的英文语义
负向语义:用于排除近似错误
属性:人数、笑脸、媒体类型等
这一步必须由 LLM 明确输出结构化 JSON,而不是只输出几个搜索词。客户端再对 JSON 做校验、修正和执行。
结构化的好处是每个条件有自己的执行位置:
- 时间走 ObjectBox 时间索引;
- 地点走本地地址字段、POI/AOI、坐标半径;
- 视觉主体走 embedding;
- OCR 和 caption 作为文本特征;
- 结果排序只在候选集合内部发生。
地点只能限定候选,不能替代主体
之前一个关键错误是:当严格地点候选存在但语义没有命中时,系统会把这些地点候选作为 metadata fallback 加进相关结果。这样“济南的猫”就会退化成“济南的照片”。
修复后的规则是:只要查询中存在明确视觉语义,metadata-only fallback 就不能替代它。地点可以缩小候选边界,但不能让没有主体的照片混成相关结果。
有视觉主体:必须过主体语义或文本特征metadata-only 不补位纯机械查询:例如“10月”“青岛”“周末”可以直接返回元数据匹配结果
这让检索从“谁分高谁赢”变成“先满足必要条件,再排序”。
Prompt 也要表达系统能力
如果 prompt 没告诉 LLM 本地有哪些索引,LLM 很容易把一切都写进视觉语义。例如“晚霞”只输出 sunset sky,而忘记它隐含下午到傍晚的时间窗口;“国庆节”只输出 holiday travel,而忘记 10 月 1 日到 7 日这个年度日期范围。
新的 prompt 明确告诉 LLM:
- 年份明确的日期走绝对日期;
- 不指定年份的月份、节日、季节走年度 MM-DD;
- 白天、夜晚、晚霞、朝霞走当地分钟窗口;
- 地名写入 geo filter;
- 视觉语义必须是英文;
- 地点名不能直接塞进 embedding query;
- 多个条件按 AND 处理。
LLM 不负责算 day-of-year,只输出人类可读的 MM-DD,客户端负责解析。
UI 必须展示检索计划
检索正确不代表用户能理解。如果 UI 只显示结果,不展示“实际用了哪些过滤条件”,用户会误以为系统没有识别时间或地点。
因此搜索页需要展示完整 query plan,包括:
- 绝对日期;
- 年度日期范围;
- 当地时间段;
- 星期;
- 地点;
- 主体语义;
- 召回语义;
- 负向语义;
- 人数、笑脸、媒体类型等属性。
这既是产品解释,也是调试工具。用户看到 time=annual:10-01..10-31,就知道“10月”确实被识别成了时间过滤。
总结
自然语言相册搜索的核心不是把所有线索混成一个分数,而是识别哪些条件是硬边界,哪些条件只是排序信号。
关键经验包括:
- 多条件搜索应优先建模为分层 AND;
- 地点、时间和主体不能互相补偿;
- LLM 输出必须结构化,客户端必须校验;
- 不指定年份的月份和节日也属于机械时间过滤;
- metadata fallback 不能替代明确视觉主体;
- UI 应展示完整 query plan,避免用户误判系统行为。
对应提交:bb19d13、ea4638a、823466e、07c235e、89ecfe6。
