去重键选错,职位搜索API对每个查询返回相同50条结果

职位搜索 API 因只用标题去重导致静默返回错误结果,改用含 apply_url 的组合键后修复。
一位职位搜索 API 开发者发现,无论用户搜 "python berlin" 还是 "rust berlin",返回的都是几乎相同的职位列表。排查后发现问题出在去重逻辑:代码只对职位标题做去重,导致同城不同公司的同名职位被静默合并、另外几条被丢弃。结果是 API 返回的不是更少的职位,而是错误的职位——没有报错、延迟正常、条数稳定,属于最难被监控发现的静默故障。修复方案是把去重键改为 (title, company, location, apply_url) 组合键,其中 apply_url 是跨聚合器唯一稳定的字段。作者强调:测试全绿是因为合成数据里每个标题都唯一,永远触发不了真实数据的碰撞;去重键的选择本质上是产品决策,而非数据清洗的卫生细节,它直接决定用户能看见什么、看不见什么。
一个做职位搜索 API 的开发者分享了一次堪称教科书级别的线上事故:无论用户搜 "python berlin" 还是 "rust berlin",返回的列表几乎一模一样,只是顺序不同。第一反应是上游数据供应商偷懒,但排查后发现真正的 bug 出在自己的结果归一化逻辑里。

