bitmap用户分群方法在贝壳DMP的实践和应用

2020 年 8 月 27 日

bitmap用户分群方法在贝壳DMP的实践和应用

1. 背景介绍


DMP 数据管理平台是实现用户精细化运营和和全生命周期运营的的基础平台之一。贝壳找房从 2018 年 5 月开始建设自己的 DMP 平台,提供了用户分群、消息推送、人群洞察等能力。关于贝壳 DMP 架构的介绍可参考文章:DMP平台在贝壳的实践和应用


目前,贝壳 DMP 数据覆盖了贝壳和链家的数亿用户,用户偏好和行为数据量达到数十亿,拥有上千维画像标签。在海量用户画像数据基础上实现用户分群,同时满足业务方越来越复杂的标签组合需求,提高人群包构建速度同时保证数据准确性,为此,我们对 DMP 平台进行了持续的迭代优化。


本文主要介绍 bitmap(位图)用户分群方法在贝壳 DMP 中的具体实践和应用。该方案上线后,贝壳 DMP 平台支持了秒级别的人群包数量预估,分钟级别的复杂人群包逻辑运算。


2. 用户分群方式介绍和对比


用户分群是在人群画像的基础上实现的。DMP 平台上包含已经加工处理好的用户画像标签,运营等同学通过在前端选择一些标签,设定这些标签之间的逻辑关系,通过引擎层的计算,最终得到符合这些标签条件的用户的集合。


在 Hive 数据层,用户画像是以关系型数据表的形式进行存储的,即构建了用户-标签的映射关系。考虑到 Hive 查询速度等方面的限制,我们最终选择了 ClickHouse(下文简称 CH)作为 DMP 平台底层的存储和计算引擎。在 Hive 数据表产出之后,通过启动 Spark 任务将 Hive 中的画像数据导入到 ClickHouse 中。


在上一版本的实现中,CH 中存储的是与 Hive 类似的关系型数据表。通过将人群包的标签组合逻辑转化成 CH SQL,将标签取值条件转化成 SQL 的 WHERE 条件部分,过滤查找符合标签条件的用户,进行得到符合条件的目标用户集合。但是这种方式一般要涉及到全表数据的扫描和过滤,且画像数据一般存储在多张表中,当选择的标签分布在不同的表中时,还会涉及到中间结果之间的 JOIN,限制了人群包数据的产出速度。


如果从另一个角度思考,使用标签进行用户分群,其本质还是集合之间的交、并、补运算。如果能够将符合每个标签取值的用户群提都提前构建出来,即构建好标签-用户的映射关系,在得到人群包的标签组合后直接选取对应的集合,通过集合之间的交/并/补运算即可得到最终的目标人群。bitmap 是用于存储标签-用户的映射关系的比较理想的数据结构之一。ClickHouse 目前也已经比较稳定的支持了 bitmap 数据结构,为基于 bitmap 的用户分群实现提供了基础。


3. Bitmap 用户分群方案思路


我们以开源数据库 ClickHouse 的 bitmap 数据结构为基础,将符合某个标签的某个取值的所有用户 ID(INT 类型)存储在一个 bitmap 结构中,构建出每个标签的每个取值所对应的 bitmap。


对于多个标签的逻辑组合,使用 bitmapAnd、bitmapOr、bitmapXor 函数进行 bitmap 之间的交、并、补运算(另外还有 bitmapToArray、bitmapCardinality 函数可用于 bitmap 的转换和基数计算),最终得到符合该标签组合的所有用户 ID 集合,即圈选出了所有符合这些画像标签的用户。


基于 bitmap 的用户分群方案完整流程如下图所示:



整个方案主要包含以下几个技术问题:


  1. 如何针对亿级用户构建全局连续唯一数字ID标识join_id?

  2. 如何设计bitmap生成规则使其适用于DMP上所有的画像标签?

  3. 如何将Hive表中的关系型数据以bitmap的形式保存到ClickHouse表中?

  4. 如何将标签之间的与/或/非逻辑转化成bitmap之间的交/并/补运算并生成bitmap SQL?


下面将逐一分析并解决这些问题。


3.1 亿级用户构建全局连续唯一数字 ID


DMP 系统中,用户都是使用 STRING 类型的 cust_join_key(不同数据表中用来关联用户的关联键)来进行标识的,不能在 bitmap 中直接使用,需要用 INT 类型的用户 ID 来标识所有的用户。同时原 hive 表中也是不包含 INT 类型的用户 ID 这个字段的,所以需要提前准备好 bitmap 分群方案所需的 bitmap_hive 表。


