根据画像标签的需求场景,我们常常将画像存储分为两部份,分别是:
- 画像基本信息的存储
- 用户画像人群的筛选需求的存储
常见画像标签存储方式:
- 根据类目创建宽表,或者根据更新的频率创建宽表
- 创建竖表-每个用户+每个标签=一条记录
- 竖表+横表=》分开计算,定时聚合
- ES 标签对象存储,rowKey为user_id,HBASE存储用户明细,通过user_id关联
- 倒排表,标签-》多个用户Id,bitmap方案
一、根据类目创建宽表,或者根据更新的频率创建宽表
1. 这是最容易想到的方式,同时也是最直观的方式,比如有基本信息表,兴趣爱好表
user_id |
姓名 |
性别 |
1 |
老五 |
男 |
2 |
老四 |
女 |
user_id |
购房意向 |
意向户型 |
1 |
高 |
50-100m |
2 |
低 |
200-300m |
优点:
- 够直观,简单,结构清晰,数据量可控
缺点:
- 标签的增加需要修改表结构 — 只要能确定标签的范围,那可以省略这个问题
- 存储一定冗余,会存在有些字段无值的场景
- 计算不友好,在同一表中的各标签值计算时间和规则是不相同的,这些标签值的来源常常来源于各实时计算,或离线计算程序,或者是有的每天计算一次,比如日活跃,有的是一周计算一次,比如周活跃
- 画像信息存储只是过程,最终的需求是要能够任意维度标签的快速查询,而在二维性数据库中,你无法对所有字段建立索引,人群筛选场景下查询效率将会极低
二、创建竖表-每个用户+每个标签=一条记录
- 这种方式应用于标签值不固定的场景,可以理解为宽表的空间换时间方案
user_id |
标签类型 |
标签值 |
1 |
性别 |
男 |
1 |
年龄 |
15 |
2 |
性别 |
女 |
优点:
- 易于扩展,适用于标签不固定场景
- 存储无冗余,只存有值的标签项
- 计算友好,每一行记录都是一个单独的计算项
- 可以对标签类型+标签值建立联合索引,任意维度查询的场景性能会比较高
缺点:
- 数据量巨大,100万用户,每个用户100个标签,将会产生1亿条数据
- 查询不友好,比如我要统计性别=男,年龄=15的人,需要这样写:
select user_id from user_tags where (tag='sex' and tag_value='男') and (tag='age' and tag_value='15')
三、竖表+横表=》分开计算,定时聚合
- 通过竖表完成各统计项的计算,以及任意标签维度查询
- 定时将这些离散的数据汇聚到宽表中,用于用户画像基本信息的展示
- 结合,第一种和第二种,能够满足,用户画像自定义维度的查询,也能够满足用户标签信息的展示
- 除了数据量问题,没有其他问题
user_id |
标签类型 |
标签值 |
1 |
性别 |
男 |
1 |
年龄 |
15 |
2 |
性别 |
女 |
定义汇聚为:
user_id |
姓名 |
性别 |
1 |
老五 |
男 |
2 |
老四 |
女 |
四、ES 标签对象存储,rowKey为user_id,HBASE存储用户明细,通过user_id关联
- 早期的大数据画像存储方案
- es的每一条记录为一个文档,同时也为一个用户的画像信息,rowKey为user_id
- 人群筛选时,通过es查询出匹配的user_id集合,然后去Hbase中查询这批集合的明细,能够实现秒级的响应
五、倒排表,标签-》多个用户Id,bitmap方案
- ES搜索之所以快,功劳在于他的倒排索引上,所以又有了以下方案:
常见的描述是用户身上有哪些属性,而倒排索引是这个属性有哪些用户拥有
标签类型 |
标签值 |
user_ids |
性别 |
男 |
1,2 |
性别 |
发 |
3 |
- 对标签类型+标签值建立索引,user_ids字段中存储所有拥有这个标签的用户Id
- user_ids字段为bitmap类型,上面为了直观,显示bitmap转换前的用户Id更多bitmap描述见:https://www.sohu.com/a/300039010_114877
- 在对人群筛选时,只需要对所有匹配的结果取user_ids的交集即可
优点
- 在人群筛选上,是五种方案中的最优解
- 因为用bitmap存储,所以占用空间少,100个标签,每个标签5个标签值,只有500条记录
- 匹配条件为二进制的与或非计算,效率很高,
缺点
- 更新困难,需要按照一定的时间周期来全局刷新
Doris提供了这样的支持,(其他OLAP大多也支持类似的功能,但Doris调研来看部暑扩展是比较容易的),bitmap函数,下面演示Doris中的使用
- 表结构创建
CREATE TABLE `user_tags_bitmap`(
`tag` varchar(45) NULL COMMENT "",
`tag_value` varchar(45) NULL COMMENT "",
`user_ids` bitmap BITMAP_UNION NULL COMMENT ""
) ENGINE=OLAP
AGGREGATE KEY(`tag`,`tag_value`)
COMMENT "OLAP"
DISTRIBUTED BY HASH(`tag`) BUCKETS 2;
- 添加数据
insert into user_tags_bitmap(tag, tag_value, user_ids) values("性别","男",to_bitmap("1"));
insert into user_tags_bitmap(tag, tag_value, user_ids) values("性别","男",to_bitmap("2"));
insert into user_tags_bitmap(tag, tag_value, user_ids) values("性别","女",to_bitmap("3"));
insert into user_tags_bitmap(tag, tag_value, user_ids) values("年龄","90后",to_bitmap("1"));
- 查询导入结果:
select tag,tag_value,bitmap_to_string(user_ids) user_ids from user_tags_bitmap;
user_ids已经完成了自动聚合
tag |
tag_value |
user_ids |
性别 |
男 |
1,2 |
性别 |
女 |
3 |
年龄 |
90后 |
1 |
- 比如查询年龄=90后并且性别=男的用户有哪些,可以这样写:
select
bitmap_to_string(intersect_count(user_id, tag_value, '男', '90后'))
from user_tags_bitmap
where (tag_type='性别' and tag_value='男')
or (tag_type='年龄' and tag_value='90后')
查询结果如下:
- 这样的设计查询效率虽然高,存储也省空间,但还缺少标签的更新能力,因此设计添加明细表,通过周期性的明细表更新到这张bitmap表,明细表设计如下:
CREATE TABLE `user_tags`(
`user_id` varchar(45) null comment "",
`tag` varchar(45) NULL COMMENT "",
`tag_value` varchar(45) NULL COMMENT "",
`update_time` datetime null
) ENGINE=OLAP
DUPLICATE KEY(`user_id`,`tag`)
COMMENT "OLAP"
DISTRIBUTED BY HASH(`user_id`) BUCKETS 2;
user_id |
tag |
tag_value |
update_time |
1 |
性别 |
男 |
2020-10-10 10:10:10 |
2 |
性别 |
女 |
2020-10-10 10:10:10 |
1 |
年龄 |
15 |
2020-10-10 10:10:10 |
- 明细表这里按照竖表设计,没有用宽表,因为doris的更新策略为整行替换,宽表某字段的更新会出现并发和性能问题,而在这里竖表中更新用户的某个标签属性时,是先执行delete操作,再执行insert完成更新或删除操作
delete from user_tags where user_id=1 and tag='性别';
insert into user_tags(user_id, tag, tag_value, update_time) values("1","性别","女",CURDATE());
- 再周期性,比如一天,将明细表的数据更新到bitmap表中,完成标签的更新操作,定期执行以下操作
truncate table user_tags_bitmap;
insert into user_tags_bitmap(tag, tag_value, user_ids) SELECT tag,tag_value,to_bitmap(user_id) from user_tags;
- 使用这种模式也能够完成人群与标签的交并差集筛选,像这样,把某一类人群,当作一个标签处理:
tag |
tag_value |
user_ids |
性别 |
男 |
1,2 |
性别 |
女 |
3 |
年龄 |
15 |
1 |
人群 |
高意向 |
1,2 |
要求:高意向中排除性别为女的人群有哪些,那就可以 (人群=高意向)\(性别=女)两个的差集
以上完成Doris用户画像标签存储
优化:
- 因为使用的bitmap,如果user_id分布稀疏,将会浪费比较大的存储和计算,建议生成全局用户自增id的字典表,映射到原始用户id
- 标签和枚举类型的标签值,建议也创建枚举字典表
- Doris支持的物化视图特性,可以使得bitmap的聚合表不需要手动维护,只需要在聚合表中建立相应的物化视图即可