๐Ÿ“š ribbonGo - Awesome Go Library for Data Structures and Algorithms

Go Gopher mascot for ribbonGo

First pure Go implementation of Ribbon filters (practically smaller than Bloom and Xor) for space-efficient approximate set membership queries

๐Ÿท๏ธ Data Structures and Algorithms
๐Ÿ“‚ Bloom and Cuckoo Filters
โญ 0 stars
View on GitHub ๐Ÿ”—

Detailed Description of ribbonGo

ribbonGo

GoDoc License: MIT Go Report Card Coverage Status

A high-performance Ribbon filter implementation in Go โ€” the space-efficient probabilistic data structure that is practically smaller than Bloom and Xor filters.

Based on the paper:

"Ribbon filter: practically smaller than Bloom and Xor" Peter C. Dillinger & Stefan Walzer, 2021 (arXiv:2103.02515)


What is a Ribbon Filter?

A Ribbon filter is a static, space-efficient probabilistic data structure for approximate set membership queries โ€” the same problem Bloom filters solve, but using significantly less space.

Given a set of keys, a Ribbon filter can answer "is this key in the set?" with:

  • No false negatives โ€” if a key is in the set, the filter always says yes.
  • Configurable false positive rate โ€” non-member keys have a tunable probability of being incorrectly reported as members (FPR โ‰ˆ 2โˆ’r for r result bits).
  • Near-optimal space โ€” approaches the information-theoretic lower bound, using only ~1โ€“5% more space than the minimum possible.

Ribbon vs Bloom vs Xor

PropertyBloomXorRibbon
Space overhead~44% over optimal~23% over optimal~1โ€“5% over optimal
ConstructionFast, incrementalFast, staticModerate, static
Supports deletionNo (without counting)NoNo
FPR at 1% target (bits/key)9.69.8โ‰ˆ 8.0

Ribbon filters are ideal for read-heavy, write-once workloads โ€” LSM-tree engines (RocksDB, LevelDB), static lookup tables, networking data planes, and any scenario where a set is built once and queried millions of times.


Quick Start

Installation

go get github.com/RibbonGo/ribbonGo

Requires Go 1.25+.

Usage

package main

import (
    "fmt"
    "log"

    "github.com/RibbonGo/ribbonGo"
)

func main() {
    // Create a filter with default settings (w=128, r=7, FPR โ‰ˆ 0.78%)
    f := ribbon.New()

    // Build the filter from a set of keys
    keys := []string{"apple", "banana", "cherry", "date", "elderberry"}
    if err := f.Build(keys); err != nil {
        log.Fatal(err)
    }

    // Query membership
    fmt.Println(f.Contains("banana"))    // true  (always correct for members)
    fmt.Println(f.Contains("fig"))       // false (probably โ€” FPR โ‰ˆ 0.78%)
}

Custom Configuration

// Trade space for faster construction with a narrower ribbon width
f := ribbon.NewWithConfig(ribbon.Config{
    CoeffBits:           64,    // w=64: balanced speed/space
    ResultBits:          8,     // r=8: FPR โ‰ˆ 0.39%
    FirstCoeffAlwaysOne: true,  // deterministic pivot (faster)
    MaxSeeds:            256,   // hash seed retries before failure
})

if err := f.Build(keys); err != nil {
    // err is ribbon.ErrConstructionFailed if banding fails
    log.Fatal(err)
}

API Reference

The public API is intentionally minimal โ€” a single type with four functions.

Types

TypeDescription
RibbonThe main filter type. Create โ†’ Build โ†’ Contains.
ConfigConstruction parameters (ribbon width, result bits, etc.).
ErrConstructionFailedSentinel error returned when banding fails for all seed retries.

Functions

FunctionSignatureDescription
Newfunc New() *RibbonCreate a filter with defaults: w=128, r=7, fcao=true, maxSeeds=256
NewWithConfigfunc NewWithConfig(cfg Config) *RibbonCreate a filter with custom parameters. Panics on invalid config.
Buildfunc (r *Ribbon) Build(keys []string) errorConstruct the filter from a set of unique keys. May be called multiple times.
Containsfunc (r *Ribbon) Contains(key string) boolTest membership. Zero allocations, safe for concurrent use after Build.

Config Fields

FieldTypeDefaultDescription
CoeffBitsuint32128Ribbon width w โˆˆ {32, 64, 128}. Larger โ†’ more compact, slower to build.
ResultBitsuint7Fingerprint bits r โˆˆ [1, 8]. FPR โ‰ˆ 2โˆ’r.
FirstCoeffAlwaysOnebooltrueForce bit 0 of coefficient rows to 1 for deterministic pivoting.
MaxSeedsuint32256Maximum hash seed retries before returning ErrConstructionFailed.

