天天看點

Doris 畫像标簽存儲實踐

根據畫像标簽的需求場景,我們常常将畫像存儲分為兩部份,分别是:

  1. 畫像基本資訊的存儲
  2. 使用者畫像人群的篩選需求的存儲

常見畫像标簽存儲方式:

  1. 根據類目建立寬表,或者根據更新的頻率建立寬表
  2. 建立豎表-每個使用者+每個标簽=一條記錄
  3. 豎表+橫表=》分開計算,定時聚合
  4. ES 标簽對象存儲,rowKey為user_id,HBASE存儲使用者明細,通過user_id關聯
  5. 倒排表,标簽-》多個使用者Id,bitmap方案

一、根據類目建立寬表,或者根據更新的頻率建立寬表

1. 這是最容易想到的方式,同時也是最直覺的方式,比如有基本資訊表,興趣愛好表
           
user_id 姓名 性别
1 老五
2 老四
user_id 購房意向 意向戶型
1 50-100m
2 200-300m
優點:
  1. 夠直覺,簡單,結構清晰,資料量可控
缺點:
  1. 标簽的增加需要修改表結構 — 隻要能确定标簽的範圍,那可以省略這個問題
  2. 存儲一定備援,會存在有些字段無值的場景
  3. 計算不友好,在同一表中的各标簽值計算時間和規則是不相同的,這些标簽值的來源常常來源于各實時計算,或離線計算程式,或者是有的每天計算一次,比如日活躍,有的是一周計算一次,比如周活躍
  4. 畫像資訊存儲隻是過程,最終的需求是要能夠任意次元标簽的快速查詢,而在二維性資料庫中,你無法對所有字段建立索引,人群篩選場景下查詢效率将會極低

二、建立豎表-每個使用者+每個标簽=一條記錄

  1. 這種方式應用于标簽值不固定的場景,可以了解為寬表的空間換時間方案
user_id 标簽類型 标簽值
1 性别
1 年齡 15
2 性别
優點:
  1. 易于擴充,适用于标簽不固定場景
  2. 存儲無備援,隻存有值的标簽項
  3. 計算友好,每一行記錄都是一個單獨的計算項
  4. 可以對标簽類型+标簽值建立聯合索引,任意次元查詢的場景性能會比較高
缺點:
  1. 資料量巨大,100萬使用者,每個使用者100個标簽,将會産生1億條資料
  2. 查詢不友好,比如我要統計性别=男,年齡=15的人,需要這樣寫:

    select user_id from user_tags where (tag='sex' and tag_value='男') and (tag='age' and tag_value='15')

三、豎表+橫表=》分開計算,定時聚合

  1. 通過豎表完成各統計項的計算,以及任意标簽次元查詢
  2. 定時将這些離散的資料彙聚到寬表中,用于使用者畫像基本資訊的展示
  3. 結合,第一種和第二種,能夠滿足,使用者畫像自定義次元的查詢,也能夠滿足使用者标簽資訊的展示
  4. 除了資料量問題,沒有其他問題
user_id 标簽類型 标簽值
1 性别
1 年齡 15
2 性别

定義彙聚為:

user_id 姓名 性别
1 老五
2 老四

四、ES 标簽對象存儲,rowKey為user_id,HBASE存儲使用者明細,通過user_id關聯

  1. 早期的大資料畫像存儲方案
  2. es的每一條記錄為一個文檔,同時也為一個使用者的畫像資訊,rowKey為user_id
  3. 人群篩選時,通過es查詢出比對的user_id集合,然後去Hbase中查詢這批集合的明細,能夠實作秒級的響應

五、倒排表,标簽-》多個使用者Id,bitmap方案

  1. ES搜尋之是以快,功勞在于他的反向索引上,是以又有了以下方案:

    常見的描述是使用者身上有哪些屬性,而反向索引是這個屬性有哪些使用者擁有

标簽類型 标簽值 user_ids
性别 1,2
性别 3
  1. 對标簽類型+标簽值建立索引,user_ids字段中存儲所有擁有這個标簽的使用者Id
  2. user_ids字段為bitmap類型,上面為了直覺,顯示bitmap轉換前的使用者Id更多bitmap描述見:https://www.sohu.com/a/300039010_114877
  3. 在對人群篩選時,隻需要對所有比對的結果取user_ids的交集即可

優點

  1. 在人群篩選上,是五種方案中的最優解
  2. 因為用bitmap存儲,是以占用空間少,100個标簽,每個标簽5個标簽值,隻有500條記錄
  3. 比對條件為二進制的與或非計算,效率很高,

缺點

  1. 更新困難,需要按照一定的時間周期來全局重新整理

Doris提供了這樣的支援,(其他OLAP大多也支援類似的功能,但Doris調研來看部暑擴充是比較容易的),bitmap函數,下面示範Doris中的使用

  1. 表結建構立
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;
           
  1. 添加資料
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"));
           
  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
  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後')
           

查詢結果如下:

user_ids
1
  1. 這樣的設計查詢效率雖然高,存儲也省空間,但還缺少标簽的更新能力,是以設計添加明細表,通過周期性的明細表更新到這張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
  1. 明細表這裡按照豎表設計,沒有用寬表,因為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());
           
  1. 再周期性,比如一天,将明細表的資料更新到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;
           
  1. 使用這種模式也能夠完成人群與标簽的交并差集篩選,像這樣,把某一類人群,當作一個标簽處理:
tag tag_value user_ids
性别 1,2
性别 3
年齡 15 1
人群 高意向 1,2

要求:高意向中排除性别為女的人群有哪些,那就可以 (人群=高意向)\(性别=女)兩個的差集

以上完成Doris使用者畫像标簽存儲

優化:

  1. 因為使用的bitmap,如果user_id分布稀疏,将會浪費比較大的存儲和計算,建議生成全局使用者自增id的字典表,映射到原始使用者id
  2. 标簽和枚舉類型的标簽值,建議也建立枚舉字典表
  3. Doris支援的物化視圖特性,可以使得bitmap的聚合表不需要手動維護,隻需要在聚合表中建立相應的物化視圖即可