Skip to content
Charles Shao
Go back

Bidder 定向过滤(Targeting Filter):竞价漏斗最前端的一票否决

Updated:
views

本文属于 Bidder 竞价服务架构 系列。

总览篇把 Bidder 拆成 解析 → 定向过滤 → 召回 → 打分 → 出价 → 排序 → 填充 七级漏斗,上一篇深挖了第一级 解析(Parse)。本篇接着讲第二级 定向过滤(Targeting Filter):漏斗最前端、成本最低、削减流量最多的一道闸门,奉行「能便宜地砍掉的流量,就别让它往下走」。建议先读总览解析篇再回到这里。

解析把原始字节流变成了一个带 deadline 的内部请求对象 BidContext。拿到它之后,Bidder 做的第一件「减法」就是定向过滤:判断这次曝光该不该竞价、有没有广告能投。它不打分、不出价,只做布尔层面的一票否决——用最便宜的判断,尽早把没希望的流量和候选砍掉,好让后面昂贵的召回与打分只作用于「有希望」的少数。

Bidder 定向过滤子流水线示意图:解析产出的 BidContext 先进入请求级 gating(反作弊 / IVT、黑白名单、合规同意、底价校验)对整次曝光做一刀切,通过后再进入候选级布尔匹配(地域 / 时段 / 设备 / 媒体 / 人群包求交)逐条筛广告,随后做可本地布尔判断的频控粗筛(用户级全局频控、已达标 campaign 位图预排除),产出候选集交给召回精排;请求整体不合格或无任何存活候选时立即快速失败返回 no-bid,不再进入召回;黑白名单、定向规则、合规策略由 Nacos 热更新下发 定向过滤子流水线(自上而下):请求级 gating(对整次曝光一刀切)→ 候选级布尔匹配(对每条广告求交)→ 频控粗筛(可本地布尔的那部分),产出候选集交给召回。蓝色为两级过滤主步骤,橙色为可前置的频控粗筛,黄色为产出的候选集,紫色为配置面(Nacos)热更新,红色为快速失败路径。

TL;DR

Table of contents

Open Table of contents

一、定向过滤为什么值得单独一篇

总览里,漏斗的排布只有一条原则:越便宜、能过滤越多流量的处理越靠前。解析是第 0~1 级(只做翻译、不做过滤),而定向过滤是第一个真正「做减法」的环节——它决定了后面昂贵的召回、打分要处理多少流量。它有三个特殊性,值得单独成篇:

与「召回」的边界:本篇讲语义,召回篇讲数据结构

总览的七级漏斗里,定向过滤(第 2 级)和召回(第 3 级)是相邻的两级,且都涉及「把曝光上下文与广告定向条件求交」。它们到底怎么分工?本系列这样切:

定向过滤(本篇)召回(下一篇)
回答的问题该不该投 / 什么算匹配(语义、资格)怎么在百万候选里毫秒级求交(数据结构、算法)
关注点定向维度、布尔语义、请求级 gating、频控顺序、缺省语义倒排索引、RoaringBitmap、Bloom Filter、粗排截断
一句话定义「匹配」的规则实现「匹配」的引擎

换言之:定向过滤定义了「匹配」这件事的语义与边界,召回负责把这套语义在极限时间内高效执行。本篇会点到工程实现(第八节),但把数据结构的深挖留给召回篇——那是另一整篇的话题。

二、两类过滤:先请求级 gating,再候选级匹配

定向过滤内部其实是两层,顺序不能颠倒:

请求级与候选级两层过滤示意图:一次 Bid Request 先进入第一层请求级 gating,对整次曝光做一刀切——依次经反作弊 / IVT 初筛、黑白名单(媒体 / 域名 bundle / 地域 / 设备 / IP 段)、合规与同意(TCF consent / COPPA / 敏感类目)、底价校验(bidfloor 归一后连底价都够不上);请求合格后才进入第二层候选级布尔匹配,从百万级广告库出发与曝光上下文求交(地域 / 时段 / 设备 / 媒体 / 人群包),再做频控粗筛(已达标位图预排除),产出候选集 Top-N 交给召回 / 打分;请求整体不合格或无广告匹配时直接返回 no-bid,省掉整轮召回 两层过滤:第一层对整次曝光一刀切(请求级 gating),任何一条不过就整请求 no-bid、省掉整轮召回;只有请求合格,才轮到第二层对每条广告逐条布尔匹配(候选级)。先做最省的一刀切,是漏斗「越便宜越靠前」原则在这一级内部的再一次体现。

为什么顺序是「先请求级、再候选级」?因为请求级一次判断就能砍掉整个请求的全部候选,成本最低、收益最大;而候选级要对成千上万条广告逐条判断,昂贵得多。把最便宜、削减面最大的判断放最前,正是漏斗原则的体现。