如何为 DMP 平台上用 STRING 类型的 cust_join_key 标识的亿级用户生成全局唯一的数字 ID 呢?hive 提供了基础的 row_number() over()函数,但是在操作亿级别行的数据时,会造成数据倾斜,受限于 Hadoop 集群单机节点的内存限制,无法成功运行。为此我们创新性的提出了一种针对亿级行大数据量的全局唯一连续数字 ID 生成方法。其核心思想如下:



具体解释为:


  1. 将全部亿级数据按照一定的规则分成多个子数据集(假设共有M个子数据集,每个子数据集各有Ni行数据,M >= 1,Ni >= 1,1<= i <= M),确保每个子数据集的数据可以在Hadoop集群的单个节点上使用行号生成函数ROW_NUMBER() OVER (PARTITION BY col_1 ORDER BY col_2 ASC/DESC)生成行号。每个子数据集中的行号都是从1开始,最大的行号为Ni。

  2. 对于第1个子数据集(M = 1)的数据,其最终行号是1,2,3,4,…,N1;对于第2个子数据集(M = 2)的数据,其最终行号是1 + N1,2 + N1,3 + N1,4 + N1,…,N2 + N1;对于第3个子数据集(M = 3)的数据,其最终行号是1 + N1 + N2,2 + N1 + N2,3 + N1 + N2,4 + N1 + N2,…,N3 + N1 + N2;…以此类推,得到全部亿级数据的行号,通过该方法为每个STRING类型的cust_join_key分配的一个行号,该行号即可作为标识每个用户的全局唯一的数字ID应用在bitmap结构中。


前面提到,DMP 所有的画像数据最终汇总到了 4 张 Hive 表中,分别保存用户的基本信息(base 表)、偏好信息(prefer 表)、行为信息(action 表)和设备信息(device 表)。构建好 join_id 后,还需要将 join_id 关联到用户画像表中,产出构建 bitmap 所需要的 bitmap_hive 表。到此也就完成了 Hive 数据层的准备工作。


3.2 Bitmap 的设计


3.2.1 标签梳理


在 DMP 用户画像标签体系中,标签数量多达上千个,根据标签属性,可将标签划分成枚举类型(enum)、连续值类型(continuous)、日期类型(date)。


枚举类型的标签,标签取值从维表中选择,标签和取值之间的逻辑关系只有等于、不等于,共 2 种。


连续值类型的标签,标签取值为整数,最小值是 0,根据标签不同,部分标签最大值可取 1、7、15…等,部分标签无最大取值限制。标签和取值之间的逻辑关系有等于、不等于、大于、大于等于、小于、小于等于,共 6 种。


日期类型的标签,标签取值格式为 yyyy-MM-dd,一般选择过去的某个日期,标签和取值之间的逻辑关系有等于、不等于、大于、大于等于、小于、小于等于,共 6 种。


根据标签对应的字段个数,可将标签划分成单一标签和复合标签。一个单一标签对应一个 hive 表(base 表和 device 表)的字段,例如常驻城市、是否安装贝壳 app、房屋近 3 天关注次数、最后一次浏览时间、设备使用习惯、贝壳激活距今天数等;对于复合标签,多个字段组合成一个标签。复合标签包含 1 个主要标签,同时包含 1 个或 3 个次要标签。


prefer 表中,2 个字段组合成一个标签,偏好字段对应主要标签,业务线字段对应次要标签。例如二手-偏好地铁、新房-偏好楼层、租赁-偏好面积等。


action 表中,4 个字段组合成一个标签,行为字段为主要标签,产品字段、业务线字段、业务城市字段为次要标签。例如贝壳-二手-北京-近 7 日活跃天数、贝壳-新房-天津-近 7 日搜索次数、链家-租赁-上海-近 7 日核访次数等。


两种不同的分类方式下,标签是可以任意组合的,其组合情况如下:


单一标签复合标签
枚举类型枚举类型单一标签枚举类型复合标签
连续值类型连续值类型单一标签连续值类型复合标签
日期类型日期类型单一标签日期类型复合标签


3.2.2 bitmap 的构建和运算转换