Features

  • Paper-faithful implementation โ€” every design decision traces to a specific section of Dillinger & Walzer (2021), with ยงN citations throughout the code
  • Configurable ribbon width โ€” w โˆˆ {32, 64, 128} to trade construction speed for space efficiency
  • Configurable result bits โ€” r bits per key for FPR โ‰ˆ 2โˆ’r
  • Dynamic slot computation โ€” overhead ratio grows logarithmically with n, ported from RocksDB's empirical tables (ribbon_config.cc)
  • firstCoeffAlwaysOne optimisation โ€” deterministic pivot from paper ยง4, skipping leading-zero scan
  • SoA memory layout โ€” Struct-of-Arrays for maximum cache utilisation (8 coefficients per cache line at w=64)
  • Width-specialised inner loops โ€” pure uint64 path for wโ‰ค64, separate lo/hi ops for w=128
  • Software-pipelined prefetching โ€” AddRange hides L2/L3 latency by prefetching the next key's slot
  • Zero allocations on hot paths โ€” no heap escapes during construction or query
  • Two-phase hash pipeline โ€” hash keys once, remix per seed attempt; uses XXH3 (64-bit)
  • Research-friendly โ€” all paper parameters exposed; reference (slow*) implementations included for cross-validation

Benchmarks

Apple M3 Pro ยท Go 1.25 ยท ARM64 ยท r = 7 result bits ยท firstCoeffAlwaysOne = true

All benchmarks follow the methodology of Dillinger & Walzer (2021), testing at both n = 10โถ and n = 10โธ keys.

Build Performance

Construction throughput and space efficiency at scale.

nWidthns/keybits/keyOverhead
10โถw=3256.8910.5431.81%
10โถw=6464.568.95911.99%
10โถw=128106.38.3804.749%
10โธw=32355.311.6245.30%
10โธw=64266.29.40617.58%
10โธw=128384.78.5857.314%

At n = 10โถ, w=128 achieves 8.38 bits/key โ€” only 4.7% above the information-theoretic minimum (7 bits for r=7). At n = 10โธ, it remains under 8.6 bits/key with just 7.3% overhead.

Query Performance

Lookup latency per key (n = 10โถ).

WidthPositive (ns/op)Negative (ns/op)
w=3237.2536.85
w=6453.7052.49
w=12888.6684.73

Query time scales linearly with ribbon width, as expected โ€” the inner loop performs a dot product over w bits. Positive and negative queries have nearly identical cost.

Space Efficiency

Bits per key at both scales, with packed (information-theoretic) comparison.

nWidthbits/keypacked bits/keyOverhead
10โถw=3210.549.22731.81%
10โถw=648.9597.83911.99%
10โถw=1288.3807.3324.749%
10โธw=3211.6210.1745.30%
10โธw=649.4068.23117.58%
10โธw=1288.5857.5127.314%

w=128 at n = 10โถ uses only 8.38 bits/key โ€” compare with Bloom filters at 9.6 bits/key for the same FPR. That's a 12.7% space saving over Bloom.

Run Benchmarks Yourself

# Full benchmark suite
go test -bench=. -benchmem -count=3 ./...

# Paper-aligned benchmarks at n=10โถ and n=10โธ
go test -run=^$ -bench='BenchmarkRibbon' -benchtime=3s -count=1

# Specific benchmark
go test -run=^$ -bench='BenchmarkRibbonBuild/w=128/n=1000000' -benchtime=3s

Architecture

The implementation follows the paper's full algorithmic pipeline:

Key โ†’ [Hash] โ†’ [Bander] โ†’ [Solver] โ†’ [Filter/Query]
       ยง2         ยง2,ยง4       ยง2           ยง2

Pipeline Layers

LayerFileDescription
uint128uint128.go128-bit integer type for coefficient rows when w=128.
Hashhash.goTwo-phase pipeline: hash each key once with XXH3, then cheaply remix per seed to derive (start, coeffRow, result) triples.
Banderbander.goOn-the-fly Gaussian elimination over GF(2). Converts hashed equations into an upper-triangular banded matrix. Hottest code path during construction.
Solversolver.goBack-substitution: solves the upper-triangular system to produce the compact solution vector encoding the filter.
Filterfilter.goQuery evaluation: one hash, one dot product over GF(2). Also provides false-positive rate estimation.
Builderbuilder.goOrchestrates the full pipeline: hashing โ†’ banding โ†’ solving โ†’ filter construction. Includes RocksDB-style dynamic slot computation.
Public APIribbon.goSole public surface: Ribbon, Config, New(), NewWithConfig(), Build(), Contains().

Key Optimisations

SoA layout โ€” Coefficient data is stored in parallel arrays (coeffLo []uint64, coeffHi []uint64, result []uint8) instead of an array of structs. For wโ‰ค64, coeffHi is nil โ€” zero memory, zero operations. This doubles the number of coefficients per cache line compared to AoS layout.

Width specialisation โ€” add() dispatches to addW64() (pure uint64, ~10 ARM64 instructions per elimination step) or addW128() (separate lo/hi uint64 ops). The generic uint128.rsh() branch dispatch is avoided entirely.

