广告系统倒排索引:为什么你的召回又慢又漏

接手广告系统检索漏斗优化后,第一个拎出来的模块就是倒排索引。这篇文章总结倒排从“能用”到“极致”的优化思路。


倒排索引基础

正排: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 延迟:延迟有数量级的改善。