Skip to main content

Bloomsieve

CI Python License: MIT

Stop sending unnecessary membership checks to Redis. Bloomsieve is a persistent mmap-backed local Bloom filter that rejects definite-negative membership queries before they ever become a network request to RedisBloom.

                         Application
                              │
                              ▼
                     ┌─────────────────┐
                     │    Bloomsieve   │
                     │   local mmap    │
                     └────────┬────────┘
                              │
                   ┌──────────┴──────────┐
                   │                     │
             definitely absent      possibly present
                   │                     │
                   ▼                     ▼
                 return               RedisBloom
                locally              verification

Why this exists

Most membership workloads are negative-heavy: "is this user active?", "is this token valid?", "is this key seen before?" are mostly answered "no". With a direct RedisBloom setup, every one of those queries crosses the network — even the ones that are trivially absent.

A Bloom filter has no false negatives, so a local "definitely absent" answer is provably correct. Bloomsieve keeps a persistent local mirror on disk (mmap), answers negatives locally, and only sends the possible positives to RedisBloom for verification. On a 99%-negative workload the local filter removes ~99% of Redis membership requests (see Benchmarks).

Installation

Core mode has no runtime dependencies:

pip install bloomsieve

RedisBloom integration is optional:

pip install "bloomsieve[redis]"

30-second example

from bloomsieve import BloomFilter


def lookup_user(user_id: str):
    """Return the cached answer, or None when the user is definitely absent."""
    bloom = BloomFilter(
        capacity=10_000_000,
        error_rate=0.001,
        filepath="./users.bloom",   # optional: persists the filter to disk
    )
    bloom.add("user:123")           # normal application write path

    if user_id not in bloom:
        return None                 # definite answer, nothing more to do

    return f"look up {user_id} in your database for an exact answer"


print(lookup_user("user:999"))      # None  – rejected by the local filter
print(lookup_user("user:123"))      # possible positive -> verify downstream

capacity is the expected number of items, error_rate the target false-positive probability. Pass filepath to persist the filter across restarts.

Redis example

import redis
from bloomsieve import BloomFilterService

client = redis.Redis(host="redis.example.com", port=6379, db=0)

svc = BloomFilterService(
    redis_client=client,
    capacity=1_000_000,
    error_rate=0.001,
    use_mmap=True,                 # enable the local pre-filter
    mmap_dir="/var/lib/bloomsieve",
)

svc.create_filter("active_tokens")
svc.add("active_tokens", "tok_abc")

svc.exists("active_tokens", "tok_xyz")   # False  – answered locally, no network
svc.exists("active_tokens", "tok_abc")   # True   – possible positive, verified in Redis

How it works

  • One SHA-256 digest per item, expanded to k positions with the Kirsch-Mitzenmacher double-hashing technique.
  • A 16-byte header (m, k) plus the bit array, stored in a memory-mapped file; reopening a file always uses the stored configuration.
  • BloomFilterService layers RedisBloom on top. Every add writes to both; every lookup checks the local mirror first and only verifies in Redis when the local answer is not a definite negative.

See docs/architecture.md for the full design including the on-disk format, failure modes, and consistency model.

Performance

Micro-benchmarks of the local filter run at ~200–300k lookups/s with sub-10µs p50 latencies. The workload A/B benchmark measures what matters — Redis requests removed:

negative workload baseline BF.EXISTS Bloomsieve requests avoided
50% 20,000 10,011 50%
75% 20,000 5,013 75%
90% 20,000 2,014 90%
99% 20,000 216 99%

Measured on a laptop over localhost; remote Redis deployments amplify the latency win because each removed request saves a round-trip. Full methodology, hardware, and how to reproduce: docs/benchmarks.md.

When should I use Bloomsieve?

Good fit:

  • membership checks against Redis are frequent and mostly negative
  • Redis is remote, so network latency matters
  • a tunable probabilistic pre-filter is acceptable
  • you benefit from a persistent, process-independent local filter (multiple app instances can share one file)

