本教程将从零开始,详细介绍如何设置广告定向条件,并一步步教你构建基于这些定向条件的布尔表达式索引,以及如何利用该索引快速检索目标广告。
关注「Go学堂」,获取后续的代码实现部分
教程共分为三个主要部分:
- 布尔表达式索引的基础知识
- 布尔表达式索引的实现
- 基于布尔表达式索引的高效检索
本教程的适用对象:
- 从事在线广告系统的程序员,并想深入了解大型程序化广告系统如何构建高效的倒排索引。
- 从事在线广告的产品经理
- 从事在线广告的运营人员
第一部分 理论篇:布尔表达式索引的基础知识
要了解布尔表达式索引与广告之间的关系,首先让我们简要介绍一下程序化广告的相关知识。
1 什么是广告?
广告是为了某种特定的需要,通过一定形式的媒体,向公众推广自己商业产品的一种手段。
一句话总结就是:广告是推广商业产品的一种手段。
例如,如果你开发了一款面向20到30岁年轻人的社交应用,你需要让更多年轻人下载并使用这款应用。其中一种推广方式就是投放广告,通过这种方式让更多的目标用户了解并下载你的应用。
2 为什么需要定向投放?
所谓定向**,实际是为了筛选流量,以提高广告的转化效率**。流量筛选一般是指根据用户信息、设备信息以及上下文信息来决定展示特定的广告的过程。因为用户只会对跟自己相关的产品更感兴趣。
比如,你在北京开了一家超市,配送服务的范围是5公里范围内。如果你将推广超市商品的广告推送给了5公里范围外,设置推送给了河北的用户,那么即使有用户下单,最终也配送不到。这就是一次无效的投放,白白浪费了广告预算。
在广告系统中,通过设置特定的定向条件,就能达到筛选流量的目的。例如有如下广告ad1、ad2和ad3三个广告,其对应的受众分别如下:
3 用布尔表达式描述定向条件
布尔表达式就是最终结果只能是true或false的表达式。在广告定向中,就是用逻辑运算符将各种条件组合起来,形成一个布尔表达式。当用户的多个属性组合起来正好和广告的定向条件正好匹配时,就代表该用户是这个广告的目标用户。或者说这个广告是该用户的目标广告。
例如,上面广告ad1可以描述为:要想展示广告ad1,用户需同时满足:geo是北京,并且性别是男性,并且年龄是28岁,并且手机操作系统是android。 用表达式表示如下:
{geo∈(北京) ∩ gender∈(男) ∩ age∈ (28岁) ∩ os∈(android) }
上面3个广告的定向条件使用布尔表达式来表示 如下: ad1:
{geo∈(北京) ∩ gender∈(男) ∩ age∈ (28岁) ∩ os∈(android)}
ad2:
{geo∈(上海) ∩ gender∈(女) ∩ age∈ (25岁) ∩ os∈(ios}
ad3:
{geo∈(北京) ∩ gender∈(女) ∩ age∈ (25岁) ∩ os∈(ios)}
4 布尔表达式基础概念
4.1 合取范式
上面布尔表达式中,各个条件之间的关系都是“且”的关系,这样的表达式称之为**合取范式Conjunction。**例如:
geo∈(北京) ∩ gender∈(男) ∩ age∈ (28岁) ∩ os∈(android)
下面这个表达式,因为条件间存在“或”的关系,所以不是合取范式,而是析取范式。
{geo∈(北京) ∩ gender∈(男) } ** ∪ ** {age∈ (28岁) ∩ os∈(android)}
但是,“或”的关系可以转换成“且”的关系。这个后面我们会提到。
4.2 赋值(assignment)
布尔表达式又由类似geo∈(北京)这样的条件组成的,这里的条件被称之为赋值assignment。例如:
geo∈(北京)是一个赋值,gender∈(男)也是一个赋值。
可以看到geo、gender是用户的维度,“北京”、“男”是属性值,通过“∈”或“∉”符号将值赋值给维度。 维度、符号和值构成了赋值。也就是接下来要介绍的赋值三要素。
4.3 赋值三元素
赋值assignment又由3个元素组成:属性名attr(例如geo)、布尔原语predicate(例如∈或∉)、属性值values(例如北京)。
4.4 Conjunction ID
这里,我们还有一个概念,就是合取范式ID:**Conjunction ID。**在编程实现或下面的描述中用这个ID表示这个合取范式。若Conjunction中的条件相同,Conjunction ID也一定相同。这个ID实现的方式是用队列中的索引来表示。
比如,有以下3个广告及其对应的定向条件: ad1:
geo∈(北京) ∩ gender∈(男)
ad2:
age∈ (25岁) ∩ os∈(ios)
ad3:
geo∈(上海) ∩ gender∈(女)
三个广告的定向条件分别对应3个合区范式。把每个范式放到队列中,队列的索引即为对应的合取范式的ID。如下:
在实现中,会建立合取范式(Conjunction ID)到广告的索引。在检索系统中,会根据合取范式ID找到对应的广告列表。
4.5 Conjunction size
Conjunction size指的是合取范式的所有赋值assignment中,条件(即predicate)为“包含”的个数。 例如: ad1的定向条件如下:
gender∈(男)∩ age∉(28岁)
则该合取范式的size=1。因为predicate为“包含”的只有gender∈(男)
这一个赋值。
在实现部分,会根据size值对合取范式进行分层。
4.6 Predicate ID
Predicate ID用来表示合取范式中的第几个包含(或排除)条件。通常会将包含的条件排在排除条件的前面。例如: ad2的定向条件如下:
gender∈(男)∩ geo∉(北京)∩ age∈(28岁)
那么,在合取范式中gender∈(男)就是该合取范式中的第1个“包含”条件,age∈(28岁)是第2个“包含”条件,geo∉(北京)是第3个条件。 这个ID实现的方式同样也是用队列的索引来表示。如下:
注意,这里的Predicate ID只是该合取范式中的顺序,而非全局排序。这个ID主要是用于在检索系统跟合取范式的包含条件的个数来做比较。
4.7 Token ID
Token ID用来唯一表示全局的属性值。例如: ad1:gender∈(男)∩ age∈(28岁) ad2:gender∈(女)∩ age∈(28岁) 那么,ad1和ad2中age的“28岁”是相同的token ID。 Token ID依然是使用队列中的索引来实现。如下:
在实现部分,会用token ID来表示用户的属性值。在索引中会建立属性值(token ID)到合取范式的索引。在检索系统中,则根据token ID检索到合取范式列表,根据合取范式列表,再检索出广告列表。
4.8 析取范式
析取范式就是合取范式的并集。例如,一个广告ad4的目标受众有两种:
ad4 -> {geo∈(北京) ∩ gender∈(男) ∩ age∈ (28岁 or 35岁) ∩ os∈(android) }
以上定向条件可以转换成两个合取范式:
合取范式1:{geo∈(北京) ∩ gender∈(男) ∩ age∈ (28岁) ∩ os∈(android)} 合取范式2:{geo∈(北京) ∩ gender∈(男) ∩ age∈ (35岁) ∩ os∈(android)}
只要满足上述任意一个合取范式的用户,就能展示这条广告。所以,本质上一个广告是对应一个析取范式的。只不过大部分场景下该析取范式只有一个合取范式而已。
5 布尔表达式索引
索引,是为了快速检索而存在的。 在广告系统中也一样。为了能够快速的为用户匹配到合适的广告,则也会根据广告的定向条件来做对应的倒排索引。 又因为广告的定向条件可以用布尔表达式表示,所以又叫做布尔表达式索引。
5.1 正排和倒排索引
广告里的正排索引就是根据广告ID查找到广告的信息。如下:
倒排索引就是根据属性值查找广告。如下:
实际上,广告的布尔表达式索引是要建立属性值->合取范式->广告这样一个二级倒排索引结构。
广告的检索系统,就是当一个用户请求时,根据用户的属性值,基于**(属性值、合取范式、广告)**的二级索引结构,一级一级最终找到广告列表。
本节内容,我们都基于以下广告示例: ad1:age∈(20) ad2:age∈(20)∩ gender∈(女) ad3:age∈(20)∩ geo∈(北京) ad4:age∈(20)∩ gender∈(女)∩ geo∈(北京) ad5:age∈(20)∩ gender∈(女)∩ geo∈(北京) ad6:age∈(20)∩ geo∈(北京)∩ mobile∈(ios) ad7:age∈(20)∩ gender∈(女)∩ mobile∈(ios)
5.1 构建合取范式
以上7个广告的定向条件,能够转换成6个合取范式,如下: ad1的定向条件组成合取范式如下:
Conjunction1:age∈(20),size=1
ad2的定向条件组成合取范式如下:
Conjunction2:age∈(20)∩ gender∈(女),size=2
ad3的定向条件组成合取范式如下:
Conjunction3:age∈(20)∩ geo∈(北京),size=2
ad4和ad5的定向条件相同,所以合取范式也相同,如下:
Conjunction4:age∈(20)∩ gender∈(女)∩ geo∈(北京),size=3
ad6的定向条件组成的合取范式如下:
Conjunction5:age∈(20)∩ geo∈(北京)∩ mobile∈(ios),size=2
ad7的定向条件组成的合取范式如下:
Conjunction6:age∈(20)∩ gender∈(女)∩mobile∈(ios),size=3
各个合取范式中的size表示对应的合取范式中有几个“包含”条件。
5.2 构建第一层倒排索引:合取范式到广告的映射
第一层倒排索引是合取范式到广告之间的索引关系。
基于以上广告的定向,我们可以建立从合区范式到广告的第一层索引。如下:
如果只有一层索引,我们看下检索广告的过程。我们假设请求的用户属性如下:
age∈(20)、gender∈(女)、geo∈(北京)、mobile∈(ios)
基于一层索引,要想找到用户的这些属性值和哪些合取范式匹配,就需要依次循环每个合取范式,然后再比较每个合取范式中的各个赋值值是否匹配。
下面以其中的Conjunction4为例进行说明。如下:
- 获取Conjunction4中“包含”的个数size=3,初始化匹配到的属性个数matched=0
- 依次比较Conjunction4中的属性值。
- Conjunction4中的第1个属性值“20岁”,和用户属性值依次比较,有20岁,因此,matched+1=1
- Conjunction4中的第2个属性值“女”,和用户属性值依次比较,能匹配到,因此,matched+1=2
- Conjunction4中的第3个属性值“北京”,和用户属性值依次比较,能匹配到,因此,matched+1=3
- Conjunction4中的属性遍历完毕
- 查看Conjunction4中匹配到的属性个数matched和size是否相等。
- 若matched==size,则表示该用户的属性完全匹配Conjunction4的定向属性。说明该Conjunction4下的广告是目标广告
- 若matched != size,则表示该用户的属性值和Conjunction4的属性值不完全匹配。说明该Conjunction4下的广告不是目标广告
- 继续匹配下一个Conjunction以及其对应的属性列表。
可以看到,每一个Conjunction都需要经历上述过程。遍历Conjunction列表一层循环、每个Conjunction中的属性列表一层循环、依次和用户属性列表进行比对一层循环,所以共计3层循环。其时间复杂度是O(n³) 。
for conjunction in conjunction-list:
for value in conjunction.values:
for user-value in user.values:
if value == user-value:
matched++
end if
end for
end for
end for
只有合取范式到广告这样一层的索引情况下,效率是非常低的。要知道,在程序化广告中,从用户访问,到广告展示给用户,只有短短的毫秒级的时间。而且,在实际的系统中,广告库是庞大的,定向条件比这更复杂。
所以,我们还需要继续优化索引,即第二层索引。
5.3 构建第二层倒排索引:属性值到合取范式的映射
第二层索引是属性值到合取范式的映射。其目的是为了能够根据用户的属性值直接找到符合条件的合取范式列表,把不相关的合取范式列表给过滤掉,以此来提高检索速度。
比如第1个合取范式中Conjunction1:age∈(20岁),size=1
涉及一个属性值age∈(20岁)。构成倒排索引如下:
第2个合取范式中 Conjunction2:age∈(20岁)∩ gender∈(女),size=2
涉及两个属性值。构成的倒排索引如下:
其中age∈(20)属性值在第1和第2个合取范式中都存在,合并后就成为如下映射:
age∈(20岁)→ [Conjunction1,Conjunction2]
上述7个广告的定向条件中共涉及到如下4个属性值:
age∈(20)、gender∈(女)、geo∈(北京)、mobile∈(ios)
各个属性值最终构建的映射列表如下:
如果合取范式中的条件有不属于的属性,该怎么表达呢? 例如,ad7的定向条件更改成如下:
age∈(20)∩ gender∈(女)∩mobile∉(ios)
本质上属性值都是ios,但只是赋值符号即Predicate不同。要么是“包含”,要么是“排除”。因此,我们用一个二元组来表示合取范式以及该属性的赋值符号Predicate,如下:
[Conjunction5,∉]
最终属性值到合取范式的映射变成如下这样:
ios → ([Conjunction5,∉], [Conjunction6, ∈])
为了格式上的统一,其他维度值到合取范式的映射关系,我们也把这个∈和∉的操作符加上。
那么,最后第二层的映射关系整体会变成如下这样:
再结合第一层索引,最终就建立好了属性值到合取范式,合取范式再到广告的这样一个二层索引关系。
5.4 二层倒排索引的检索过程
我们假设用户的属性值有2个:age∈(20岁)、geo∈(北京)。
根据age∈(20岁)能找到对应的合取范式列表:
[Conjunction1,∈]、[Conjunction2,∈]、[Conjunction3,∈]、[Conjunction4,∈]、[Conjunction5,∈]、[Conjunction6,∈])
然后,根据geo∈(北京)能找到对应的合取范式列表:
([Conjunction3,∈]、[Conjunction4,∈]、[Conjunction5,∈])
最终,根据两个属性值找到的合取范式还是全部。但我们知道,Conjunction4、Conjunction5、Conjunction6是不符合条件的。因为这几个合取范式需要满足多于2个的条件才行,而我们的用户只有2个属性值。
显然还是没达到筛选的目的。怎么办呢?这就用到size值对Conjunction进行分层。
5.5 利用size对Conjunction进行分层
如果一个用户有N个属性。那么,合取范式中赋值的包含个数必须小于等于N,才有可能符合该用户的定向条件。
举例: 用户有两个属性age∈(20岁)和geo∈(北京),那么Conjunction4、Conjunction5、Conjunction6都不会被匹配到。因为这三个合取范式中定向条件是3个。Conjunction4中不满足gender∈(女)的条件。Conjunction5和Conjunction6中不满足mobile∈(ios)的条件。
因此,用size=N对第二层的倒排索引再进行分组,就能进一步的降低检索的数据量,提高检索效率。
最终,进行分层后的第二层倒排索引就会变成如下这样:
size | 维度值 | 合取范式列表 |
---|---|---|
size=1 | age∈(20岁) | ([Conjunction1,∈]) |
size=2 | age∈(20岁) | ([Conjunction2,∈]、[Conjunction3,∈]) |
gender∈(女) | ([Conjunction2,∈]) | |
geo∈(北京) | ([Conjunction3,∈]) | |
size=3 | age∈(20岁) | ([Conjunction4,∈]、[Conjunction5,∈]、[Conjunction6,∈]) |
gender∈(女) | ([Conjunction4,∈]、[Conjunction6,∈]) | |
geo∈(北京) | ([Conjunction4,∈]、[Conjunction5,∈]) | |
mobile∈(ios) | ([Conjunction5,∉]、 [Conjunction6, ∈]) |
再来看下这个检索过程。 还是收到了用户属性为age∈(20)∩ geo∈(北京)的请求。
因为该用户的属性值个数为2,则从广告的倒排索引中查找size≤2的合取范式列表: size=2的合取范式列表:
age∈(20岁)→ ([Conjunction2,∈]、[Conjunction3,∈])
geo∈(北京)→([Conjunction3,∈])
那同时满足age∈(20岁)和geo∈(北京)两个属性值的就只有Conjunction3及其对应的广告了。
再来看size=1的查询:
age∈(20岁)→ ([Conjunction1,∈])
只有Conjunction1满足条件,对应的广告就是ad1。
你看,这样是不是通过属性个数就能排除一部分数据,提高了检索速度了。
当然,在实际实现过程中,size到合取范式的索引会有另外一种变体。就是放在合区范式中。如下:
维度 | 合取范式列表 |
---|---|
age∈(20岁) | ([Conjunction1,∈,size=1]) |
age∈(20岁) | ([Conjunction2,∈,size=2]、[Conjunction3,∈,size=2]) |
gender∈(女) | ([Conjunction2,∈,size=2]) |
geo∈(北京) | ([Conjunction3,∈,size=2]) |
age∈(20岁) | ([Conjunction4,∈,size=3]、[Conjunction5,∈,size=3]、[Conjunction6,∈,size=3]) |
gender∈(女) | ([Conjunction4,∈,size=3]、[Conjunction6,∈,size=3]) |
geo∈(北京) | ([Conjunction4,∈,size=3]、[Conjunction5,∈,size=3]) |
mobile∈(ios) | ([Conjunction5,∉,size=3]、 [Conjunction6, ∈,size=3]) |
这样,通过age∈(20岁)找到合取范式列表:
[Conjunction1,∈, size=1]、[Conjunction2,∈, size=2]、[Conjunction3,∈,size=2]、[Conjunction4,∈,size=3]、[Conjunction5,∈,size=3]、[Conjunction6,∈,size=3])
再把size>2的合区范式过滤掉,就剩如下列表:
[Conjunction1,∈, size=1]、[Conjunction2,∈, size=2]、[Conjunction3,∈,size=2]
然后根据geo∈(北京)找到合取范式列表:
([Conjunction3,∈,size=2]、[Conjunction4,∈,size=3]、[Conjunction5,∈,size=3])
同样把size>2的合取范式过滤掉,就剩
[Conjunction3,∈,size=2]
最终根据两个属性值找到的合取范式如下:
Conjunction1、Conjunction2、Conjunction3。
但Conjunction2需要满足gender∈(女)的条件,显然不是该用户的目标。
那如何再从Conjunction1、Conjunction2、Conjunction3中找到即满足age∈(20岁),又满足geo∈(北京)的合区范式呢?
5.6 计算同时满足所有属性值的合取范式
上面我们只是根据每个属性值找到了对应的合取范式列表:Conjunction1、Conjunction2和Conjunction3。 但我们还需要进一步剔除Conjunction2。那就是通过排序+计算的算法来实现该目标。这里我们只用图解说明下思路即可。
我们已经根据用户的两个属性分别找到了各自的合取范式列表,每个范式链表都按ID从小到大排序。同时,取出每个属性值的合取范式列表中的第一个元素,也按从小到大排序。如下:
然后,我们看下查找最终合取范式的算法步骤: 1、将每个属性值的合取范式列表按编号从小到大排序。同时,纵向中,也按合取范式列表编号从小到大进行排序。 2、取各属性合取范式列表中的第一个元素,组成一个纵向列表。 3、取纵向列表中的第1个元素作为基准,根据其size值,取纵向列表的前size个元素。 4、若前size个元素的合取范式ID都相等,则说明该合取范式的定向条件和用户的定向条件匹配。 5、若第size个元素的合取范式ID和第一个不相等,则说明该用户的属性不满足该合取范式的所有条件。
以上面为例,两个属性的链表中的各自第1个元素组成一个纵向有序队列,如下
纵向列表中第一个元素是Conjunction1,该合取范式的size=1,说明只需要有一个条件满足即可。所以取纵向列表的前1个元素。同时,将Conjunction1从第一个范式链中移除,纵向列表中的每个元素又重新组成一个新的有序列表。如下:
再以纵向列表中的第1个元素Conjunction2为基准,取前size=2个元素,比较最后一个元素的合取范式ID是否和Conjunction2相等。因为不相等,则将Conjunction2从第一个链表中移除。再组成一个新的纵向有序列表。如下:
再以纵向列表中的第1个元素Conjunction3为基准,取前size=2个元素,比较最后一个元素的合取范式ID是否和Conjunction3相等。因为相等,说明该用户的属性满足Conjunction3的所有定向条件。同时将Conjunction3从两个列表中都移除。
这样,我们就找到了Conjunction1和Conjunction3是符合条件的,再找到各自对应的广告ID即可。
以上只是逻辑上的布尔表达式索引和检索过程。第二部分,我们将从代码实现的角度来讲解布尔表达式索引的构建和检索过程。
总结
本文是构建布尔表达式索引的理论篇,后续代码实现均以此为基础。在理论篇中,主要从正排如何演化成合取范式到广告的倒排,再演化成属性值到合取范式的倒排过程。