Releases: thanos/ex_data_sketch
Release list
v0.9.0 - Streaming Integrations
ExDataSketch v0.9.0 Release Notes
Release date: 2026-05-20
Theme: Streaming Integrations -- the BEAM-native streaming approximate analytics infrastructure layer.
Highlights
Stream and Collectable Integration
All 13 mergeable sketch types now implement the Collectable protocol and support lazy stream consumption via ExDataSketch.Stream:
# Lazy stream consumption
sketch = 1..100_000 |> Stream.map(&to_string/1) |> ExDataSketch.Stream.hll(p: 14)
# Collectable protocol
sketch = Enum.into(1..50_000, ExDataSketch.ULL.new(p: 14))
# Partitioned parallel reduction
sketch = ExDataSketch.Stream.reduce_partitioned(1..1_000_000, ExDataSketch.HLL, partitions: 8, p: 14)Broadway, GenStage, and Flow
Production-grade integration for streaming pipelines:
ExDataSketch.Broadway.accumulate/3-- build sketches from Broadway message batchesExDataSketch.Broadway.PeriodicAggregator-- periodic accumulation with timer flushExDataSketch.GenStage.SketchConsumer-- back-pressure-aware sketch accumulationExDataSketch.GenStage.SketchProducer-- emit accumulated sketches on demandExDataSketch.Flow.reduce/3andmerge/2-- parallel partition-local reduction
Five Persistence Backends
Save, load, merge, and delete sketches with atomic operations:
# ETS (in-memory)
ExDataSketch.Storage.ETS.save(sketch, table, "daily:2024-01-15")
{:ok, loaded} = ExDataSketch.Storage.ETS.load(ExDataSketch.HLL, table, "daily:2024-01-15")
ExDataSketch.Storage.ETS.merge(partial, table, "daily:2024-01-15") # atomic merge
# Also: DETS, CubDB, Mnesia, EctoProduction-Grade Telemetry
Structured :telemetry events at batch boundaries with category-based enable/disable:
:telemetry.attach("my-handler", [:ex_data_sketch, :sketch, :ingest], fn _name, meas, meta, _ ->
Logger.info("#{meta.sketch_type}: #{meas.count} items in #{meas.duration}ns")
end, nil)
# OpenTelemetry bridge (optional)
ExDataSketch.Telemetry.OpenTelemetry.setup()ULL Accuracy Fix
UltraLogLog estimates with few registers now use linear counting correction (when zeros > 0) and large-range bias correction, improving accuracy from 62.5% error to 0.8% at p=8/n=1000.
v1 Serialization Escape Hatch
# Produce backward-compatible v0.7.x binary (requires :phash2 hash strategy)
binary = ExDataSketch.HLL.serialize(sketch, format: :v1)Nine Production-Oriented Livebooks
From streaming cardinality to AI token analytics. See guides/livebooks.md for the recommended reading order.
Breaking Changes
None. All v0.8.0 APIs are fully backward compatible.
ULL estimate change: ULL estimates at very low cardinalities (p < 12, n < 500) may differ from v0.8.0. The v0.9.0 estimates are more accurate. Add tolerance for small cardinalities in tests.
New Dependencies
:telemetry ~> 1.0(required, was already a transitive dependency):broadway,:flow,:cubdb,:ecto_sql,:opentelemetry_api(optional, only loaded when available)
Full Changelog
See CHANGELOG.md for the complete list of changes.
Upgrade Guide
From v0.8.0: No code changes required. Add {:ex_data_sketch, "~> 0.9.0"} to your dependencies.
New features are opt-in:
- Telemetry is enabled by default; disable with
config :ex_data_sketch, telemetry_enabled: false - Persistence backends are enabled when their runtime dependency is available
- Stream/Broadway/GenStage/Flow modules are available when their dependencies are loaded
v0.8.0 — Deterministic Foundations
ex_data_sketch v0.8.0 — Deterministic Foundations
Release date: 2026-05-12
v0.8.0 transforms ex_data_sketch from a collection of probabilistic algorithms into a production-grade probabilistic runtime for the BEAM. Zero new sketch families. Five phases of substrate investment.
Highlights
Deterministic Hashing (Phase 1)
Every sketch now shares a documented, validated, byte-stable hash layer. Hash.XXH3, Hash.Murmur3, Hash.Metadata, and Hash.Validation are new. HLL.new/1, ULL.new/1, Theta.new/1, and CMS.new/1 now honor a user-supplied :hash_strategy (:xxhash3 | :murmur3 | :phash2). The :murmur3 option was silently overridden in v0.7.x — this is now a behavior change: code that passed hash_strategy: :murmur3 will produce different estimates than before (correct ones, using Murmur3).
Binary Stability & Corruption Detection (Phase 2)
EXSK serialization bumps from v1 to v2. Every serialize/1 output now carries a CRC32C checksum and an embedded Hash.Metadata block. deserialize/1 transparently reads both v1 and v2. A 200-mutation bit-flip fuzz suite verifies that no single-bit corruption silently propagates. v0.7.x readers cannot read v2 frames.
Hot-Path Performance (Phase 3)
8 new Rust NIFs (_raw_h_nif family) dispatch hashing by algorithm byte, extending in-Rust hashing to Murmur3. XXH3 throughput remains 25–34 M items/sec at p=14; Murmur3 is within 8%. Legacy _raw_nif family preserved for v0.7.x binary stability.
Precompiled NIFs (Phase 4)
Windows x86_64 and ARM64 MSVC targets added. The matrix is now 8 targets x 2 NIF versions = 16 artifacts per release. mix test.nif_on / mix test.nif_off aliases handle local NIF-mode flips.
Property-Based Validation (Phase 5)
14 new StreamData properties lock monotonicity (HLL/ULL), error bounds, rank consistency (KLL/REQ), overestimation-only (CMS), and no-false-negative guarantees (Bloom/XorFilter/Cuckoo). Binary v2 corruption never silently produces a valid sketch.
Post-Release Fixes (since tag)
- #240 — EXSK v2
Header.decode/1now rejects non-zero reservedflagswith a structuredDeserializationError, matching the documented v2 contract. Previously, frames withflags != 0were silently accepted. - #238 —
serialize/1on any Murmur3-configured sketch no longer crashes withCaseClauseError. The sketch-localhash_strategy_byte/1encoders across HLL, ULL, Theta, and CMS now map:murmur3to wire byte3. - #239 — Release docs and the checksum file are no longer git-tracked; plan docs are excluded from the Hex package.
- Credo lint: replaced chained
Enum.map |> Enum.mapwith single passes, replacedlist ++ [item]with prepend-and-reverse, replacedList.foldlwithEnum.reduce, replacedList.lastwith pattern matching, removed fully-qualified module references, removed restatement comments, documented previously@doc falsepublic helpers.
Wire Format
| Sketch (empty) | v1 size | v2 size | Overhead |
|---|---|---|---|
| HLL p=4 | 18 B | 50 B | +32 (2.8x) |
| HLL p=14 | 16,398 B | 16,430 B | +32 (0.2%) |
| KLL k=200 (populated) | ~3-5 KB | ~3-5 KB | +32 (~1%) |
For any sketch larger than ~1 KB, overhead is negligible.
Performance
| Path (HLL p=14) | Throughput |
|---|---|
| Pure phash2 | ~1.7 M items/sec |
| Pure xxhash3 | ~1.9 M items/sec |
| Rust raw XXH3 | ~30 M items/sec |
| Rust raw_h Murmur3 | ~28 M items/sec |
Test Suite
| Metric | v0.7.1 | v0.8.0 |
|---|---|---|
| Tests (NIF on) | 1,186 | 1,317 |
| Doctests | 169 | 202 |
| Properties (NIF on) | 152 | 171 |
| Line coverage | 88% | 92.7% |
| Credo issues | 0 | 0 |
Breaking Changes
- EXSK v2 is one-way. v0.7.x readers cannot decode v2 frames. Stage your rollout: deploy v0.8.0 readers first, then producers.
:murmur3is no longer silently overridden. Code that passedhash_strategy: :murmur3(and got:xxhash3in v0.7.x) will now actually use Murmur3. Estimates are still correct but differ from v0.7.x output for the same input.- Serialized binary format changes. The version byte shifts from
1to2. Tests asserting<<"EXSK", 1, ...>>must update to2.
Migration
Most users need no code changes. Full guide: guides/v0.8.0_migration_notes.md (shipped in HexDocs).
# mix.exs
{:ex_data_sketch, "~> 0.8.0"}For deployments that share persisted sketches across nodes:
- Deploy v0.8.0 to all readers first. v0.8.0 reads both v1 and v2.
- Verify reader stability for one deploy cycle.
- Deploy v0.8.0 to producers. They now emit v2 frames.
Known Issues
- ULL accuracy at low p. Use
p >= 12for production. Atp < 12and high cardinality, estimates diverge significantly. - HLL memory at high cardinality. Streaming 10M+ items into a single HLL allocates ~1.86 GB of transient BEAM state. Workaround: smaller chunk sizes.
- Windows ARM64 precompiled NIF has limited CI history. Fallback:
EX_DATA_SKETCH_BUILD=1 mix deps.compile. - Backend.default/0 returns
Pureeven when the Rust NIF is loaded. Opt in explicitly:backend: ExDataSketch.Backend.Rust.
Non-Goals for v0.8.0
No new sketch families. No Apache DataSketches interop beyond Theta CompactSketch. No streaming integrations (Broadway/Flow). No persistence layers (ETS/DETS/CubDB). No telemetry. No SIMD. No 6-bit register packing. No raw-NIF path for membership filters.
Full design docs: plans/0.8.0_architectural_summary.md, plans/0.8.0-risks.md, plans/0.8.0_migration_notes.md, plans/0.8.0_serialization_compatibility.md.
v0.7.1
ExDataSketch v0.7.1 Release Notes
Release date: 2026-03-23
Summary
v0.7.1 is a performance and correctness release that moves hash computation
into Rust NIF batch calls, adds configurable hash functions and seeds, fixes a
quotient filter wrap-around bug, and adds merge-time hash compatibility
validation across all hash-based sketches.
What's new in v0.7.1
NIF batch hashing (94.6% memory reduction)
The update_many path for HLL, ULL, Theta, and CMS previously hashed every
item in Elixir before sending packed hashes to the NIF. For 10M items this
created ~30M+ transient heap allocations (term_to_binary + xxhash3 NIF
bigint + binary encoding per item).
v0.7.1 sends raw item binaries directly to Rust via ListIterator for
zero-copy Erlang list iteration. Rust decodes items, hashes each with xxhash3
in-loop, and updates registers -- zero per-item Elixir heap allocation beyond
the initial list.
| Items | Before (memory) | After (memory) | Reduction |
|---|---|---|---|
| 100K | 28 MB | 1.5 MB | 94.6% |
| 1M | 281 MB | 15 MB | 94.7% |
| 10M | 2.75 GB | 148 MB | 94.6% |
The existing hash-based NIF path is preserved for custom :hash_fn users and
backward compatibility.
Custom hash functions and seeds (#198)
All four hash-based sketches (HLL, ULL, Theta, CMS) now accept :hash_fn and
:seed options:
# Custom seed for reproducible hashing
hll = ExDataSketch.HLL.new(seed: 42)
# Custom hash function (disables NIF batch path)
hll = ExDataSketch.HLL.new(hash_fn: fn item -> MyHash.hash(item) end)Seed values are propagated through serialization/deserialization and validated
at merge time.
Merge hash-compatibility validation (#205)
merge/2 on HLL, ULL, Theta, and CMS now validates that both sketches use the
same hash strategy and seed. Merging sketches with different hashing
configurations raises IncompatibleSketchesError instead of producing silently
corrupted results:
a = ExDataSketch.HLL.new(seed: 1)
b = ExDataSketch.HLL.new(seed: 2)
ExDataSketch.HLL.merge(a, b)
# ** (ExDataSketch.Errors.IncompatibleSketchesError) HLL seed mismatch: 1 vs 2Merging sketches with custom :hash_fn is explicitly rejected since hash
function compatibility cannot be verified at runtime.
Quotient filter wrap-around fix (#203, #204)
Both Pure and Rust backends had a bug in extract_all where clusters wrapping
from slot N-1 to slot 0 caused nil quotients (Pure crash) or silent data
corruption (Rust). The fix correctly tracks the current quotient across array
boundary wrap-around.
Pure backend optimizations
- HLL
update_many: Pre-aggregate map with sorted binary splice replaces
tuple-based per-hash full-tuple copies, reducing transient allocation from
O(n * m) to O(n + m). - Batch path restoration: HLL, ULL, and Theta Pure backend
update_many
restored to use chunk + batch*_update_manyinstead of per-item*_update.
Test infrastructure
- 39 new tests covering deserialization edge cases, custom
hash_fnpaths,
Rust-only helper functions, and merge hash-compatibility validation. - Configurable coverage baselines via
EX_DATA_SKETCH_COVERAGE_BASELINEenv
var for separate pure-only and Rust CI coverage thresholds. - Coverage reporting added to
test-rustCI job. - Hash-dependent vector tests tagged with
@tag :rust_niffor correct
pure-only CI behavior.
Closed Issues
- #198 -- Wire
:hash_fnand:seedoptions through HLL, ULL, Theta, CMS - #202 -- Move hashing into NIF batch calls via ListIterator
- #203 -- Quotient filter Pure backend wrap-around bug
- #204 -- Quotient filter Rust backend wrap-around bug
- #205 -- Merge hash-compatibility validation
Installation
def deps do
[
{:ex_data_sketch, "~> 0.7.1"}
]
endPrecompiled Rust NIF binaries are downloaded automatically on supported
platforms (macOS ARM64/x86_64, Linux x86_64/aarch64 glibc/musl). No Rust
toolchain required. The library works in pure Elixir mode on all other
platforms.
Upgrade Notes
- No breaking changes from v0.7.0.
- EXSK binaries produced by earlier v0.x releases remain fully compatible.
- Sketches serialized without a seed default to seed 0 on deserialization,
preserving backward compatibility. - The NIF batch hashing path is used automatically when no custom
:hash_fn
is set. Custom hash function users see no behavior change. - CMS
merge/2now validates width, depth, and counter_width explicitly
instead of comparing full option keyword lists.
What's Next
v0.8.0 directions under consideration include Binary Fuse Filters for even
smaller static membership filters, Ribbon Filters for space-optimal static
filters, and expanded Apache DataSketches interop for cross-language sketch
exchange.
Links
v0.7.0
v0.6.0
ExDataSketch v0.6.0 Release Notes
Release date: 2026-03-12
Summary
v0.6.0 adds two new sketch algorithms (REQ and Misra-Gries), integrates
XXHash3 as an opt-in hash function via Rust NIF, and delivers Rust NIF
acceleration for all six membership filters (Bloom, Cuckoo, Quotient, CQF,
XorFilter, IBLT). Every membership filter now has byte-identical parity between
the Pure Elixir and Rust NIF backends, verified by deterministic parity tests.
ExDataSketch now covers 16 sketch types across eight categories:
| Category | Algorithms |
|---|---|
| Cardinality | HyperLogLog (HLL) |
| Frequency | Count-Min Sketch (CMS) |
| Set operations | Theta Sketch |
| Quantiles | KLL, DDSketch, REQ |
| Frequency ranking | FrequentItems (SpaceSaving), Misra-Gries |
| Membership | Bloom, Cuckoo, Quotient, CQF, XorFilter |
| Set reconciliation | IBLT |
| Composition | FilterChain |
What's new in v0.6.0
REQ Sketch (ExDataSketch.REQ)
Relative-error quantile sketch with configurable accuracy bias. REQ provides
rank-proportional error: the relative error at rank r is bounded by
alpha * r (HRA mode) or alpha * (1-r) (LRA mode), making it ideal for
high-percentile monitoring where tail accuracy matters most.
Key features:
- High-rank accuracy (HRA) and low-rank accuracy (LRA) modes
- Configurable k parameter (accuracy vs memory tradeoff)
quantile/2,quantiles/2,rank/2queries- Merge support for distributed aggregation
- REQ1 binary state format
- EXSK serialization (sketch ID 13)
Quick start:
req = ExDataSketch.REQ.new(k: 12, hra: true)
req = ExDataSketch.REQ.update_many(req, 1..100_000)
ExDataSketch.REQ.quantile(req, 0.99) # 99th percentile with high accuracy
ExDataSketch.REQ.quantile(req, 0.999) # 99.9th percentile
ExDataSketch.REQ.rank(req, 50_000.0) # normalized rank of a valueMisra-Gries Sketch (ExDataSketch.MisraGries)
Deterministic heavy-hitter detection with guaranteed frequency thresholds.
Unlike the probabilistic FrequentItems (SpaceSaving), Misra-Gries provides
a hard guarantee: any item with true frequency exceeding n/k is tracked.
Key features:
- Deterministic guarantee: items above n/k frequency are always tracked
- Configurable key encoding:
:binary,:int,{:term, :external} estimate/2for per-item frequency lower boundstop_k/2for ranked heavy hittersfrequent/2for threshold-based filtering- Merge support
- MG01 binary state format
- EXSK serialization (sketch ID 14)
Quick start:
mg = ExDataSketch.MisraGries.new(k: 10)
mg = Enum.reduce(1..1000, mg, fn _, s -> ExDataSketch.MisraGries.update(s, "hot") end)
mg = Enum.reduce(1..100, mg, fn i, s -> ExDataSketch.MisraGries.update(s, "cold_#{i}") end)
ExDataSketch.MisraGries.top_k(mg, 5) # [{"hot", 1000}, ...]
ExDataSketch.MisraGries.estimate(mg, "hot") # 1000XXHash3 Integration (ExDataSketch.Hash)
XXHash3 is now available as an opt-in hash function, providing faster hashing
with better distribution than the default phash2-based hash. When the Rust NIF
is available, XXHash3 output is stable across platforms and OTP versions.
Key features:
xxhash3_64/1andxxhash3_64/2(with seed)- Rust NIF implementation for speed
- Automatic phash2-based fallback when NIF is unavailable
- Seeds are masked to u64 range for safe NIF interop
- Opt-in for backwards compatibility with existing serialized data
Quick start:
# Use XXHash3 as hash function for any sketch
hll = ExDataSketch.HLL.new(p: 14, hash_fn: &ExDataSketch.Hash.xxhash3_64/1)
hll = ExDataSketch.HLL.update_many(hll, ["alice", "bob", "carol"])
ExDataSketch.HLL.estimate(hll)KLL cdf/pmf and DDSketch rank
ExDataSketch.KLL.cdf/2-- cumulative distribution function at split pointsExDataSketch.KLL.pmf/2-- probability mass function at split pointsExDataSketch.DDSketch.rank/2-- normalized rank of a value
Quantiles Facade (ExDataSketch.Quantiles)
Unified API for quantile sketches. Write algorithm-agnostic code that works
with either KLL or DDSketch:
sketch = ExDataSketch.Quantiles.new(type: :kll)
sketch = ExDataSketch.Quantiles.update_many(sketch, 1..10_000)
ExDataSketch.Quantiles.quantile(sketch, 0.5)
ExDataSketch.Quantiles.count(sketch)Rust NIF Acceleration for Membership Filters
All six membership filters now have Rust NIF acceleration for batch operations.
The NIF backend is used automatically when available, with dirty scheduler
thresholds to avoid blocking normal schedulers on large inputs.
| Filter | Accelerated operations | Dirty threshold |
|---|---|---|
| Bloom | put_many, merge |
10,000 / 50,000 items |
| Cuckoo | put_many |
10,000 items |
| Quotient | put_many, merge |
10,000 / 50,000 items |
| CQF | put_many, merge |
10,000 / 50,000 items |
| XorFilter | build |
10,000 items |
| IBLT | put_many, merge |
10,000 / 50,000 items |
Both backends produce byte-identical serialized output for the same inputs,
verified by deterministic parity tests.
Benchmarks
New benchmark suites:
bench/req_bench.exs-- REQ sketch operationsbench/misra_gries_bench.exs-- Misra-Gries operationsbench/xxhash3_bench.exs-- XXHash3 NIF throughput at various data sizes
Existing membership filter benchmarks now automatically show Pure vs Rust
comparison columns when the NIF is available.
Run all benchmarks with mix bench.
Algorithm Matrix
| Algorithm | Purpose | EXSK ID | Backend |
|---|---|---|---|
| HLL | Cardinality estimation | 1 | Pure + Rust |
| CMS | Frequency estimation | 2 | Pure + Rust |
| Theta | Set operations | 3 | Pure + Rust |
| KLL | Rank/quantile/cdf/pmf | 4 | Pure + Rust |
| DDSketch | Value-relative quantiles/rank | 5 | Pure + Rust |
| FrequentItems | Probabilistic heavy-hitter detection | 6 | Pure + Rust |
| Bloom | Membership testing | 7 | Pure + Rust |
| Cuckoo | Membership with deletion | 8 | Pure + Rust |
| Quotient | Membership with deletion/merge | 9 | Pure + Rust |
| CQF | Multiset membership/counting | 10 | Pure + Rust |
| XorFilter | Static membership testing | 11 | Pure + Rust |
| IBLT | Set reconciliation | 12 | Pure + Rust |
| REQ | Relative-error quantiles | 13 | Pure |
| MisraGries | Deterministic heavy hitters | 14 | Pure |
| FilterChain | Filter composition | -- | Pure |
| Quantiles | Quantile facade (KLL/DDSketch) | -- | -- |
Installation
def deps do
[
{:ex_data_sketch, "~> 0.6.0"}
]
endPrecompiled Rust NIF binaries are downloaded automatically on supported
platforms (macOS ARM64/x86_64, Linux x86_64/aarch64 glibc/musl). No Rust
toolchain required. The library works in pure Elixir mode on all other
platforms.
Upgrade Notes
- No breaking changes from v0.5.0.
- EXSK binaries produced by earlier v0.x releases remain fully compatible.
- The
ExDataSketch.Backendbehaviour now includes additional callbacks for
REQ and Misra-Gries. Custom backend implementations must add these callbacks. ExDataSketch.Quantilesno longer includes REQ in the facade (KLL and
DDSketch only). REQ is accessed directly viaExDataSketch.REQ.- Membership filter NIF acceleration is automatic and transparent. No code
changes are needed to benefit from the Rust backend. - The
Quantiles.quantiles/2typespec was tightened from
[float() | nil] | nilto[float() | nil].
Rust NIF Safety Hardening
This release includes several safety improvements to the Rust NIF layer:
- All recursive traversals in quotient filter slot arithmetic converted to
bounded iterative loops to prevent stack overflow shift_rightbounded to at mostslot_countiterations to prevent infinite
loops on full/corrupt filter state- Binary header validation before slicing in all NIF entry points to prevent
out-of-bounds panics - Parameter validation (zero bucket counts, out-of-range fingerprint bits) to
prevent arithmetic overflow OwnedBinary::new()allocation failure returns error tuples instead of
panicking- XorFilter build uses deterministic deduplication (sort + dedup) and
deterministic peeling order for cross-node reproducibility - XorFilter seed retry uses wrapping addition to prevent u32 overflow panic
- MisraGries
binary_to_termuses[:safe]option to prevent atom-table
exhaustion from untrusted data
What's Next
v0.7.0 adds UltraLogLog (ULL), a next-generation cardinality estimation
sketch based on Ertl (2023). ULL delivers approximately 20% lower relative
error than HyperLogLog at the same memory footprint, with full Pure Elixir
and Rust NIF backends, EXSK serialization, and distributed merge support.
Links
v0.5.0
Release date: 2026-03-11
Summary
v0.5.0 adds six new structures for advanced membership testing and set
reconciliation, plus FilterChain for composing filters into lifecycle-tier
pipelines. This is the largest feature release yet, bringing ExDataSketch to
13 sketch types across seven categories.
ExDataSketch now covers:
| Category | Algorithms |
|---|---|
| Cardinality | HyperLogLog (HLL) |
| Frequency | Count-Min Sketch (CMS) |
| Set operations | Theta Sketch |
| Quantiles | KLL, DDSketch |
| Frequency ranking | FrequentItems (SpaceSaving) |
| Membership | Bloom, Cuckoo, Quotient, CQF, XorFilter |
| Set reconciliation | IBLT |
| Composition | FilterChain |
What's new in v0.5.0
Cuckoo Filter (ExDataSketch.Cuckoo)
Membership testing with deletion support using partial-key cuckoo hashing.
Unlike Bloom filters, Cuckoo filters support safe deletion of previously
inserted items. Better space efficiency than Bloom at low false positive rates.
Key features:
- Partial-key cuckoo hashing with configurable fingerprint size (8, 12, 16 bits)
- Configurable bucket size (2 or 4 slots) and max kicks (100..2000)
- Safe deletion without false negatives on subsequent queries
- CKO1 binary state format
- EXSK serialization (sketch ID 8)
Quick start:
cuckoo = ExDataSketch.Cuckoo.new(capacity: 100_000, fingerprint_size: 8)
{:ok, cuckoo} = ExDataSketch.Cuckoo.put(cuckoo, "user_42")
ExDataSketch.Cuckoo.member?(cuckoo, "user_42") # true
{:ok, cuckoo} = ExDataSketch.Cuckoo.delete(cuckoo, "user_42")
ExDataSketch.Cuckoo.member?(cuckoo, "user_42") # falseQuotient Filter (ExDataSketch.Quotient)
Membership testing with safe deletion and merge. Splits fingerprints into
quotient (slot index) and remainder (stored value) with metadata bits for
cluster tracking. Supports merge without re-hashing.
Key features:
- Quotient/remainder fingerprint splitting with linear probing
- Metadata bits (is_occupied, is_continuation, is_shifted) for cluster tracking
- Safe deletion and merge support
- QOT1 binary state format
- EXSK serialization (sketch ID 9)
Quick start:
qf = ExDataSketch.Quotient.new(q: 16, r: 8)
qf = ExDataSketch.Quotient.put(qf, "item_a")
ExDataSketch.Quotient.member?(qf, "item_a") # true
# Merge two quotient filters
merged = ExDataSketch.Quotient.merge(qf_a, qf_b)Counting Quotient Filter (ExDataSketch.CQF)
Extends the quotient filter with variable-length counter encoding to answer
not just "is this present?" but "how many times was this inserted?" Counts are
approximate (never underestimated).
Key features:
- Variable-length counter encoding within runs
estimate_count/2for approximate multiplicity queries- Safe deletion (decrements count)
- Merge sums counts across filters
- CQF1 binary state format
- EXSK serialization (sketch ID 10)
Quick start:
cqf = ExDataSketch.CQF.new(q: 16, r: 8)
cqf = ExDataSketch.CQF.put(cqf, "event_x")
cqf = ExDataSketch.CQF.put(cqf, "event_x")
cqf = ExDataSketch.CQF.put(cqf, "event_x")
ExDataSketch.CQF.estimate_count(cqf, "event_x") # >= 3
ExDataSketch.CQF.member?(cqf, "event_x") # trueXorFilter (ExDataSketch.XorFilter)
Static, immutable membership filter with the smallest footprint and fastest
lookups. All items must be provided at construction time via build/2.
No insertion, deletion, or merge -- query only.
Key features:
- Build-once immutable construction via
build/2 - 8-bit or 16-bit fingerprints (configurable false positive rate)
- Smallest memory footprint of all membership filters
- XOR1 binary state format
- EXSK serialization (sketch ID 11)
Quick start:
items = MapSet.new(1..100_000)
{:ok, xor} = ExDataSketch.XorFilter.build(items, fingerprint_bits: 8)
ExDataSketch.XorFilter.member?(xor, 42) # true
ExDataSketch.XorFilter.member?(xor, 999_999) # false (probably)IBLT (ExDataSketch.IBLT)
Invertible Bloom Lookup Table for set reconciliation. Two parties each build
an IBLT from their sets, exchange them, and subtract to discover only the
differing items -- without transmitting the full sets.
Key features:
- Set mode (items only) and key-value mode
subtract/2for cell-wise differencelist_entries/1for peeling decoded entries (positive/negative sets)- Merge via cell-wise addition
- IBL1 binary state format
- EXSK serialization (sketch ID 12)
Quick start:
# Set reconciliation between two nodes
iblt_a = ExDataSketch.IBLT.new(cell_count: 1000) |> ExDataSketch.IBLT.put_many(set_a)
iblt_b = ExDataSketch.IBLT.new(cell_count: 1000) |> ExDataSketch.IBLT.put_many(set_b)
diff = ExDataSketch.IBLT.subtract(iblt_a, iblt_b)
{:ok, %{positive: only_in_a, negative: only_in_b}} = ExDataSketch.IBLT.list_entries(diff)FilterChain (ExDataSketch.FilterChain)
Capability-aware composition framework for chaining membership filters into
lifecycle-tier pipelines. Enforces valid chain positions based on each
filter's capabilities.
Key features:
add_stage/2with position validation (front/middle/terminal/adjunct)put/2fans out to all stages supporting insertion (skips static stages)member?/2queries stages in order, short-circuits on false- Lifecycle-tier patterns: hot Cuckoo (writes) -> cold XorFilter (snapshots)
- IBLT stages placed as adjuncts (not in query path)
- FCN1 binary state format
Quick start:
chain =
ExDataSketch.FilterChain.new()
|> ExDataSketch.FilterChain.add_stage(ExDataSketch.Cuckoo.new(capacity: 10_000))
|> ExDataSketch.FilterChain.add_stage(ExDataSketch.Bloom.new(capacity: 100_000))
{:ok, chain} = ExDataSketch.FilterChain.put(chain, "item_1")
ExDataSketch.FilterChain.member?(chain, "item_1") # trueBenchmarks
Benchmark suites added for all new structures:
bench/cuckoo_bench.exsbench/quotient_bench.exsbench/cqf_bench.exsbench/xor_filter_bench.exsbench/iblt_bench.exsbench/filter_chain_bench.exs
Run all benchmarks with mix bench.
Algorithm Matrix
| Algorithm | Purpose | EXSK ID | Backend |
|---|---|---|---|
| HLL | Cardinality estimation | 1 | Pure + Rust |
| CMS | Frequency estimation | 2 | Pure + Rust |
| Theta | Set operations | 3 | Pure + Rust |
| KLL | Rank/quantile estimation | 4 | Pure + Rust |
| DDSketch | Value-relative quantiles | 5 | Pure + Rust |
| FrequentItems | Heavy-hitter detection | 6 | Pure + Rust |
| Bloom | Membership testing | 7 | Pure |
| Cuckoo | Membership with deletion | 8 | Pure |
| Quotient | Membership with deletion/merge | 9 | Pure |
| CQF | Multiset membership/counting | 10 | Pure |
| XorFilter | Static membership testing | 11 | Pure |
| IBLT | Set reconciliation | 12 | Pure |
| FilterChain | Filter composition | -- | Pure |
Installation
def deps do
[
{:ex_data_sketch, "~> 0.5.0"}
]
endPrecompiled Rust NIF binaries are downloaded automatically on supported
platforms (macOS ARM64/x86_64, Linux x86_64/aarch64 glibc/musl). No Rust
toolchain required. The library works in pure Elixir mode on all other
platforms.
Upgrade Notes
- No breaking changes from v0.4.0.
- EXSK binaries produced by earlier v0.x releases remain fully compatible.
- The
ExDataSketch.Backendbehaviour now includes additional callbacks for
Cuckoo, Quotient, CQF, XorFilter, IBLT, and FilterChain. Custom backend
implementations must add these callbacks. - New error types:
UnsupportedOperationErrorandInvalidChainCompositionError. - All membership filter modules now export
capabilities/0returning a MapSet
of supported operations.
What's Next
- v0.6.0 - Rust NIF acceleration for v0.5.0 structures, and advanced Quantiles
- v0.7.0 - advanced Cardinality Sketches including CPC, HLL++, ULL (UltraLogLog), SetSketch
- v0.8.0 scope is under discussion. Potential directions include Rust NIF
acceleration for v0.5.0 structures, additional composition patterns, or
new sketch types.
Links
v0.4.0
Full Changelog: v0.3.0...v0.4.0
What's new in v0.4.0
Bloom filter:
- Automatic parameter derivation from capacity and false_positive_rate
- Double hashing (Kirsch-Mitzenmacher) for k bit positions from one hash
- BLM1 binary format (40-byte header + packed bitset)
- Merge via bitwise OR
- Capacity overflow validation for the u32 binary format constraints
- 40 unit tests, 5 property tests, 2 statistical FPR validation tests
| Algorithm | What it does |
|---|---|
| HyperLogLog (HLL) | Count distinct elements (~0.8% error, 16 KB) |
| Count-Min Sketch (CMS) | Estimate item frequencies |
| Theta Sketch | Set union/intersection cardinality |
| KLL | Rank-accurate quantiles (median, P99) |
| DDSketch | Value-relative quantiles (P99 latency +/- 1%) |
| FrequentItems | Top-k most frequent items (SpaceSaving) |
| Bloom Filter | "Have I seen this before?" with tunable FPR |
ExDataSketch v0.3.0 -- FrequentItems / Heavy-Hitters
ExDataSketch v0.3.0 -- FrequentItems / Heavy-Hitters
ExDataSketch v0.3.0 adds FrequentItems, a streaming heavy-hitter sketch based on the SpaceSaving algorithm. This is the sixth sketch family in the library and the first non-numeric sketch type.
What is FrequentItems?
FrequentItems tracks the approximate top-k most frequent items in a data stream using bounded memory. It maintains at most k counters, each storing an item, its estimated count, and a maximum overcount error. The SpaceSaving algorithm guarantees that any item whose true frequency exceeds N/k will always be tracked.
sketch =
ExDataSketch.FrequentItems.new(k: 10)
|> ExDataSketch.FrequentItems.update_many(stream_of_page_views)
# Get the top 5 most frequent items
ExDataSketch.FrequentItems.top_k(sketch, 5)
# [%{item: "/home", estimate: 4821, error: 12, lower: 4809, upper: 4821}, ...]
# Check a specific item
ExDataSketch.FrequentItems.estimate(sketch, "/checkout")
# {:ok, %{estimate: 312, error: 5, lower: 307, upper: 312}}Highlights
Full Pure Elixir + Rust NIF dual-backend support
Like all ExDataSketch algorithms, FrequentItems ships with a pure Elixir implementation and optional Rust NIF acceleration. Both backends produce byte-identical serialized output for the same inputs.
Canonical FI1 binary state format
FrequentItems uses a 32-byte header followed by variable-length entries sorted by item bytes. The format is deterministic and portable across backends.
Flexible key encoding
Three key encoding modes are supported:
| Encoding | Use case |
|---|---|
:binary (default) |
String keys, raw binary data |
:int |
Integer keys (signed 64-bit little-endian) |
{:term, :external} |
Arbitrary Erlang terms via :erlang.term_to_binary/1 |
Deterministic merge
FrequentItems merge is commutative. It combines counts additively across the union of keys, then replays weighted updates in sorted key order into an empty sketch. Count (n) is always exactly additive regardless of whether entries are dropped during capacity enforcement.
Smart NIF routing
Query operations (top_k, estimate) route to Rust only when k >= 256, where NIF acceleration outweighs the call boundary overhead. Header reads (count, entry_count) always use Pure Elixir since they are O(1) binary pattern matches.
Other changes
- Rust NIF for
theta_compactwith dirty scheduler support. - Mox added as a test dependency for backend contract testing.
- EXSK codec sketch ID 6 for FrequentItems.
Upgrading
def deps do
[
{:ex_data_sketch, "~> 0.3.0"}
]
endNo breaking changes from v0.2.1. All existing sketches (HLL, CMS, Theta, KLL, DDSketch) are unchanged.
Algorithm coverage
| Algorithm | Purpose | Status |
|---|---|---|
| HyperLogLog (HLL) | Cardinality estimation | Pure + Rust |
| Count-Min Sketch (CMS) | Frequency estimation | Pure + Rust |
| Theta Sketch | Set operations on cardinalities | Pure + Rust |
| KLL Quantiles | Rank and quantile estimation | Pure + Rust |
| DDSketch | Relative-error quantile estimation | Pure + Rust |
| FrequentItems (SpaceSaving) | Heavy-hitter detection | Pure + Rust |
v0.2.1
v0.2.0-alpha.1
What's Changed
- Deterministic vectors by @thanos in #76
- Add KLL quantiles sketch with Pure Elixir and Rust NIF backends by @thanos in #86
Full Changelog: v0.1.0-alpha.12...v0.2.0-alpha.1