基於bitmap實現用戶畫像的標籤圈人功能

用戶畫像系統中有一個很重要的功能點: 基於標籤圈人。這裏有個很核心的概念,什麼是標籤?java

標籤是簡化用戶表示的一種思惟方式。 刻畫用戶的標籤越多,用戶畫像就越立體。 好比:
90後,碼農,宅男 3個標籤就刻畫了一類人。標籤相似於戲曲中的臉譜來表現人物的性格和特徵。 數據結構

標籤有哪些類型呢?ide

枚舉類標籤: 描述性別,地理位置。這類標籤取值一般是可枚舉出來的。
時間類標籤: 描述業務觸達和流失時間信息。 注: 時間類標籤可存儲成數值。
數值類標籤: 好比帳戶金額,積分數量等。oop

因此本質上,標籤只有兩種: 離散枚舉和連續數值。 性能

有了標籤後,如何在計算機中建模存儲呢?大數據

最簡單最直觀的方式就是設置大寬表,即每一個標籤一個字段。 一般一個小型的畫像系統,有幾百個標籤足夠。因此對於大部分場景寬表足夠簡單可依賴。優化

寬表通常存儲在Hive中,出於性能考慮,會存儲到Impala中。當數據量較大時,Impala也通常沒法知足查詢的性能需求。這是由於Impala沒有索引,每次查詢都是掃表。因此,爲了可以利用索引提高性能,大寬表通常會從Impala轉存到Elasticsearch中。this

當一個用戶Id附着成百上千個標籤,按ES存儲方式,會至關耗費存儲資源,導入數據到ES也會成爲性能瓶頸。 因此變通的方案是將全部的標籤存儲到ES的一個array字段中。但本質上,仍是大寬表的方案。rest

大寬表的方案最大的問題: 新增標籤時間成本太大,因此畫像系統基本是T+1的實效性。 若是對響應時間沒有苛刻的要求,基於Hadoop生態的ad hoc查詢引擎構建寬表,好比Impala或presto是能夠使用多張寬表來解決新曾標籤T+0生效問題,畢竟大數據系統,存儲資源仍是很充足的。code

惋惜的是業務對系統的需求是: 更高,更快,更強,像體育運動同樣。

咱們用ES存儲標籤,查詢速度快的緣由是ES構建了倒排索引。咱們構建標籤時,標籤數據的主體是用戶ID, 而在ES的世界,站在倒排索引的角度,標籤數據的主體是標籤,這徹底是兩個對立面。

咱們使用標籤圈人,本質上是集合的交併補運算。 因此,咱們能夠乾脆再往前邁一步: 直接構建標籤-用戶ID的映射關係,而非原始的用戶ID-標籤

這樣,整個數據結構就變成相似以下的樣式:

男: 張三,李四,王五...

因爲一個標籤能夠圈定上億的用戶,如何存儲這樣的結構? RoaringBitmap 。這樣存儲後,標籤圈人就脫離了SQL和ES語法,還原到最本質的集合運算:A and B or (C and D)

使用標籤-用戶ID這種數據建模方式,有個很大的問題: 數值類標籤的處理。好比用戶積分。 一般有一種解決方法就是分段,然而這樣作損失了數據精度。變得不靈活了。還有一種解決方法是爲每一個值創建一個bitmap。 這樣作一則耗費空間,二則沒法很好處理區間查詢的問題。

使用標籤-用戶ID這種方式, bitmap存儲數據關係是標籤值等於XXX的用戶ID, 提取核心點bitmap存儲的是等於關係。 那麼bitmap存儲大於或者小於關係也是能夠的。

對於數值型標籤,咱們從新定義存儲關係: bitmap(2) 表示value值大於2的全部用戶ID。 同理, bitmap(5) 表示value值大於5的全部用戶ID。這樣的話,計算value=(3,1000)之間的用戶,使用bitmap(3) andNot bitmap(999)就能夠了。很好地解決了區間查詢的問題。 依然遺留了一個問題: 須要爲每一個值準備一個bitmap。

這個問題的解決思路很巧妙: 多個bitmap組合表示一個數值。例如200, 拆分紅個位,十位,百位3個部分,每一部分用10個bitmap存儲。這樣就可以把bitmap的數量控制在有限的數量裏面。好比對於int整型,最多須要100個bitmap。

優化是沒有止盡的,咱們還能走得更遠。若是數值採用二進制表示,那麼每一位只須要2個bitmap, 一個Int類型最多須要64個bitmap。 採用二進制,存儲的規則能夠以下設置:

bitmap(0)表示該位爲0的用戶ID集合。
bitmap(1)表示該位爲0或1的用戶ID集合。

因爲對於二進制的某一位,取值只有0和1兩種可能,因此對於二進制,每一位只須要bitmap(0), 因此最多須要32+1=33個bitmap存儲。

綜上, 咱們解決bitmap數量的問題,也解決了區間查詢的問題。可是多位二進制組合處理區間查詢,又引出了新的問題: 多個bitmap如何組合表示一個區間?

咱們把問題再簡化一下,多個bitmap如何表示一個小於等於的區間。 好比i<7 如何用bitmap表示? 再回顧bitmap的存儲規則:

bitmap(0)表示該位爲0的用戶ID集合。
bitmap(1)表示該位爲0或1的用戶ID集合。

咱們按從右到左的順序給bitmap位取名字,下標從1開始。 例如01,有兩位,分別是b2,b1。

這樣的話: i<7 = i<0111, 用bitmap表示就是b4。 再舉幾個例子:

i<5 = i < 0101, 用bitmap表示就是 (b4 and b2) or (b4 and b3)

爲了理解這個過程,我本身畫了以下的橫向樹形圖:

1
   1
      0
0
      1
   0  
      0

來觀察這個規律,最後實現的代碼以下:

import lombok.Data;
import org.roaringbitmap.RoaringBitmap;

import java.util.*;

public class RangeBitmapDemo2 {

    @Data
    private static class QueryCond{

        private List<String> base = new ArrayList<>();

        private List<List<String>> lowerRange = new ArrayList();

        public void addBase(String val){
            this.base.add(val);
        }

        public void addLowerRange(String val){
            List<String> list = new ArrayList();
            list.addAll(base);
            list.add(val);
            this.lowerRange.add(list);
        }
    }

    public static void query(int upper, int binaryLength){
        String val =String.format("%"+binaryLength+"s", Integer.toBinaryString(upper)).replaceAll(" ","0"); //這裏能夠補空格
        System.out.println("query: upper is "+val);

        QueryCond cond = new QueryCond();

        char cur = val.charAt(0);

        if(cur=='0'){
            cond.addBase("b"+binaryLength);
        }

        for(int i=1;i<val.length();i++){
            cur = val.charAt(i);
            if(cur == '0'){

                int back = i-1;
                while(back>=0 && val.charAt(back)=='1'){
                    cond.addLowerRange("b"+(binaryLength-back));
                    back -=1;
                }
                cond.addBase("b"+(binaryLength-i));
            }
        }

        System.out.println("query cond: "+cond);

    }

    public static void main(String[] args) {

        for(int i=0;i<32;i++){
            query(i, 6);
        }
    }
}

打印的結果

query: upper is 000110
RangeBitmapDemo2.QueryCond(base=[b6, b5, b4, b1], lowerRange=[[b6, b5, b4, b2], [b6, b5, b4, b3]])

表示000110的組合關係爲(b6 & b5 & b4 & b3 & b2 & b1) or (b6 & b5 & b4 & b2) or (b6 & b5 & b4 & b3) 即整個結果由3個部分組合而成。

相關文章
相關標籤/搜索