针对以上 6 大类标签,设计了针对每种标签的 bitmap 构建和运算转换规则。单一标签和复合标签这种分类情况中,两种类型标签的区别只是标签对应字段的数量。对于复合标签,约定将复合标签的所有字段名按照字典序排列,各个字段的取值也按照字段名的对应顺序排列,复合标签的主要标签即可按照单一标签的处理逻辑进行处理。因此,只针对枚举类型标签、连续值类型标签和日期类型标签三种标签类型进行设计即可。通过标签名 dim 和标签取值 dim_value 标识每个 bitmap(此处做了简化,实际应用中每个 bitmap 是通过数据版本 dt、标签所属 hive 表 tbl、标签名 dim 和标签取值 dim_value 来标识的)。


Bitmap 构建和运算转换的理论情况如下:


a. enum 类型标签


标签和取值之间的逻辑关系只有等于、不等于,共 2 种。


可构建 2 大类 bitmap:


1)将等于某个标签的某个取值的所有用户 ID 存储在一个 bitmap 中;


2)将全部用户存储在一个全量 bitmap 中。


单个标签取值到 bitmap 运算的转换关系为:


opvalue运算规则
=X直接选取相应的bitmap进行运算
<>X选取等于情况下的bitmap和全量bitmap进行异或运算


b. continuous 类型标签


标签和取值之间的逻辑关系有等于、不等于、大于、大于等于、小于、小于等于,共 6 种。


标签取值为整数,0、1、2、~。最小值为 0,部分标签有最大值,部分标签理论上无最大值。根据线上标签使用情况,为部分标签确定一个最大值,即得到一个取值区间。


可构建 2 大类 bitmap:


1)针对区间内的每个值,例如[0,100]区间内的每个值 X,构建>=X 的 bitmap。即将标签取值>=0(>=1、~、>=6、>=7、~、>=100)的所有用户 ID 各存储在一个 bitmap 中;


2)全量 bitmap 即为标签取值>=0 的 bitmap。


单个标签取值到 bitmap 运算的转换关系为:


opvalue运算规则
=8>=8 bitmapXor >=9
<>8>=0 bitmapXor (>=8 bitmapXor >=9)
>8>=9
>=8>=8
<8>=0 bitmapXor >=8
<=8>=0 bitmapXor >=9


解释:对于某个连续值标签,将取值>=8 的人群存储在一个 bitmap 结构 b1 中,将取值>=9 的人群存储在一个 bitmap 结构 b2 中,为圈出某个连续值标签取值=8 的用户群体,使用 b1 和 b2 做异或运算即可。


c. date 类型标签


标签和取值之间的逻辑关系有等于、不等于、大于、大于等于、小于、小于等于,共 6 种。所有的日期数据一定是小于当前日期的。同样根据实际情况和线上标签使用,需人为确定一个最大值,即得到一个取值区间。


同样可构建 2 大类 bitmap:


1)string 类型 yyyy-MM-dd 处理成数值类型 yyyyMMdd。对一个区间,例如[0,180]区间内的每个值 X 构建<=X 的 bitmap,即将标签取值<=-0d,<=-1d,<=-2d,<=-3d,<=-4d,<=-5d,<=-6d,… 的所有用户 ID 存储在一个 bitmap 中;


2)全量 bitmap 即为标签取值<=-0d 的 bitmap。


单个标签取值到 bitmap 运算的转换关系为:


opvalue运算规则
=2020-03-05<=20200304 bitmapXor <=20200305
<>2020-03-05<=-0d bitmapXor (<=20200304 bitmapXor <=20200305)
>2020-03-05<=-0d bitmapXor <=20200305
>=2020-03-05<=-0d bitmapXor <=20200304
<2020-03-05<=20200304
<=2020-03-05<=20200305


解释:对于某个日期类型标签,将取值<=20200304 的人群存储在一个 bitmap 结构 b1 中,将取值<=20200305 的人群存储在一个 bitmap 结构 b2 中,为圈出某个连续值标签取值=2020-03-05 的用户群体,使用 b1 和 b2 做异或运算即可。


3.2.3 边界值的处理


对于一些连续值和日期类型的标签,当标签取值取到定义的边界值或者标签本身的边界值时,按照上面的转化规则,会出现取出不存在的 bitmap 的情况。为此,需要对部分标签的边界值情况进行处理。


以近 7 日活跃天数这个连续值类型标签为例,在底层 hive 数据中,该标签字段的取值时[0, 7]。假设该标签为单一标签,针对该标签会生成>=0, >=1, >=2, >=3, >=4, >=5, >=6, >=7 共 8 个 bitmap。对于取边界值等于 7 的情况,根据上述规则,会使用到>=8 的 bitmap,然而并不存在这个>=8 的 bitmap,造成结果为空。所以需要针对这种边界值进行转换处理。具体的针对边界值的处理方案如下:


边界值X转换前的bitmap选择转换后转换后的bitmap选择解释
=7>=7 bitmapXor >=8>= 7>= 7不存在>=8的bitmap;=7的人群和>=7的人群一致
<>7>=0 bitmapXor (>=7 bitmapXor >=8)<7>=0 bitmapXor >=7不存在>=8的bitmap;<>7的人群和<7的人群一致
>7>=8不需转换,结果为空集合
>=7>=7不需转换
<7>=0 bitmapXor >=7不需转换
<=7>=0 bitmapXor >=8>=0>=0不存在>=8的bitmap;<=7的人群和>=0的人群一致


3.2.2 节提到,针对连续值类型和日期类型的标签,结合实际标签使用情况和数据库存储空间的限制,我们分别选择了[0,100]和[0,180]的区间构建 bitmap。对于当边界值取到 100 或-180d 的时候,也会出现因为不存在相关的 bitmap 而造成结果不准确的现象,此处可结合实际情况限制用户对标签的的最大取值为区间最大值减 1 或扩大区间范围以减少边界值的影响。


3.3 Bitmap_CK 表的设计


bitmap 数据是通过 Spark 任务以序列化的方式写入到 CH 中的,为此我们再 CH 中创建了一个 null 引擎的表,bitmap 的类型为 string。然后以 null 引擎的表为基础创建了一个物化视图表,通过 base64Decode()函数将 String 类型的 bitmap 转换成 CH 中的 AggregateFunction(groupBitmap, UInt32)数据结构,最后以物化视图表为物理表,创建分布式表用于数据的查询。同时为了减少 CH 集群的处理压力,我们还进行了一个优化,即在 null 引擎表之前创建了一个 buffer 引擎的表,数据最先写入 buffer 引擎的表,积攒到一定的时间/批次后,数据会自动写入到 null 引擎的表。


3.4 Hive 的关系型数据到 CH 的 bitmap 数据


Spark 任务中,先通过 spark SQL 将所需 hive 数据读取,保存在 DataSet<Row>中。根据不同的标签类型按照 3.2.2 中设计的规则使用 spark 聚合算子进行运算。处理逻辑如下:


单一标签复合标签
enum类型对于某个标签,读取标签的取值列和用户ID列,使用spark聚合算子将取值相同的用户ID进行聚合,最终对于该标签的每个取值,得到等于该取值的用户ID集合。
将数据版本、hive表名、标签名称、标签取值和用户ID集合写入CK。
复合标签是由多个标签组合而成的,其中有且只有一个主要标签,至少包含一个次要标签。
对于复合标签中多个标签的顺序问题,约定将这些标签以字典序排列。
复合标签的处理和单一标签的处理思路一致,即将主要标签按照单一标签的处理逻辑进行处理,所有标签字段按照字典序排列后作为标签名称,标签值也按照对应的顺序排列后作为标签取值。
continuous类型对于某个标签,读取标签的取值列和用户ID列,对数据进行flatMap操作,(如对于取值是3的一行,flatMap后将成为0、1、2、3共4行数据),对flatMap后的数据使用spark聚合算子将取值相同的用户ID进行聚合,最终对于该标签的每个取值,得到等于该取值的用户ID集合。
将数据版本、hive表名、标签名称、标签取值和用户ID集合写入CK。
date类型对于某个标签,读取标签的取值列和用户ID列,对数据进行flatMap操作,(如今天是2020-04-12,对于取值是2020-04-10的一行,flatMap后将成为2020-04-12、2020-04-11、2020-04-10共3行数据),对flatMap后的数据使用spark聚合算子将取值相同的用户ID进行聚合,最终对于该标签的每个取值,得到等于该取值的用户ID集合。
将数据版本、hive表名、标签名称、标签取值和用户ID集合写入CK。


在这个过程中,我们还使用了 bitmap 的循环构建、spark 任务调优、异常重试机制、bitmap 构建后的数据验证等方法来提高任务的运行速度和稳定性。


3.5 bitmap SQL 的生成


在 2019 年 7-8 月的优化迭代中,DMP 建设了自己的标签后台系统,设计了用于存储人群包标签组合的存储格式,本次的生成 bitmap SQL 的过程也是完全兼容了之前的标签组合存储格式。


通过处理人群包的标签组合,确定所需要的 bitmap 以及这些 bitmap 之间的逻辑关系(下图红线标识),最终生成的 bitmap SQL 示例如下图所示。同时通过使用 GLOBAL IN 代替比较耗时的 GLOBAL ANY INNER JOIN,CH SQL 运行效率也有了大幅度的提升。



