node
現有的過濾器都僅僅支持point query,例如如今RocksDB裏面有一張學生表,如今要作查詢,找出年齡等於18歲的學術,咱們能夠經過在每個SSTable(LSM Tree的分層結構)上加一個布隆過濾器減小磁盤IO,從而加速查找過程。可是如今查詢請求變成了學生表中是否含有年齡段在22到25之間的學生,這個時候布隆過濾器就沒有辦法工做了。git
本篇論文的核心是提出了一種基於succinct data structure的trie樹,同時對該樹進行合理的編碼,從而下降佔用內存的大小,同時保留查詢能力,既支持point query,也支持range quey。 github
爲了在集合中查找字符串,首先想到的是Trie,一棵不作任何處理的Trie樹以下圖所示數據庫
從起始節點到最底層的葉子節點存儲了一個完整的key,因此它是徹底精確的,在集合中查找某個字符串的時候,不會出現關鍵字是否存在判別錯誤的狀況。可是它有一個缺點就是佔用的內存空間太大。爲了讓這棵Trie樹變小,就要去截斷一部分後綴,只會保存最短的前綴且這個前綴能夠與集合中其餘元素不一樣,這棵Trie樹被稱爲Surf-Base,如圖所示性能
可是Surf-Base有個問題就是若是如今有一個字符串的前綴和樹中存儲的字符串前綴相同,但它又不在給定的字符串集合中,這時判別集合中是否有關鍵字的FPR(False Positive Rate)就會很高,好比經過上圖右部的SuRF-Base去判別集合中SIGMETRIC是否存在,就會認爲該字符串存在於該集合中,就會獲得一種錯誤信息。爲了下降FPR,做者對原來的SuRF-Base結構作了改進,提出了SuRF-Hash、SuRF-Real以及SuRF-Mixed三種結構。測試
SuRF-Hash(SuRF with Hashed Key Suffixes):針對SuRF-Base有很高的FPR,在將集合中的關鍵字加入到SuRF-Base樹的同時,也會對關鍵字進行hash計算,將獲得的hash值的n個bit存儲到最終的value中,當進行關鍵字的查找時,不只要在Trie樹上面查找,還要對比hash值。這種結構有利於Point查詢,且保存的hash值每多一位,作Point quey的FPR就會減小一半。可是這個結構並不會對Range query有任何幫助,不能減小range query的FPR。編碼
SuRF-Real(SuRF with Real Key Suffixes):和SuRF with Hashed Key Suffixes不一樣,SuRF-Real將存儲的hash值的n個bit換成了真實key(即value中存放着key),例如上圖的右部分表示添加了8bit的suffixes,這樣雖然同時加強了Point query和Range query,可是關鍵字的區分度仍是不高,在point查詢下, 它的FPR比SuRF-Hash要高。spa
SuRF-Mixed(SuRF with Mixed Key Suffixes):爲了同時享受Hash和Real兩種方式的優勢, Mixed模式就是將兩種方式混合使用,存儲的value中有一部分是real key,另外一部分是hashed key,混合的比例能夠根據數據分佈進行調節來得到最好的效果。以下圖是一個案例:.net
FAST SUCCINCT TRIES是做者提出來的一種對Trie樹進行編碼的方式,能夠減少該樹在內存中空間,同時保留了查詢的能力。由於這種方式是基於LOUDS(Level-Ordered Unary Degree Sequence)提出來的,因此須要先了解LOUDS的編碼規則:指針
從根節點開始,按廣度優先的方式去遍歷這棵樹。
掃描到一個節點時,該節點有n個孩子,則用n個1和一個0對這個節點進行編碼。
舉例以下圖所示:從根節點開始依次層序遍歷這棵樹,遇到一個節點,該節點有幾個孩子,就用幾個1再加上一個結束標誌0對該節點編碼,例如,對於節點3,它有3個孩子,就用「1110」對該節點編碼。
對這棵樹編碼完成後獲得的是一組01字符串,如今要根據這個字符串來訪問樹中的節點,能夠總結成兩個經典的操做:
經過父親節點找孩子節點
經過孩子節點找父親節點
爲了可以實現上述兩個操做從而實現訪問樹中的任意節點,根據該樹的編碼特色以及字符串的形式,定義了四個操做:
rank1(i) : 返回在 [0, i] 位置區間內 1 的個數
rank0(i) : 返回在 [0, i] 位置區間內 0 的個數
select1(i) : 返回第i個1的位置(整個bit序列)
select0(i) : 返回第i個0的位置(整個bit序列)
上面的操做能夠經過下面的表格來具體詳細解釋,其中value行的比特序列是上面那張圖中的編碼序列:
如今基於下面三個公式來訪問整個樹:
求層序遍歷的第i個節點在比特序列中的位置
position(i-th node) = select0(i) +1 //由於節點間編碼以0間隔開,因此當前序列位置前面有多少0,就表示有多少節點,第i個節點的位置,前面有i個節點(節點序號從0開始),及定位到第i個0,就能夠定位到第i-1個節點編碼序列最後一個比特在比特序列的位置,加1後就表示第i個節點的起始位置了。便可知,對任何一個位置來講,開始位置到該點之間的bit 0出現的個數表示該點前面有多少個節點。
求在比特序列中起始位置爲p的節點的孩子位置
first-child(i) = select0(rank1(p)) + 1 //由於每個節點都會經過1的個數去標記其直接孩子的個數,根據這個特性,對任何一個位置,開始位置到該點之間的bit 1出現的個數表示該點前面的節點加上其直接孩子的節點數目。
求在比特序列中起始位置爲p的節點的父親位置
parent(i) = select1(rank0(p)) //這個關係能夠根據父親求孩子來倒推。先求出該節點前面有多少節點,而後根據「對任何一個位置,開始位置到該點之間的bit 1出現的個數表示該點前面的節點加上其直接孩子的節點數目」。倒推父親節點。
舉例以下:
求第4個節點在編碼序列中的位置:select0(4)+1 = 11 + 1 =12
求在比特序列中起始位置爲12的孩子位置:select0(rank1(12)) = select0(9)+1 = 22
求在比特序列中起始位置爲22的節點的父親位置:select1(rank0(22)) = select1(9) = 12
基於LOUDS編碼方式, FST( FAST SUCCINCT TRIES)對LOUDS進行了進一步壓縮, 下圖介紹了基本的壓縮方法:
FST將LOUDS分紅了兩層, 上層節點數量少,數據訪問頻繁, 使用LOUDS-Dense編碼方式, 下層節點數多, 數據訪問次數少,使用LOUDS-Sparse編碼方式.
1. LOUDS-Dense
每一個節點最多有256個子節點, 那麼在LOUDS-Dense編碼方式中, 每一個節點使用3個256個bit的bit map和一個bit序列來保存信息. 它們分別是:
D-Labels : 爲節點中的每個值作一個分支標記。例如根節點有以 f,s 和 t做爲前綴的三個分支,那麼會將這個大小爲256的bit map的第 102(f),115(s) 和 116 (t)bit 位就會設置爲 1。能夠看到,具體哪個bit 位,就是 ASCII 碼的值。
D-HasChild : 標記對應的子節點是不是葉子節點仍是中間節點。以根節點的三個分支爲例,f 和 t 都有子節點,而 s 沒有,因此 102 和 116 bit 都會設置爲 1。
D-IsPrefixKey : 標記當前前綴是不是有效的key。
D-Values : 存儲的是固定大小的 value,在本文中,表示的是指向以前說過三種後綴(hashed, Real, Mixed)的指針。
如今仍然可使用select&rank操做來訪問Trie樹中LOUDS-Dense對應的節點:
求孩子節點:假設某一結點的label分支有節點,即對應的D-HasChild[pos] = 1,則對應的分支的孩子節點的位置是 D-ChildNodePos(pos)=256×rank1(D-HasChild,pos)
舉例:求根節點的中D-Label爲t的孩子節點(D-HasChild(pos)=1)分支,Position(t) = 116,則:
D-ChildNodePos(256)=256×rank1(D-HasChild,pos) = 256 * 2= 512 //第三個節點的起始位置爲512。
注:operator(seq,pos) 表示在序列seq上作operator操做,上式就是在D-HasChild中作rank1(pos)操做
求父親節點:假設求pos=623(第三個節)的父親位置:
D-ParentNodePos(pos) = select1(D-HasChild, ⌊pos/256⌋)
帶入公式得D-ParentNodePos(pos) = select1(D-HasChild,⌊623/256⌋) = select1(D-HasChild, 2) = 116
2. LOUDS-Sparse
使用3個bit序列來對trie樹進行編碼, 在整個bit序列中, 每一個節點的長度相同, 這三個bit序列分別是:
S-Labels(bit-sequences) : 直接存儲節點中的值,按照 level order 的方式記錄了全部 node 的 label,用0xFF($)標記該前綴也是key節點(做用至關於LOUDS-Dense中的D-IsPrefixKey )。
S-HasChild(one bit) : 記錄每一個節點中的label是否含有分支子節點, 有的話標記爲1, 每一個label使用一個bit。
S-LOUDS(one bit) : 記錄每一個label是不是該節點的第一個label。譬如上圖第三層,r,p 和 i 都是本節點的第一個label,那麼對應的 S-LOUDS 就設置爲 1 了。
S-Values : 存儲的是固定大小的 value,在本文中,表示的是指向以前說過三種後綴(hashed, Real, Mixed)的指針。
使用select&rank操做來訪問Trie樹中LOUDS-Sparse對應的節點:
求孩子節點:假設某一結點的label分支有節點,即對應的S-HasChild[pos] = 1,則對應label分支的孩子節點的位置是:S-ChildNodePos(pos) = select1(S-LOUDS,rank1(S-HasChild,pos) + 1)
例如,S-HasChild[5]=1,rank1(S-HasChild, pos) = 2 + 5 =7(這裏要加上LOUDS-Dense上的D-HasChild),select1(S-LOUDS, 7 + 1) = 9(S-LOUDS主要表明節點的label邊界,須要減去LOUDS-Dense上的3個節點,實際上求的是select1(S-LOUDS, 8-3))
求父親節點:假設求pos=623(第三個節)的父親位置:
S-ParentNodePos(pos) = select1(S-HasChild, rank1(S-LOUDS, pos) -1);
例如,如今求pos = 9的父節點,rank1(S-LOUDS, pos) = 8( rank1(S-LOUDS, pos) = 5 可是加上LOUDS-Dense上的3個節點)select1(S-HasChild, 7) = 6 (S-HasChild還包括了LOUDS-Dense上的D-HasChild)
假設這棵Trie樹有H層,LOUDS-Dense-Size(l) 表示從0到l(不包含l)層採用LOUDS-Dense編碼,而LOUDS-Sparse-Size(l) 表示從l到H層採用LOUDS-Sparse方式編碼,這棵樹按多少比例採用兩種方式去編碼:
LOUDS-Dense-Size(l) × R ≤ LOUDS-Sparse-Size(l) //一般R默認值是64
因而,LOUDS-Sparse方式的編碼大小會決定這棵Trie樹的實際編碼空間大小。如今給定n個個關鍵字的集合,S-labes須要使用8n個bits, S-HasChild和S-LOUDS一共使用2n個bits, 因此LOUDS-Sparse使用10n個bits。而Dense佔用的空間要遠遠小於Sparse部分,因此整個LOUDS-DS編碼的Trie樹接近10n個bits。
論文中使用了兩組key的數據進行性能對比測試。 一組是由YCSB輸出的64bit的整數, 另外一組是由字符串組成的電子郵件地址,,其中整數的key有50M個,電子郵件地址組成的key有25M個。相關細節以下:
1. FPR對比
首先對比了SuRF不一樣模式和布隆過濾器在FPR上的對比:
通常狀況,在point query下,SuRF比bloom filter仍是要差一些。從該圖的中間部分能夠看出,隨着SuRF-Hash的hash後綴的bit位數的增長,它對range query起不到任何做用。該圖的右側的Mixed query說明,隨着後綴的長度的增長,SuRF-Real對Point和Range query均可以加強做用,因此它降低的最快,而 SuRF-Hash只對Point query起做用,因此它的後綴增長到必定後,只是將Point query的FPR下降了,可是Range query的FPR不會變化,而在整數和Email的實驗中SuRF-Real和SuRF-Mixed的變化趨勢不一樣,是由於在整數中後綴添加一個bit,這個 值變化很大,區分度高,可是相對於字符串,特別是郵箱,後綴添加一個bit,即使是一個字節,區分度可能不高(好比ttttttx@cs.cmu.ed和tttttts@cs.cmu.ed)
2. 性能對比
SuRF的不一樣模式和bloom filter的吞吐對比,吞吐實際上指的是查詢速度。
能夠看出不管是Point,Range,仍是Mixed Query下,SuRF的三種模式吞吐量差異不大,並且在作Point Query時,布隆過濾器的吞吐量仍是相對高的。
應用場景測試
做者對RocksDB的過濾器作了些改動,提出了四種場景的RocksDB的測試案例
(1)no filter
(2)Bloom filter (14 bits per key)
(3)SuRF-Hash (4 bit suffix per key)
(4)SuRF-Real (4 bit suffix per key)
實驗的數據集是100G,查詢的key是隨機產生的。做者首先作的是性能對比
上圖左側表示的是Point query的性能對比,能夠看出添加布隆過濾器後,查詢時涉及的磁盤IO最少,它的吞吐量最大;圖中右側表示Range query的性能對比,此時SuRF的兩種變體就有一些性能上的提高。
接下來做者爲了更大程度的顯示SuRF的優點,因而作一組關於在range query時,故意設置一些查詢語句返回爲空時的性能對比試驗。並逐漸增長這些查詢語句在全部查詢語句的比例。
從圖中能夠看出隨着查詢中查詢結果爲空的比例不斷增多,SuRF的性能就會不斷的提高,而帶布隆過濾器的SuRF的性能始終沒有任何變化。
文中的SuRF是一種即支持Point query又支持Range query的過濾器結構
若是具體應用中針對Point query的FPR的要求很高,布隆過濾器則比SuRF更好。可是若是查詢中出現empty result的狀況不少的話,且關注性能的提高時,可使用SuRF結構。
能夠調節SuRF-Mixed中後綴部分hashed key和Real key的各自的長度,通常都是從SuRF-Real這種模式開始作調整,由於這種模式能夠對Point query和Range query都很好,而後慢慢的逐步將Real 換成Hashed Suffixes。
SuRF是常駐內存的,並且很高效,它的FPR能夠經過調整後綴的長度來下降FPR。
製做的PPT在github上:地址
HuanchenZhang. 2018. Sigmod. SuRF : Practical Range Query Filtering with Fast Succinct Tries
Guy Jacobson. 1989. Space-efficient static trees and graphs. In Foundations of Computer Science.IEEE,549–554.