IBLT (Invertible Bloom Lookup Table) Tutorial

Copy Markdown View Source
Mix.install([
  {:ex_data_sketch, "~> 0.10"}
],
config: [
    ex_data_sketch: [
      backend: ExDataSketch.Backend.Rust,
      integrations: [opentelemetry: false]
    ]
  ])

Introduction

ExDataSketch.IBLT answers a different question than the other filters here: not "is this item in the set," but "what's different between two sets" -- without transferring either set. Inserting an item touches hash_count cells, each XOR-accumulating the item's key into a running key_sum and bumping a count. Two IBLTs built from mostly-overlapping sets can be subtract/2'd cell-by-cell (also just XOR); every cell where the sets agreed cancels out to zero, and any cell left with count = +-1 holds exactly one surviving key -- which list_entries/1 peels off, repeating until nothing's left to peel:

flowchart LR
    A[item] --> B["hash_count cells"]
    B --> C["count += 1,\nkey_sum ^= key"]
    D[Party A IBLT] --> F["subtract/2\n(XOR cell-by-cell)"]
    E[Party B IBLT] --> F
    F --> G["list_entries/1:\npeel cells with\ncount = +-1"]

This is the classic use case for syncing two replicas, or diffing two nodes' key sets, when most of the data already matches -- exchange just the IBLT (small, proportional to cell_count, not to the set size) instead of either full set.

Use it for: reconciling two mostly-agreeing sets -- replica sync, "what changed since the last checkpoint," diffing two nodes' key sets -- when you have a reasonable estimate of how many items differ, even if you have no idea which ones.

Don't use it for: plain membership testing (not what this answers at all -- see ExDataSketch.Bloom/ExDataSketch.Cuckoo); cases where the actual difference could exceed what :cell_count was sized for -- decode then fails cleanly ({:error, :decode_failed}, never a silently wrong answer) but you get nothing back, so :cell_count needs a real estimate of the expected diff size, not the total set size.

What it buys you: an IBLT's size is proportional to :cell_count (the expected difference), never to the sets themselves. At cell_count: 100 (used throughout this tutorial), each IBLT is a fixed ~2.4 KB -- whether the underlying sets have 200 keys or 200,000:

ApproachData to diff two 200K-key setsAnswer
Transfer both full sets2+ MBExact
ExDataSketch.IBLT (cell_count=100)~2.4 KB, fixedExact diff, if it fits :cell_count

Sample data (cached locally)

Two servers' key sets, 200,000 keys in common, each with a handful of keys the other doesn't have -- realistic for "two replicas that mostly agree, but drifted slightly."

{server_a_keys, server_b_keys, only_in_a, only_in_b} = ExDataSketch.SampleData.iblt_keys()
IO.puts("Server A: #{length(server_a_keys)} keys, Server B: #{length(server_b_keys)} keys")
IO.puts("True diff: #{length(only_in_a)} keys only in A, #{length(only_in_b)} keys only in B")

Basic usage

alias ExDataSketch.IBLT

iblt = IBLT.new() |> IBLT.put("hello")
IBLT.member?(iblt, "hello")

Reconciling two large, mostly-overlapping sets

Both servers build an IBLT sized for the expected diff (a handful of items), not their full 200,000+-key sets:

cell_count = 100

iblt_a = IBLT.new(cell_count: cell_count) |> IBLT.put_many(server_a_keys)
iblt_b = IBLT.new(cell_count: cell_count) |> IBLT.put_many(server_b_keys)

IO.puts("Each IBLT: #{IBLT.size_bytes(iblt_a)} bytes, regardless of the 200,000+ keys inside")

diff = IBLT.subtract(iblt_a, iblt_b)
{:ok, entries} = IBLT.list_entries(diff)

IO.puts("Recovered #{length(entries.positive)} positive and #{length(entries.negative)} negative entries")

list_entries/1 returns {key_hash, value_hash} pairs, not the original strings (an IBLT stores hashes, not the items themselves) -- positive entries are in A but not B, negative are in B but not A. To turn a hash back into a known candidate item, hash your own candidates with the same function IBLT itself uses and match:

positive_hashes = MapSet.new(entries.positive, fn {key_hash, _value_hash} -> key_hash end)
negative_hashes = MapSet.new(entries.negative, fn {key_hash, _value_hash} -> key_hash end)

recovered_only_in_a =
  Enum.filter(only_in_a, fn key ->
    MapSet.member?(positive_hashes, ExDataSketch.Hash.hash64(key, seed: 0))
  end)

recovered_only_in_b =
  Enum.filter(only_in_b, fn key ->
    MapSet.member?(negative_hashes, ExDataSketch.Hash.hash64(key, seed: 0))
  end)

IO.puts("Correctly recovered #{length(recovered_only_in_a)}/#{length(only_in_a)} A-only keys")
IO.puts("Correctly recovered #{length(recovered_only_in_b)}/#{length(only_in_b)} B-only keys")

In a real reconciliation, you already know your own full key set on each side, so "which of my candidates does this hash belong to" is exactly the natural query -- IBLT tells you which of your local keys the other side is missing (or vice versa) without either side ever sending its full set.

What happens when the diff exceeds capacity

Undersize cell_count relative to the actual difference and decoding fails cleanly instead of returning a wrong answer:

undersized_a = IBLT.new(cell_count: 4) |> IBLT.put_many(server_a_keys)
undersized_b = IBLT.new(cell_count: 4) |> IBLT.put_many(server_b_keys)

undersized_diff = IBLT.subtract(undersized_a, undersized_b)
IBLT.list_entries(undersized_diff)

Merging (set mode)

put_many/2 builds from a batch directly; merge/2 combines two already-built IBLTs (both must share the same cell_count/hash_count/ seed) -- useful for the same distributed-worker pattern as the other mergeable sketches:

half = div(length(server_a_keys), 2)
{first_half, second_half} = Enum.split(server_a_keys, half)

worker_a = IBLT.new(cell_count: cell_count) |> IBLT.put_many(first_half)
worker_b = IBLT.new(cell_count: cell_count) |> IBLT.put_many(second_half)

merged = IBLT.merge(worker_a, worker_b)
IO.puts("Merged contains a first-half key: #{IBLT.member?(merged, hd(first_half))}")

Serialization

binary = IBLT.serialize(iblt_a)
{:ok, restored} = IBLT.deserialize(binary)
IO.puts("Round-tripped count: #{IBLT.count(restored)}")

See also

  • ExDataSketch.IBLT module documentation -- full API reference, including key-value mode (put/3, delete/3) for reconciling key-value pairs, not just bare keys.
  • ExDataSketch.Bloom/ExDataSketch.Cuckoo -- if you only need "is this item present," not "what's different," a plain membership filter is cheaper; see livebooks/sketches/bloom.livemd.