4. 总结和展望


在整个方案的实现过程中,除解决上述技术问题外,我们还对 bitmap 方案的数据准确性验证、考虑到前后两种方案数据产出时间的差异,对两种 SQL 方案的选择切换、bitmap 方案不适用的少数场景、bitmap SQL 生成过程中全量 bitmap 的选择等问题进行了考虑。我们也对该方案中的核心技术申请了专利。


目前基于 bitmap 的用户分群方案已经上线运行超过 3 个月,支持秒级别的人群包数量预估,分钟级别的复杂人群包逻辑运算,日均支持 500+周期性人群包的运行,保障了消息推送任务的正常发送,日均触达用户数千万。


在下一步的工作中,我们将从覆盖全部用户画像标签、提升 bitmap 数据产出和构建速度、完善标签上线流程、丰富 bitmap 数据应用场景等方面对 DMP 平台进行持续的迭代优化。


本文转载自公众号贝壳产品技术(ID:beikeTC)。


原文链接


bitmap用户分群方法在贝壳DMP的实践和应用


2020 年 8 月 27 日 10:051497

评论 2 条评论

发布
用户头像
我最近也在使用clickhouse的bitmap功能,方便沟通下吗?我的邮箱是407368744@qq.com
2020 年 11 月 25 日 10:17
回复
用户头像
这都可以
2020 年 11 月 09 日 14:08
回复
没有更多评论了
发现更多内容

二叉查找树-增删查和针对重复数据的 Java 实现

多选参数

数据结构 算法 二叉树 数据结构与算法

队列高级应用之设计一个高性能线程池

架构师修行之路

分布式 线程池 架构设计 架构师

敏捷软件工程实践书籍

Bob Jiang

敏捷 敏捷书籍 工程实践

智“营”时代,众盟科技荣膺“2020毕马威中国领先消费科技TOP50企业榜单”

人称T客

面试造火箭,看下这些大厂原题

前端有的玩

Java 前端 面试题

网站改版神秘公式,教你躲避改版陷阱

北柯

创业 网站 网站搭建 网站改版

实践总结:在 Java 中调用 Go 代码

jiacai2050

架构师训练营 - 第 7 周学习总结

红了哟

第10周总结+作业

林毋梦

推荐一个替代印象笔记,onenote的神奇笔记!

申屠鹏会

笔记

简谈Python3关键字nonlocal使用场景

王坤祥

Python Python基础

IT人的身体健康

隆隆

IT人健康

应用研发平台特惠专场,助力企业加速数智化发展

应用研发平台EMAS

合约一键跟单软件开发技术,跟单系统搭建app

WX13823153201

比特币 区块链

libuv 异步模型之设计概览

Huayra

libuv 异步模型

面试官:说下对cookie,session,Token的理解

Java小咖秀

Java Java 面试

MySQL备份脚本,应该这么写

Simon

MySQL

一瓶可乐的自动售货机指令“旅程”

华为云开发者社区

物联网 嵌入式 华为云 数据传输 无线通信

一口气搞懂「文件系统」,就靠这 20 张图了

小林coding

操作系统 计算机基础 文件管理 文件存储 文件系统

HashMap、LinkedHashMap 学习笔记

陈俊

Phalcon注解学习

半亩房顶

php phalcon

年近而立,Java何去何从?

华为云开发者社区

Java 程序员 编程语言 开源代码 Bugayenko Yegor

JavaScript中的正则表达式详解

华为云开发者社区

Java 正则表达式 程序员 字符串 语法

图解JavaScript——代码实现(new、Object.create()、Object.assign()、flat()等十四种代码原理实现不香吗?)

执鸢者

Java 前端 代码原理

Rust竟然没有异常处理?

袁承兴

rust 异常 java异常处理

MySQL explain 中的 rows 究竟是如何计算的?

flyer0126

MySQL

AI能写浙江高考满分作文了!在线满分作文生成器,一键圆你满分梦

程序员生活志

AI

翻译: Effective Go (5)

申屠鹏会

golang 翻译

领域驱动设计(DDD)实践之路(二):事件驱动与CQRS

vivo互联网技术

DDD 架构设计 CQRS

troubleshoot之:分析OutOfMemoryError异常

程序那些事

Java JVM 异常 JIT

腾讯人均月薪7.5w,我这是又被平均了?

程序员生活志

腾讯 职场 薪资

bitmap用户分群方法在贝壳DMP的实践和应用-InfoQ