TGViewer
Daria’s room Daria’s room @dariasroom · 1.2K subscribers
Post #103 1.51K
Ribbon Filter: Part 2 - построение фильтра через elimination и back substitution

В первой части мы разобрались, что Ribbon не хранит элементы напрямую, а кодирует весь набор ключей как систему XOR-уравнений внутри массива cells.

Остается вопрос: как именно мы получаем значения в cells, чтобы все эти уравнения выполнялись?

Сборка фильтра происходит внутри BuildFromKeyStream и делится на 2 этапах:
1. elimination - упрощение XOR уравнений через исключение одинаковых переменных
2. back substitution - вычисление неизвестных значений cells через те, которые стали известными после elimination

Как работает elimination

Ранее разбирали, что поиск в Ribbon реализуется за счет решения XOR уравнения с fingerprint ключа и значениями в cells внутри окна. Fingerprint нам известен на этапе поиска, поэтому при построении фильтра надо определить, что будет в cells

Алгоритм сборки в BuildFromKeyStream:
1. у каждой строки есть ведущая колонка (pivot - первая переменная в уравнении для cells[i])
2. если pivot для неё ещё не занят - строка становится pivot
3. если занят - строка XOR’ится с существующим pivot

Про pivots[i] - когда приходит новая строка с той же ведущей колонкой, она XOR’ится с уже сохранённой строкой и тем самым эта переменная «исключается» из уравнения (elimination). Чтобы вкурить, советую посмотреть комментариями в коде и как это реализовано в цикле elimination.

for cur.mask != 0 {
leadCol := cur.leadingColumn()

if !pivots[leadCol].isSet {
pivots[leadCol].row = cur
pivots[leadCol].isSet = true
break
}

cur = xorRows(cur, pivots[leadCol].row)
}

Тут очередная экономия, так как вместо огромной матрицы Ribbon хранит только опорные уравнения - по одному pivot на колонку.

Как две строки XOR’ятся между собой

Так как mask хранит не глобальные индексы, а позиции внутри локального окна, перед XOR две строки надо сначала выровнять относительно общего пространства

cells[i]:
shift := int(pivot.start) - int(cur.start)
aligned = pivot.mask << shift // или >> если shift отрицательный
cur.mask ^= aligned


Если окна не пересекаются (разница по start слишком большая - в моем случае это +32 ), совпадающих cells нет, и XOR ничего не сокращает.

Затем строка нормализуется: убираются хвостовые нули, а start сдвигается вперёд через TrailingZeros64

tz := bits.TrailingZeros64(cur.mask)
cur.start += uint32(tz)
cur.mask >>= tz


Обратная подстановка (back substitution)

После elimination мы ещё не знаем значения cells, только зависимости между ними в виде уравнений.

Дальше мы последовательно вычисляем их справа налево:
- к моменту вычисления cells[i] все зависимые значения с бОльшими индексами уже посчитаны, поэтому можно выразить cells[i] через них, XOR’нув известные значения с fingerprint
- если для cells[i] pivot нет, значит в XOR нечего класть - значение считается равным 0 (подробнее)

Look up: как работает Contains

После адовой сборки лукап получается удивительно коротким: достаточно найти составляющие XOR и сравнить результат с fingerprint:

for localMask != 0 {
bitPos := bits.TrailingZeros64(localMask)
acc ^= rf.cells[start+uint32(bitPos)]
localMask &= localMask - 1
}


Вывод

Как можно заметить, Ribbon filter переносит всю сложность в этап построения: мы решаем систему XOR-уравнений и затем сохраняем её решение в cells.

За счёт этого lookup превращается в простую и быструю операцию в виде нескольких XOR внутри локального окна.

А чтобы не оставаться в теории, я прогнала все три фильтра на одинаковой конфигурации на 1 млн ключей

По размеру индексов выиграл, правда, Cuckoo (1.2 MB), затем Ribbon (1.8 MB) и Bloom (2.7 MB).

Универсальным вариантом (ровный по перфу и простой в реализации) я бы назвала Bloom.

Если нужен быстрый lookup и поддержка delete - это Cuckoo. Но это оправдано, если не критичны стоимость сборки и дополнительные аллокации памяти.

Ribbon даёт низкий false positive rate, экономнее по памяти и быстрее Bloom на реальных данных. Но эта выгода достигается ценой сложной реализации и, соответственно, поддержки.

Как обычно - пример, готовый к использованию и сама дока

#fts #perf
  • 👍 11
  • 🔥 6
  • ❤ 4
  • 🏆 2
More from @dariasroom
  1. Sep 15, 2026Автоматическое сжатие на клиенте vs ручное на сервере Стандартный клиент http.Transport са…
  2. Sep 5, 2026Причина перекосов в уровне балансировки L4-балансировщик выбирает серверную ноду при созда…
  3. Aug 28, 2026Итераторы… TL;DR: в iter.Seq итератор сам передаёт следующие элементы в код внутри range,…
  4. Aug 17, 2026Что на самом деле нужно сохранять при сериализации сложной структуры? TL;DR: Важно отделит…
  5. Aug 13, 2026Привет! Вас стало больше, так что пора наконец представиться 🙂 Я Даша, давно пишу на Go,…
  6. Aug 12, 2026HNSW: как устроен графовый индекс для векторного поиска One million years later, я наконец…
Threads Profile ViewerView any public Threads profile without an account.Open ThreadLook →Writing with AI? Make it sound human.Metric37 rewrites AI drafts so they read naturally. Free AI detector, 1,500 words free.Try Metric37 →