广告系统倒排索引:为什么你的召回又慢又漏
接手广告系统检索漏斗优化后,第一个拎出来的模块就是倒排索引。这篇文章总结倒排从“能用”到“极致”的优化思路。
倒排索引基础
正排:doc → [terms]
广告ID123 → ["宝马", "二手车", "北京"]
倒排:term → [docs]
"宝马" → [ID123, ID456, ID789, ...]
"二手车" → [ID123, ID234, ID567, ...]
检索时:query 分词 → 每个 term 的倒排链 → 求交 → 结果集。
倒排链的三种存储结构
1. 连续数组 (PForDelta)
最朴素的方式是存每个 docID,用差值编码 + PForDelta 压缩。
原始:[100, 105, 108, 200, ...]
差值:[100, 5, 3, 92, ...]
PForDelta 压缩后:每个差值用尽可能少的 bit
压缩比通常能达到 5-10x。
2. 跳表 (Skip List)
在倒排链中每隔 K 个位置放一个指针,求交时可以跳过大段。
倒排链: [100] → [105] → [108] → [200] → ...
跳表: [100] ──skip──→ [200]
3. 布隆过滤器 + 分层
高频词(如“北京”)的倒排链可能有几十万条。在第一层就用布隆过滤器快速排除不可能命中的广告。
截断策略:高截断率 → 近乎零截断 的优化
倒排链比较长时,不可能全部遍历。传统做法是按静态质量截断——比如只召回质量分前 K 个广告。
但固定截断有两个问题:
- 有些 query 场景需要更多候选(相关性分散)
- 有些 query 前 K 个已经足够(相关性集中)
我们做了动态截断:
if (已召回 >= 目标候选数 && 当前 doc 质量分 < 已召回最低分的 50%) {
截断
}
这个简单策略将截断率从 5% 降到了 0.19%(几乎相当于 limitless),同时延迟反而降低了——因为无效的遍历减少了。
延迟优化的关键数字
| 优化项 | P97 延迟下降 |
|---|---|
| 动态截断 | -8ms |
| PForDelta 压缩 | -5ms |
| 倒排链预排序(按质量分) | -3ms |
| 短链合并 | -2.5ms |
| 缓存热点 query | -2.5ms |
最终 P97 延迟:延迟有数量级的改善。