Software-pipelined prefetching โ€” addRange() touches coeffLo[nextKey.start] while processing the current key, pulling the next cache line into L1. This mirrors RocksDB's BandingAddRange approach and yields 20โ€“36% throughput improvement at scale.

Dynamic overhead ratio โ€” The number of slots m is computed using RocksDB-style empirical tables where overhead grows logarithmically with n. This ensures reliable banding at all scales, unlike a fixed overhead formula that breaks down at large n.

Two-phase hashing โ€” Keys are hashed once with XXH3. On seed retry, only a cheap remix is applied to derive new (start, coeff, result) triples โ€” no re-hashing of the original key data.


How It Works

At a high level, a Ribbon filter encodes set membership as a system of linear equations over GF(2):

  1. Hash each key to a triple: starting position s, a w-bit coefficient row c, and an r-bit result r.
  2. Band the equations into an upper-triangular matrix via Gaussian elimination (the "banding" step).
  3. Solve the triangular system by back-substitution, producing a compact solution vector Z.
  4. Query a candidate key by computing its triple, then checking if c ยท Z[s..s+w] == r.

The "ribbon" name refers to the banded structure of the coefficient matrix โ€” each equation touches only w consecutive columns starting at position s, forming a ribbon-like diagonal band.

Why is it smaller than Bloom?

A Bloom filter wastes space because its bit-setting positions are independent โ€” you can't pack information as tightly. A Ribbon filter encodes key membership as equations, allowing the solver to pack nearly r bits of information per key into the solution vector. The information-theoretic minimum is r bits/key, and Ribbon achieves within 1โ€“5% of this.


Testing

23 test functions across the public API, plus extensive internal tests โ€” all passing.

Tests cover:

  • Correctness โ€” single insertion, no false negatives, false-positive rate validation, collision chains, 128-bit boundary crossing, redundant/contradictory equations
  • All configurations โ€” w โˆˆ {32, 64, 128} ร— firstCoeffAlwaysOne โˆˆ {true, false} ร— r โˆˆ {1, 4, 7, 8}
  • Scale โ€” builds at n = 100,000 verified against expected FPR
  • Edge cases โ€” empty input, single key, nil/empty filter, rebuild semantics, invalid config panics
  • Cross-validation โ€” optimised add() vs reference slowadd(), addRange() vs add()-loop โ€” all verified slot-by-slot
go test -v -count=1 ./...

Project Structure

ribbon/
โ”œโ”€โ”€ ribbon.go              # Public API (Ribbon, Config, New, Build, Contains)
โ”œโ”€โ”€ builder.go             # Pipeline orchestration, slot computation
โ”œโ”€โ”€ bander.go              # Gaussian elimination (banding)
โ”œโ”€โ”€ hash.go                # XXH3-based two-phase hash pipeline
โ”œโ”€โ”€ solver.go              # Back-substitution solver
โ”œโ”€โ”€ filter.go              # Query evaluation and FPR estimation
โ”œโ”€โ”€ uint128.go             # 128-bit integer type
โ”œโ”€โ”€ ribbon_test.go         # Public API tests (23 functions)
โ”œโ”€โ”€ ribbon_bench_test.go   # Paper-aligned benchmarks
โ”œโ”€โ”€ filter_test.go         # Internal filter/pipeline tests
โ”œโ”€โ”€ filter_bench_test.go   # Internal benchmarks
โ”œโ”€โ”€ bander_test.go         # Bander unit tests
โ”œโ”€โ”€ bander_bench_test.go   # Bander benchmarks
โ”œโ”€โ”€ hash_test.go           # Hash layer tests
โ”œโ”€โ”€ hash_bench_test.go     # Hash benchmarks
โ”œโ”€โ”€ solver_test.go         # Solver tests
โ”œโ”€โ”€ solver_bench_test.go   # Solver benchmarks
โ”œโ”€โ”€ uint128_test.go        # uint128 tests
โ”œโ”€โ”€ go.mod
โ”œโ”€โ”€ go.sum
โ”œโ”€โ”€ LICENSE
โ””โ”€โ”€ README.md

Contributing

Contributions are welcome! This project follows the paper's design closely โ€” please read the relevant paper section before modifying any algorithm.

Getting Started

git clone https://github.com/RibbonGo/ribbonGo.git
cd ribbonGo
go test ./...

Guidelines

  • Paper-first โ€” every design decision should cite a specific section (ยงN) of Dillinger & Walzer (2021)
  • RocksDB cross-references โ€” use [RocksDB: FunctionName in file.h] format for implementation parallels
  • All 6 configs โ€” tests must cover w โˆˆ {32, 64, 128} ร— firstCoeffAlwaysOne โˆˆ {true, false}
  • Reference implementations โ€” include slow* variants for cross-validation of optimised code
  • Zero allocations โ€” hot paths must not escape to heap; verify with go test -bench=X -benchmem
  • Naming โ€” unexported everything (standardBander, not StandardBander); constants use kCamelCase

References

License

MIT ยฉ 2026 RibbonGo