quotient filter(QF)不存储原始的key,而是存储原始key的指纹,因此在指纹冲突时,无法区分key是否真的存在,此时也会false positive。论文中硬冲突定义为指纹冲突,软冲突定义为指纹不同但是商相同。
相比bloom filter的优势
- cache友好。
- 支持扩容。
- 支持合并。
- 支持删除。
原理
quotient filter底层实现使用了hash table。Quotienting将指纹分为商和余数两部分,商为指纹的高q位,余数为指纹的低r位。商用于定位到hash表的bucket,而余数则作为hash表的值存储起来。注意,通过将商和余数结合起来能够重建hash,而不需要使用原始的key重新哈希,这也是quotient filter支持扩容和合并等操作的关键所在。
在hash表冲突(即不同key的指纹的商相同)时,使用线性探测法来存储。使用线性推测法时,相同商的指纹的余数部分连续存储。quotient附加了额外的约束来加快检索,要求余数从小到大排序。排序有三个作用
- 检索时只需要扫描到≥余数的桶就可以停止扫描;
- 在合并QF时可以有序合并;
- 扩容时效率更高。扩容时增加商的位数,将余数的高位移至商的低位。余数的有序性保证了顺序扫描时新的商和新的余数都是递增的。
相同商的指纹的余数构成了一个run,由于冲突的存在,一个run的实际存储位置和正式位置(根据商定位到的bucket)会不同。 run 指的是一段商相同的连续记录的余数,如下图 c、d、e 的商均为 3;cluster 指多个连续的 run,并且首个 run 从商对应的位置开始,而后续的 run 并不是从商对应的位置开始的。如图中标出的 cluster,首个 run 从 3 开始,和商相同,第二个 run 从 6 开始,而余数为 4,第三个 run 从 7 开始,而余数为 6。
其中在存储余数的同时,还会在每个哈希桶中增设 3 个元信息位is_occupied
、is_continuation
和is_shifted
。is_occupied
表示过滤器中是否有和余数对应的商,其与下标所对应的商相同,如 c、d、e 的商均为 3,因此下标为 3 的哈希桶is_occupied
位是 1。is_continuation
表示当前哈希桶中的余数,是否和左边相邻的元素属于一个 run,如下标 4 的哈希桶中余数,与下标 3 的哈希桶中余数属于一个 run,因此is_continuation
为 1。is_shifted
表示余数存储的位置是否和余数对应的商表示的下标有偏移,如 c 存储的位置与 c 对应的商表示的下标均为 3,因此is_shifted
为 0;而 f 存储的位置为 6,f 对应的商表示的下标为 4,因此is_shifted
为 1。
上图给出了一个示例,分别是等价的使用开链和使用线性探测结合元信息bit位的hash表示。
is_occupied | is_continuation | is_shifted | meaning |
---|---|---|---|
0 | 0 | 0 | Empty Slot |
0 | 0 | 1 | Slot is holding start of run that has been shifted from its canonical slot. |
0 | 1 | 0 | not used. |
0 | 1 | 1 | Slot is holding continuation of run that has been shifted from its canonical slot. |
1 | 0 | 0 | Slot is holding start of run that is in its canonical slot.This is also the start of the cluster. |
1 | 0 | 1 | Slot is holding start of run that has been shifted from its canonical slot.Also the run for which this is the canonical slot exists but is shifted right. |
1 | 1 | 0 | not used. |
1 | 1 | 1 | Slot is holding continuation of run that has been shifted from its canonical slot.Also the run for which this is the canonical slot exists but is shifted right. |
实现
下文中的代码均来自 https://github.com/vedantk/quotient-filter 。
定位run的起始下标
fq为要查找的商。由于前面的商可能挤占了fq的位置,fq不一定在正式的位置。
- 第1个循环向左查找,找到cluster的起始位置。cluster起始位置的is-shifted位一定为0.
- 第2.1个循环跳过其他的run,找到下一个没有设置is-continuation的下标。
- 第2.2个循环找到下一个run,即设置了is-occupied的下标。
|
|
查找
- 计算商fq和余数fr,T_fq为fq处存储的值。
- 如果is-occupied位没有设置,则一定不存在。
- 找到run的起始位置,依次扫描run中的元素,如果余数相同,则可能存在。如果余数大于fr,则一定不存在。
|
|
插入
元信息变更情况:
- 桶fq 的
is_occupied
需置为 1 - 若元素并不是 run 内的首个元素,则需要将元素 remainder 的
is_continuation
置为 1,否则将当前 run 开始的is_continuation
置为 1(因为添加完成后,当前 run 的开始变成了延续) - 向后移动已有 remainder 时
is_shifted
均会置 1,is_occupied
保持在原有的 quotient 处
具体步骤
- 计算余数fq,商fr,T_fq为fq处的值。
- 第13行为最基本的情况,位置为空,即3个位都没有设置,此时设置is-occupied位,并填充余数就大功告成了。
- 19行判断如果没有设置过is-occupied位, 则进行设置。
- 26行判断如果已经设置过is-occupied位,则需要找到fr的插入位置,即已有余数中>fr的第一个位置。38行中fr为run的新head时,原有head需要设置is-continuation位,否则44行设置新元素的is-continuation位。
- 后面调用insert_into来将entry插入到QF中。
|
|
insert_into需要将elt写到s指定的桶。
- 在循环中读取s当前的值prev,如果非空的话,需要设置is-shifted标记,并根据是否is-occupied标记,设置curr的is-occupied位,清理prev的is-occupied位。
- 更新s的桶为curr。
- 重复1和2直到遇到一个空桶。
|
|
参考资料
Quotient Filter 各类操作分析 - 知乎 (zhihu.com)
http://dept.cs.williams.edu/~jannen/teaching/s20/cs333/meetings/Filters-slides.pdf
https://en.wikipedia.org/wiki/Quotient_filter
https://www.gakhov.com/articles/quotient-filters.html
https://systemdesign.one/quotient-filter-explained/#variants-of-quotientfilter