根據畫像标簽的需求場景,我們常常将畫像存儲分為兩部份,分别是:
- 畫像基本資訊的存儲
- 使用者畫像人群的篩選需求的存儲
常見畫像标簽存儲方式:
- 根據類目建立寬表,或者根據更新的頻率建立寬表
- 建立豎表-每個使用者+每個标簽=一條記錄
- 豎表+橫表=》分開計算,定時聚合
- 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 "" //bitmap_union 該标簽值使用者集合的并集
) 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表中,完成标簽的更新操作,定期執行以下操作
//清空bitmap表
truncate table user_tags_bitmap;
//将明細表導入到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的聚合表不需要手動維護,隻需要在聚合表中建立相應的物化視圖即可