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附加了额外的约束来加快检索,要求余数从小到大排序。排序有三个作用

  1. 检索时只需要扫描到≥余数的桶就可以停止扫描;
  2. 在合并QF时可以有序合并;
  3. 扩容时效率更高。扩容时增加商的位数,将余数的高位移至商的低位。余数的有序性保证了顺序扫描时新的商和新的余数都是递增的。

相同商的指纹的余数构成了一个run,由于冲突的存在,一个run的实际存储位置和正式位置(根据商定位到的bucket)会不同。 run 指的是一段商相同的连续记录的余数,如下图 c、d、e 的商均为 3;cluster 指多个连续的 run,并且首个 run 从商对应的位置开始,而后续的 run 并不是从商对应的位置开始的。如图中标出的 cluster,首个 run 从 3 开始,和商相同,第二个 run 从 6 开始,而余数为 4,第三个 run 从 7 开始,而余数为 6。

其中在存储余数的同时,还会在每个哈希桶中增设 3 个元信息位is_occupiedis_continuationis_shiftedis_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表示。

Quotient filter - Wikipedia

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. 第1个循环向左查找,找到cluster的起始位置。cluster起始位置的is-shifted位一定为0.
  2. 第2.1个循环跳过其他的run,找到下一个没有设置is-continuation的下标。
  3. 第2.2个循环找到下一个run,即设置了is-occupied的下标。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
/* Find the start index of the run for fq (given that the run exists). */
static uint64_t find_run_index(struct quotient_filter *qf, uint64_t fq)
{
	/* Find the start of the cluster. */
	uint64_t b = fq;
	while (is_shifted(get_elem(qf, b))) {
		b = decr(qf, b);
	}

	/* Find the start of the run for fq. */
	uint64_t s = b;
	while (b != fq) {
		do {
			s = incr(qf, s);
		} while (is_continuation(get_elem(qf, s)));

		do {
			b = incr(qf, b);
		} while (!is_occupied(get_elem(qf, b)));
	}
	return s;
}

查找

  1. 计算商fq和余数fr,T_fq为fq处存储的值。
  2. 如果is-occupied位没有设置,则一定不存在。
  3. 找到run的起始位置,依次扫描run中的元素,如果余数相同,则可能存在。如果余数大于fr,则一定不存在。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
bool qf_may_contain(struct quotient_filter *qf, uint64_t hash)
{
	uint64_t fq = hash_to_quotient(qf, hash);
	uint64_t fr = hash_to_remainder(qf, hash);
	uint64_t T_fq = get_elem(qf, fq);

	/* If this quotient has no run, give up. */
	if (!is_occupied(T_fq)) {
		return false;
	}

	/* Scan the sorted run for the target remainder. */
	uint64_t s = find_run_index(qf, fq);
	do {
		uint64_t rem = get_remainder(get_elem(qf, s));
		if (rem == fr) {
			return true;
		} else if (rem > fr) {
			return false;
		}
		s = incr(qf, s);
	} while (is_continuation(get_elem(qf, s)));
	return false;
}

插入

元信息变更情况:

  • 桶fq 的is_occupied需置为 1
  • 若元素并不是 run 内的首个元素,则需要将元素 remainder 的is_continuation置为 1,否则将当前 run 开始的is_continuation置为 1(因为添加完成后,当前 run 的开始变成了延续)
  • 向后移动已有 remainder 时is_shifted均会置 1,is_occupied保持在原有的 quotient 处

具体步骤

  1. 计算余数fq,商fr,T_fq为fq处的值。
  2. 第13行为最基本的情况,位置为空,即3个位都没有设置,此时设置is-occupied位,并填充余数就大功告成了。
  3. 19行判断如果没有设置过is-occupied位, 则进行设置。
  4. 26行判断如果已经设置过is-occupied位,则需要找到fr的插入位置,即已有余数中>fr的第一个位置。38行中fr为run的新head时,原有head需要设置is-continuation位,否则44行设置新元素的is-continuation位。
  5. 后面调用insert_into来将entry插入到QF中。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
bool qf_insert(struct quotient_filter *qf, uint64_t hash)
{
	if (qf->qf_entries >= qf->qf_max_size) {
		return false;
	}

	uint64_t fq = hash_to_quotient(qf, hash);
	uint64_t fr = hash_to_remainder(qf, hash);
	uint64_t T_fq = get_elem(qf, fq);
	uint64_t entry = (fr << 3) & ~7;

	/* Special-case filling canonical slots to simplify insert_into(). */
	if (is_empty_element(T_fq)) {
		set_elem(qf, fq, set_occupied(entry));
		++qf->qf_entries;
		return true;
	}

	if (!is_occupied(T_fq)) {
		set_elem(qf, fq, set_occupied(T_fq));
	}

	uint64_t start = find_run_index(qf, fq);
	uint64_t s = start;

	if (is_occupied(T_fq)) {
		/* Move the cursor to the insert position in the fq run. */
		do {
			uint64_t rem = get_remainder(get_elem(qf, s));
			if (rem == fr) {
				return true;
			} else if (rem > fr) {
				break;
			}
			s = incr(qf, s);
		} while (is_continuation(get_elem(qf, s)));

		if (s == start) {
			/* The old start-of-run becomes a continuation. */
			uint64_t old_head = get_elem(qf, start);
			set_elem(qf, start, set_continuation(old_head));
		} else {
			/* The new element becomes a continuation. */
			entry = set_continuation(entry);
		}
	}

	/* Set the shifted bit if we can't use the canonical slot. */
	if (s != fq) {
		entry = set_shifted(entry);
	}

	insert_into(qf, s, entry);
	++qf->qf_entries;
	return true;
}

insert_into需要将elt写到s指定的桶。

  1. 在循环中读取s当前的值prev,如果非空的话,需要设置is-shifted标记,并根据是否is-occupied标记,设置curr的is-occupied位,清理prev的is-occupied位。
  2. 更新s的桶为curr。
  3. 重复1和2直到遇到一个空桶。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/* Insert elt into QF[s], shifting over elements as necessary. */
static void insert_into(struct quotient_filter *qf, uint64_t s, uint64_t elt)
{
	uint64_t prev;
	uint64_t curr = elt;
	bool empty;

	do {
		prev = get_elem(qf, s);
		empty = is_empty_element(prev);
		if (!empty) {
			/* Fix up `is_shifted' and `is_occupied'. */
			prev = set_shifted(prev);
			if (is_occupied(prev)) {
				curr = set_occupied(curr);
				prev = clr_occupied(prev);
			}
		}
		set_elem(qf, s, curr);
		curr = prev;
		s = incr(qf, s);
	} while (!empty);
}

参考资料

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