Poor fit:

  • almost every lookup is positive (the local filter buys you nothing)
  • membership checks are already local (you don't need Redis at all)
  • exact membership is required with no verification step (Bloom filters have false positives)
  • the dataset churns faster than your rebuild/rotation cycle can refresh the mirror

Bloom-filter semantics, precisely:

  • no false negatives under correct operation — a local "absent" is definite;
  • possible false positives — a local "present" must be verified against RedisBloom (or another authoritative source) when exact membership matters;
  • false positives can be traded down by lowering error_rate (larger filter).

Features

  • Standalone BloomFilter: in-memory or persistent mmap, zero dependencies.
  • BloomFilterService: local-negative short-circuit in front of RedisBloom.
  • rebuild() + swap() rotation with chunked bulk insertion.
  • Advisory Redis locks for coordinated rebuilds.
  • Corrupt/truncated file detection (BloomFilterFileError), conservative Redis failure fallbacks, and full logging of fallback situations.

API overview

BloomFilter

BloomFilter(capacity: int, error_rate: float, filepath: str | None = None)
  • add(item: str | bytes) -> bool — insert; True if a bit changed, False if already likely present.
  • item in bf — membership (no false negatives; True = possible positive).
  • clear() -> None — reset all bits.
  • flush() -> None — persist dirty pages to disk.
  • close() -> None — flush and close file handles (context-manager compatible).
  • m, k, byte_size, newly_created, synced — read-only diagnostics.

BloomFilterService

BloomFilterService(redis_client, capacity=1_000_000, error_rate=0.001,
                   expansion=2, use_mmap=False, mmap_dir="bloom_filters")
  • create_filter(name, capacity=None, error_rate=None) -> bool — reserve via BF.RESERVE (createFilter kept as a backwards-compatible alias).
  • add(name, item) -> bool
  • exists(name, item) -> bool — the local-negative short-circuit.
  • rebuild(name, items, capacity=None, error_rate=None) -> bool
  • swap(temp_name, live_name) -> bool — rotate a rebuilt filter into place.
  • get_info(name) -> dict, load_ratio(name) -> float
  • acquire_lock(name, ttl=600) / release_lock(name) -> bool
  • flush(name=None) -> None

Persistence / mmap behavior

  • Writes go to the kernel page cache immediately and are visible to every process mapping the file; they are durable on disk after flush()/close() or OS writeback.
  • Reopening a file trusts the stored header; a corrupt header or a file truncated below its bit array raises BloomFilterFileError.
  • The rotation path (swap) flushes the temporary mirror before renaming it into place.

Consistency and recovery

Redis and the local filesystem are updated as two separate steps — Bloomsieve does not claim a cross-system atomic swap:

  1. RENAME the filter in Redis; if that fails nothing else happens.
  2. Rotate the local files.

If a failure lands between the two steps the service logs it and returns False; a subsequent rebuild() repopulates both sides consistently. A freshly created local mirror is treated as "unknown" (falling back to Redis) until items have been added through the service, so an empty mirror can never produce false negatives. Details: docs/architecture.md.

Limitations

  • Bloom filters cannot delete items; refresh with rebuild()/swap().
  • After capacity is exceeded the false-positive rate rises; it does not break.
  • The local mirror is only as fresh as its last flush()/close(); if the process crashes mid-write the mirror can lag Redis (rebuild to recover).
  • The service's locks are advisory; they are not a consensus-grade distributed lock.
  • Core is tested on Python 3.9–3.13; Python 3.8 is not supported.

Development

git clone https://github.com/deepak7448/bloomsieve.git
cd bloomsieve
python -m venv .venv && source .venv/bin/activate
pip install -e ".[dev,redis]"

Testing

ruff check .
pytest                                 # unit tests (no Redis required)

# also run the opt-in live-Redis integration suite:
BLOOMSIEVE_REDIS_URL=redis://localhost:6379/0 pytest

Benchmarks

python benchmarks/benchmark_core.py    # standalone in-memory filter
python benchmarks/benchmark_mmap.py    # persistent mmap filter
BLOOMSIEVE_REDIS_URL=redis://localhost:6379/0 python benchmarks/benchmark_redis.py

See docs/benchmarks.md for methodology and results.

Contributing

Issues and pull requests are welcome. Please run the linter and the full test suite (including the live Redis suite if you can) before submitting.

License

MIT. See LICENSE.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

bloomsieve-0.2.0.tar.gz (29.8 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

bloomsieve-0.2.0-py3-none-any.whl (16.0 kB view details)

Uploaded Python 3

File details

Details for the file bloomsieve-0.2.0.tar.gz.

File metadata

  • Download URL: bloomsieve-0.2.0.tar.gz
  • Upload date:
  • Size: 29.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.9.25

File hashes

Hashes for bloomsieve-0.2.0.tar.gz
Algorithm Hash digest
SHA256 83cdf5794cf0abd46c8d1e2caaf61a1453bee2e01b3a74a42c68aead4495257d
MD5 9e25f71e7395b36eaae7cd857bd97199
BLAKE2b-256 00b426a8f59217e4c86ce4779e2a1e09a423c6438a935ae3029063b71401b3f3

See more details on using hashes here.

File details

Details for the file bloomsieve-0.2.0-py3-none-any.whl.

File metadata

  • Download URL: bloomsieve-0.2.0-py3-none-any.whl
  • Upload date:
  • Size: 16.0 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.9.25

File hashes

Hashes for bloomsieve-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 7bc1056b64374dc89754362bbb980cd2c0c41d77d6e11e47d9c420196cef3da8
MD5 c87d24518f72b64674483953349ab4ee
BLAKE2b-256 4f786128e04a206a11f2e5aa21739cc021ebe5e6fef93908a2e3e2b4c137aa12

See more details on using hashes here.

Release history Release notifications | RSS feed

0.2.1

2 files

This release

0.2.0 This release

2 files

0.1.4

2 files

0.1.3

2 files

0.1.2

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page