三、请求级资格判定:先看这波流量值不值得竞价

请求级 gating 处理的是「对整次曝光成立、与具体广告无关」的判断。四类最常见:

3.1 反作弊 / IVT 初筛

程序化流量里混杂着大量无效流量(IVT)——机器人、数据中心 IP、点击农场、伪造的 App bundle 等。对这类流量出价是纯粹的亏损。定向过滤最前端会做一层轻量初筛:已知的作弊 IP 段 / 数据中心 ASN、异常 UA、声明与实际不符的流量特征等,命中即整请求丢弃。

注意「初筛」二字:请求级只做能本地快速判断的部分;复杂的作弊识别(依赖行为序列、跨请求关联、模型判定)是离线 / 近线的事,其结果以黑名单形式回灌到这里(呼应总览「状态靠回灌」的数据面原则),在线路径上只做一次布尔查表。

3.2 黑白名单

最朴素、也最高频的一票否决,本质是几张本地内存里的集合 / 位图,命中即放行或拒绝:

黑白名单的规模会随业务膨胀(见第九节),但判断本身必须是 O(1) 的本地查表,绝不能为一次黑名单判断发起远程调用。

3.3 合规与同意

这一层是法律红线,漏做的代价不是效果差,而是合规风险:

合规判断同样应配置化、可热更新(法规和媒体政策经常变)、可审计(每一次因合规拒投都要留痕),并作为一票否决置于最前——一旦不合规,后面算得再好也不能投。保守优先:consent 字符串解析失败 / 缺失时,应按「未授权」处理(宁可少投,不可违规),而非默认放行。

3.4 底价校验(bidfloor)

Bid Request 里的 bidfloor 是本次曝光的最低成交价。如果我方对这次曝光的出价上限(受 campaign 出价、pacing 系数约束)连底价都够不上,那么无论召回打分怎么算,最终都不可能赢——不如现在就 no-bid,省掉整条链路的算力

这里有个和解析篇呼应的细节:bidfloor 带币种(bidfloorcur),必须用解析层归一化后的统一币种来比,否则会拿美元底价去比人民币出价,判断完全失真。

小结:请求级 gating 的四道闸门——反作弊、黑白名单、合规、底价——共同回答一个问题:「这次曝光,我方到底该不该参与竞价?」 任何一条不过,立即 no-bid,这是全链路最省的一次淘汰。

四、定向维度分类:候选级匹配的输入

请求过了 gating,才轮到候选级匹配。要把「曝光上下文」与「广告定向条件」求交,先得清楚定向有哪些维度。广告主在投放后台设置的定向条件,落到 Bidder 里大致分这么几类:

维度曝光侧(来自 BidContext广告侧(campaign 定向条件)
地域 geoIP / GPS 解析出的国家 / 省 / 市投放地域集合(含排除地域)
时段 dayparting请求到达的本地时间、星期允许投放的时段(如工作日 9–18 点)
设备 / 环境OS、机型、网络(WiFi/4G/5G)、运营商、语言目标设备 / OS / 网络条件
媒体 / 版位媒体分类、App bundle、广告位类型(banner/视频/原生)、尺寸允许 / 排除的媒体与版位类型
人群包 / DMP用户 ID 命中的人群标签定向 / 排除的人群包
上下文 / 内容页面 / App 的内容分类、关键词内容定向条件

两个贯穿所有维度的概念:

两个维度上的口径陷阱,务必与配置面对齐

  • 地域 geo:IP 库与 GPS 的精度、粒度(国家 / 省 / 市)不一致,且一次请求可能同时带 IP 与 GPS 两个来源。要显式约定「以哪个为准、精度不足时落到哪一级」,并统一到 ISO 3166 编码(呼应解析篇 2.2 与配置落地篇 3.2 的口径归一)。
  • 时段 dayparting:「工作日 9–18 点」按广告主时区还是用户本地时区解释,跨国投放能差十几个小时。这条口径在配置面(层级配置篇 7.2)就要定死,运行时只按定死的口径比对,绝不在 Bidder 里临时猜时区。

频次也常被归为「定向」的一种,但它的判定顺序有特殊性——放到第六节单独讲。

五、布尔匹配的语义:最容易出错的地方

定向匹配本质是一个布尔表达式求值:把这次曝光的上下文代入每条广告的定向条件树,得到 true(可投)或 false(不可投)。看似简单,但语义细节里藏着几个最容易烧钱的坑。

5.1 定向条件树:AND / OR / NOT

一条广告的定向通常是多维度的组合:「投(北京 OR 上海)AND(iOS)AND(NOT 已排除媒体)AND(命中人群包 X)」。工程上把它表达成一棵布尔条件树,匹配即对树求值。关键约束:

走一个具体例子,把「维度间 AND、维度内 OR、排除、多值、短路」一次看全:

曝光上下文(来自 BidContext,已归一化):
  geo      = CN-Shanghai
  os       = iOS
  media    = news.example.com
  segments = {high_value, sports_fan}     // 用户命中的人群标签(多值)

广告 A 的定向条件树:
  geo       ∈ {CN-Beijing, CN-Shanghai}       // 维度内 OR
  AND os    ∈ {iOS}
  AND media ∉ {gambling.example.com}           // 排除媒体(NOT)
  AND segments ∩ {high_value} ≠ ∅              // 命中人群包(集合求交非空)

逐维求值(任一为 false 立即短路):
  geo       CN-Shanghai ∈ {北京, 上海}          → true
  os        iOS ∈ {iOS}                        → true
  media     news… ∉ {gambling…}                → true(未命中排除集)
  segment   {high_value,sports_fan} ∩ {high_value} ≠ ∅ → true
  ───────────────────────────────────────────
  整体 = true  → 广告 A 进入候选集

os 换成 Android,第 2 维即 false后两维直接不再求值——短路省下的,正是候选级最贵的那部分(人群包求交、内容匹配)。这也解释了排布顺序:把最便宜、最能砍量的维度(geo / os)放在条件树前面,让短路尽早生效

5.2 正向 vs 排除:别让 NOT 悄悄反转

排除定向(NOT / exclude)是 bug 高发区。「投全国排除上海」和「投上海」,在代码里可能只差一个取反,语义却完全相反。工程建议:

把这条语义摊成真值表(P = 正向集合,E = 排除集合,v = 曝光在该维的取值):

正向 P排除 E命中条件含义
非空v ∈ P只投 P
非空非空v ∈ Pv ∉ E先入选、再剔除
非空v ∉ E投所有、排除 E正向为空 = 不限
true该维不限(缺省语义见 5.3 / 5.5)

一句话:正向为空表示「不限」而不是「全不通」,排除永远是一次「减法」。把这两条钉死,NOT 就不会在某个分支被一个手滑的取反悄悄反转。

5.3 缺省语义:这一篇最贵的一个坑

「某个维度广告主没设定向条件,算什么?」 —— 这个缺省语义(default semantics)是定向过滤里最容易出事、代价最大的一处:

两种约定都「自洽」,但必须全系统统一、且与投放后台的语义严格对齐。真正的事故往往发生在边界:某维度在后台是「不限」、传到 Bidder 却被当成「空集」,于是「不限」被误判成「全不匹配」。这类 bug 不报错、不超时,只是悄悄地把钱投错或投不出去,极难在监控上第一时间发现。 务必用真实流量回放 + 明确的缺省语义单测来守住。

5.4 匹配依赖解析层的归一化

所有布尔匹配的正确性,都建立在字段已被解析层归一化的前提上(见解析篇 2.2 节):地域编码统一、设备 / OS 取值统一、币种统一、结构化 UA 与原始 UA 已收敛。否则同一个「iOS」在 A 家 ADX 叫 iOS、B 家叫 IOS、C 家靠 os=ios,匹配就会时对时错。定向过滤不负责归一化,但它是归一化质量的第一个「受害者」——上游归一化没做好,这一级的匹配全线失真。

5.5 把「缺省」与「缺失」收进一张决策表

5.3 的「广告主没设」(广告侧缺省)和 7.3 的「曝光字段缺失」(曝光侧缺失)是两个正交问题,却在同一处求值里相遇,最容易被混为一谈、在某个组合上「悄悄投错」。把二者摊进一张表,才能逐格约定清楚:

情形属于约定该维求值
广告某维未设定向广告侧未设 = 不限(推荐,须与后台一致)true
广告某维未设定向广告侧未设 = 全不通false(极易欠投,慎用)
曝光某维字段缺失曝光侧保守:缺失即不匹配false(合规 / 品牌安全场景)
曝光某维字段缺失曝光侧激进:缺失视为不限按「不限」放行(追量场景)

两条约定各自独立、都要显式可配置,且缺省语义必须在「后台展示 = 编译定型 = 运行时匹配」三处对齐(呼应配置落地篇 3.2 节把「未设」在编译期落成具体集合)。一旦某维在后台是「不限」、编译时被当成空集、运行时又把空集判成「全不通」,这条线就会零投放且不报错——正是 5.3 那类最贵、最难查的事故。

六、频次控制的位置问题:为什么不能全放最前

总览 3.2 节埋了一个钩子:频次控制(Frequency Capping,限制「同一用户对某广告 / campaign 的曝光次数」)看起来是纯粹的过滤,似乎该和黑白名单一起放最前端;但真正的精细频控做不到全放最前。本节把这个顺序问题讲透。

频次控制的位置示意图:左侧一组是能前置到定向过滤的频控(纯本地布尔、不依赖具体候选)——用户级全局频控(该用户今日已达总曝光上限则整请求丢弃)、已达标 campaign 的位图预排除(本地位图标记已封顶广告,求交时直接剔除);请求依次经这两步后进入召回(倒排 + 位图求交)产出存活候选;右侧一组是必须在召回之后才能做的精细频控——per-user×per-campaign 逐条封顶(查该用户看过这条广告几次,常需带超时的 Redis 点查)、随排序闸门与预算 / pacing 闸门并列作为付费前最后一道;一条注释说明为何不能全放最前:精细频控需要具体候选加用户历史,召回前无候选、也不宜为每次请求都远程点查,最后胜者进入填充响应 频控的两半:左半(用户级全局频控、已达标位图预排除)不依赖具体候选、可纯本地布尔判断,能前置到定向过滤;右半(per-user × per-campaign 精细封顶)依赖「具体候选 + 用户历史」,只能在召回之后、随排序闸门执行,且常需一次带超时的 Redis 点查。

6.1 能前置的:不依赖具体候选的粗筛

这两类频控只需本地布尔判断,可以放在定向过滤最前端:

6.2 必须后置的:per-user × per-campaign 精细封顶

真正的精细频控是「这个用户,对这条具体广告 / campaign,已经看过几次」。它做不到前置,有两个硬原因:

6.3 与预算闸门的类比

精细频控和预算闸门是同一类东西:都是「付费前对少数存活候选逐条施加的、依赖外部可变状态的最后一道闸门」。总览把它们一起放在「排序 + 预算/频控闸门」那一级(3.6 节),正是这个道理。它们同样面临分布式一致性问题——计数分散在多实例、真实曝光有延迟——解法也类似(本地计数 + 异步回灌 + 容忍少量超出)。分布式频次控制的一致性细节,本系列后续单独成篇。

一句话:频控里「与候选无关、可本地布尔」的部分尽量前置削流量;「per-user × per-campaign、需查历史」的部分只能后置、随闸门执行。把后者硬塞到最前端,要么逻辑上无候选可判、要么把 Redis 点查风暴提前引爆。

七、快速失败、缺省策略与热更新

7.1 快速失败:无候选立即 no-bid

定向过滤的每一步都可能把候选清空。一旦某步之后无任何存活候选,立即返回 no-bid,不再进入召回、打分。这与解析层的「非法即拒」一脉相承:越早失败,被浪费的下游算力越少。请求级 gating 不过是「整请求 no-bid」,候选级匹配后无候选也是「整请求 no-bid」——两处快速失败都要有明确的无出价原因码(NBR)便于归因。

7.2 过滤本身要轻量

有一个容易被忽视的悖论:过滤是为了省成本,但过滤本身也有成本。如果为了一次判断而发起远程调用、构建大对象、做复杂计算,就违背了「越便宜越靠前」的初衷。原则:

7.3 数据缺失时的策略:保守还是激进

现实里 BidContext 的某些字段可能缺失(上游没传、解析没解出)。这时匹配该算命中还是不命中?这是一个必须显式约定的策略,而非交给「恰好取到 null」的偶然行为:

没有普适答案,但必须统一约定、可配置,且与第五节的缺省语义区分清楚——「广告主没设定向」和「曝光字段缺失」是两回事,前者是广告侧、后者是曝光侧。

7.4 规则热更新:经 Nacos 下发

黑白名单、定向规则、合规策略、频控阈值都会频繁变动,绝不能靠发版更新。与解析层的适配规则一样,它们应经配置中心(Nacos)热更新下发到 Bidder 进程,原子切换生效(呼应总览「缓存更新须原子切换」——半新半旧的规则集会导致过滤行为不一致)。

八、工程实现要点(点到即止,深入留给召回篇)

本篇聚焦语义,工程实现只列几条关键约束,数据结构的深挖留给召回篇:

九、生产实战:常见问题

十、速查表

定向过滤的本质,是一道用最便宜的布尔判断、尽早砍掉最多流量与候选的闸门。它不产生竞价价值,却决定了竞价价值的作用范围——把该砍的流量在这里砍掉,后面昂贵的召回与打分才跑得起、跑得快。理解它的分层(请求级 / 候选级)、语义陷阱(缺省语义)、频控的顺序,即掌握了漏斗「越便宜越靠前」原则最典型的一次落地。下一篇进入第 3 级 召回,看这套匹配语义如何用倒排索引与位图在毫秒级执行。


延伸阅读

规范与一手资料:

附录:术语表


views
Share this post on:

Previous Post
Bidder 频次控制配置篇:五维配置与身份口径降级
Next Post
Bidder 解析层(Parse):把原始竞价请求变成内部可算对象