去重键只用标题,等于在无声地丢掉真实职位
问题根源在于去重逻辑:代码对职位标题字符串做了单独去重,理由是 "Senior Backend Engineer" 这样的标题在一个城市里应该足够独特。
事实证明这个假设完全站不住脚。同一个都市圈里,三家不同公司在同一周都招 "Senior Backend Engineer",去重逻辑会把它们合并成一条记录,另外两条被静默丢弃。
所以 API 不是返回了更少的职位,而是返回了错误的职位,而且还很自信。这是最难在监控里发现的故障类型——接口没报错,延迟正常,返回条数也稳定,就是内容错了。
对任何做聚合类产品的团队来说,这个细节值得停下来想一想:你的去重键决定的不只是数据干净程度,它直接决定了用户看到什么、看不到什么。
静默故障(silent failure) 是与"响亮故障"相对的一类系统失效模式:服务不报错、不抛异常、延迟与吞吐指标均正常,但输出内容本身是错的。因为监控通常只覆盖可用性与性能指标(如 HTTP 5xx、P99 延迟、错误日志),这类故障很难被现有的告警体系捕获。对聚合类产品而言,静默故障的危害尤其大——用户不会立刻意识到数据缺失,而是慢慢对结果质量失去信任。业内把这种"指标全绿但用户不满"的状态称为监控盲区,应对方法通常是引入基于业务语义的校验,例如结果多样性抽样、跨查询结果交集分析,以及从用户侧收集显式反馈。
修复方案:用 apply_url 做稳定标识
最终的修复谈不上优雅,但很有效——把去重键改成 (title, company, location, apply_url) 的组合键。
四个字段里,apply_url 是唯一在不同聚合器之间保持稳定的标识符。标题会被改写,公司会改名,地址会被不同地理编码服务解析成不一样的坐标,但申请链接是没人会去重写的那一个字段。
这个结论的适用范围比职位搜索广得多。原文作者也特别提到,如果做的是职位、商品、活动、新闻之类的聚合产品,应该默认"人类可读的字段会骗你",然后找到那个不会骗人的字段。
去重键(dedup key) 是指在数据入库或查询时,用来判断两条记录是否"属于同一条"的字段集合。它直接决定数据库的 UNIQUE 约束、ETL 的 merge 逻辑以及 API 的返回集合。一个理想的去重键应当满足三个条件:跨数据源稳定、与业务语义一一对应、不受展示层改写影响。实践中常见的错误是选用"人类最爱看"的字段(标题、名称、描述),因为它们在跨源抓取时最容易被各个上游做 SEO 式改写。更稳健的做法是引入上游实体 ID、规范化 URL 或内容指纹(如对关键字段做哈希)。选择去重键时,还应明确"宁可重复还是宁可丢失"的倾向:重复只会降低体验,丢失则会直接改变结果集的完整性。
测试全绿,因为测试数据本身就是错的
整个过程中最扎心的一点是:测试套件一直是绿的。原因很简单——合成数据里每个标题都是独立的,永远触发不了真实的碰撞。
真实世界的数据充满了碰撞(collisions)。标题相同的概率远比直觉中高,尤其是在垂类招聘、二手交易、本地活动这类场景里。
这件事真正的教训不在于 "去重要用组合键" 这种技术细节,而是:去重键的选择本质上是一个产品决策,而非数据清洗的卫生细节。它决定了哪些数据被合并、哪些被丢弃,进而决定了用户能看到的全部内容。
合成数据(synthetic data) 是指人工构造、用于测试的模拟记录,通常为了保证可读性和可重复性而刻意制造出高度规整的字段。这种做法会导致测试集与真实数据分布严重脱节,尤其体现在碰撞率上——真实世界里姓名、标题、地名等高熵字段的重合概率往往远高于直觉预期(著名的生日悖论就是同类现象:23 人中两人生日相同的概率就超过 50%)。要避免这种盲区,常见手段包括:将生产数据脱敏后采样回灌、在测试集里显式注入同名/同标题样本、对去重逻辑单独做属性测试(property-based testing),以及在 CI 中统计"单次查询返回唯一标题数"这类业务级指标,让碰撞能在测试阶段被暴露。
从发现问题到打包成工具
作者最终把这个修复打包成了一个公开的职位搜索工具,托管在 x402.freeq.one/tools/jobs.html。据其描述,组合键的修复上线那一周,用户的投诉就停止了。
这个案例的价值不在于技术复杂程度——修复本身只有几行代码——而在于它揭示了一类容易被忽视的 bug:逻辑正确的代码,在错误的假设下会静默地产出错误结果。监控很难抓到它,测试也可能因为数据不够真实而漏掉它,只有真实用户的直觉反馈才能把它暴露出来。
如果你正在构建任何聚合类产品,可以先自查一件事:当前的去重键,在真实数据发生碰撞时,会丢掉哪一条记录?
逻辑正确但假设错误(correct code, wrong assumptions)是软件工程里极具代表性的一类缺陷:代码本身遵循既定规则运行,只是这些规则建立在一个与现实不符的前提上。它比语法错误或边界条件 bug 更难发现,因为问题不发生在函数内部,而发生在"函数存在的理由"层面。经典案例包括:假设用户名唯一、假设时区一致、假设货币单位相同、假设 ID 全局唯一。识别这类问题需要跳出代码去审视不变量(invariant)——即那些在整个系统里被默认为成立、却从未被显式验证或写入文档的前提。把它们记录下来、在评审时单独检视,是从结构上降低此类风险的有效手段。
相关推荐

3分钟看懂DeepSeek:从量化私募到开源AI领跑者的崛起之路
3分钟看懂DeepSeek发展史:从幻方量化的算力积累,到V2的MLA推理降本、V3的557.6万美元训练账单、R1开源引爆全球,再到V4与70亿美元A轮融资,梳理深度求索的关键节点与技术路线。

本地文学创作模型PK:Qwen3.8 27B微调版胜出,DeepSeek v4.1表现平淡
B站UP主横向测评七款本地文学创作模型:海明威、HumanLike Chat基于Qwen3.8 27B微调表现最佳,DeepSeek v4.1叙事平淡,东方朔角色混乱垫底。附三道测试题评分与使用建议。

梁文锋的三次豪赌:DeepSeek为何将82%毛利开源?
DeepSeek为何把82%毛利的推理框架DeepInfer开源,又启动千亿融资?从技术路线、国产芯片到组织纯粹性,解析梁文锋的三次豪赌与开源战略背后的商业与理想逻辑。