第四章 广告检索系统设计
广告检索,说白了就是在一堆广告里快速找到匹配的那几个。我刚开始做广告系统时,觉得这不就是个查数据库嘛,后来才发现——嗯,事情没那么简单。
一个成熟的广告系统,每秒要处理几万甚至几十万次请求。每次请求来了,你得在几十毫秒内从几百万条广告里找出符合条件的。这活儿,常规的数据库查询根本扛不住。
倒排索引原理
倒排索引,我习惯叫它「反向查字典」。你想想看,普通索引是「文档→关键词」,倒排索引反过来——「关键词→文档列表」。
举个例子。假设有三条广告:
- 广告A:标签「运动、篮球、夏季」
- 广告B:标签「运动、足球、冬季」
- 广告C:标签「科技、手机、夏季」
倒排索引长这样:
运动 → [A, B]
篮球 → [A]
夏季 → [A, C]
足球 → [B]
冬季 → [B]
科技 → [C]
手机 → [C]
用户搜「运动+夏季」,直接取交集——A和C。不用全表扫描,速度飞快。
核心要点:倒排索引的本质是用空间换时间。把关键词到文档的映射提前算好存起来,查询时直接定位。
我在项目中遇到过一个问题:广告标签可能有几百种,但每个广告的标签数量很少(一般不超过20个)。这时候倒排索引的稀疏性就很重要。我建议用位图(Bitmap)来存储文档ID列表,压缩率高,求交集也快。
广告检索的布尔表达式
广告检索的条件,本质上就是个布尔表达式。比如:
(性别=男 AND 年龄∈[18,30]) OR (兴趣=运动 AND 地域=北京)
怎么高效计算?我常用的方法是转成析取范式(DNF)。说白了,就是把表达式拆成「或」连接的多个「与」子句。
上面那个例子转成DNF:
(性别=男 AND 年龄∈[18,30]) OR (兴趣=运动 AND 地域=北京)
每个子句内部是AND关系,子句之间是OR关系。计算时,先算每个子句的匹配结果,再取并集。
避坑指南:我曾经把表达式嵌套太深,结果性能惨不忍睹。后来发现,把表达式转成DNF后,虽然存储空间多了点,但计算速度提升了3倍以上。记住:检索系统里,计算复杂度比存储成本更值钱。
具体实现时,我习惯用「跳表」来加速AND操作。两个有序列表求交集,跳表可以跳过大量不匹配的元素。你想想看,如果列表A有100万条,列表B有50万条,用跳表可能只比较几万次就出结果了。
检索性能优化
性能优化,我一般从三个维度下手:缓存、分片、索引结构。
缓存策略
缓存是性价比最高的优化手段。我建议分两层:
- 结果缓存:相同的查询条件,直接返回上次结果。适合热门广告位。
- 索引缓存:把倒排索引的热门关键词缓存到内存。适合高频标签。
但要注意,广告系统的数据更新很快。缓存时间不能太长,我一般设5-10秒过期。太短了缓存没意义,太长了数据不一致。
警告:千万别把所有索引都塞进缓存。我见过有人把10GB的索引全放内存,结果GC把服务搞挂了。只缓存Top 20%的热门关键词,能覆盖80%的请求,这才是合理的二八原则。
分片策略
当广告量超过千万级,单机就扛不住了。分片是必须的。
我常用的分片方式:
| 分片方式 | 原理 | 适用场景 |
|---|---|---|
| 哈希分片 | 按广告ID哈希取模 | 均匀分布,适合通用场景 |
| 范围分片 | 按地域/行业等维度分 | 查询条件有明确维度 |
| 一致性哈希 | 环形哈希,减少扩容影响 | 需要动态扩缩容 |
我个人习惯用一致性哈希。为什么呢?因为广告系统经常要扩容,一致性哈希只需要迁移少量数据,不像普通哈希那样要全量重排。
分片后还有个问题:查询时怎么聚合?我建议用「Scatter-Gather」模式——把查询请求广播到所有分片,各分片并行计算,最后汇总结果。注意要设置超时,避免某个慢分片拖垮整个请求。
实时索引更新
广告系统最头疼的问题之一:索引更新。广告主随时可能修改出价、暂停广告、新增创意。你不能每次修改都重建索引吧?
我常用的方案是「双缓冲」:
// 伪代码示例
class IndexManager {
Index currentIndex; // 当前服务使用的索引
Index buildingIndex; // 后台构建中的索引
void updateIndex() {
buildingIndex = buildNewIndex(); // 后台构建
// 原子切换
currentIndex = buildingIndex;
}
List<Ad> search(Query q) {
return currentIndex.search(q); // 无锁读取
}
}
说白了,就是准备两份索引。一份给线上查询用,另一份在后台慢慢构建。构建完了,指针一换,新索引就上线了。整个过程对查询线程无锁,性能影响极小。
经验之谈:我曾经用「增量更新」的方式,每次只更新变化的部分。但后来发现,增量更新容易产生碎片,索引膨胀得厉害。双缓冲虽然内存多了一倍,但稳定性和性能都好得多。对于广告系统这种对延迟敏感的场景,我建议优先考虑双缓冲。
更新频率怎么控制?我建议:
- 高优先级(如暂停广告):秒级更新
- 中优先级(如修改出价):分钟级更新
- 低优先级(如新增创意):小时级批量更新
这样既保证了实时性,又不会让索引构建太频繁。
小结
广告检索系统,核心就三件事:倒排索引让你找得快,布尔表达式让你算得准,缓存和分片让你扛得住。实时索引更新,则是保证数据新鲜度的关键。
嗯,这些内容看起来多,但真正落地时,你会发现每个点都有很多坑。我建议先从最简单的倒排索引+单机缓存开始,等量上来了再考虑分片和双缓冲。别一开始就搞得太复杂,容易把自己绕进去。