A colleague recently asked how to add Bloom filter capabilities into an existing system, noting the scarcity of clear documentation and code examples online. After reviewing the codebase, I’ve outlined a straightforward approach to integrate Redis-backed Bloom filters into .NET Core projects.
Start by installing the RedisBloom module in your Redis instance. Various installation guides are available, and I’ve verified several of them work. The existing codebase already uses StackExchange.Redis with some custom abstractions, so the goal is to extend that foundation without extra dependencies.
Defining Extension Methods for Bloom Commands
Create extension methods that encapsulate Redis Bloom filter operations. These map directly to native BF.RESERVE, BF.ADD, BF.MADD, BF.EXISTS, and BF.MEXISTS commands.
using StackExchange.Redis;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace RedisBloomExtensions
{
public static class RedisBloomCommands
{
public static async Task ReserveBloomFilterAsync(this IDatabaseAsync db, RedisKey filterKey, double allowedError, int capacity)
{
await db.ExecuteAsync("BF.RESERVE", filterKey, allowedError, capacity);
}
public static async Task<bool> AddToBloomAsync(this IDatabaseAsync db, RedisKey filterKey, RedisValue item)
{
return (bool)await db.ExecuteAsync("BF.ADD", filterKey, item);
}
public static async Task<bool[]> AddMultipleToBloomAsync(this IDatabaseAsync db, RedisKey filterKey, IEnumerable<RedisValue> items)
{
var parameters = new List<object> { filterKey };
parameters.AddRange(items.Cast<object>());
return (bool[])await db.ExecuteAsync("BF.MADD", parameters.ToArray());
}
public static async Task<bool> CheckBloomExistsAsync(this IDatabaseAsync db, RedisKey filterKey, RedisValue item)
{
return (bool)await db.ExecuteAsync("BF.EXISTS", filterKey, item);
}
public static async Task<bool[]> CheckMultipleBloomExistsAsync(this IDatabaseAsync db, RedisKey filterKey, IEnumerable<RedisValue> items)
{
var parameters = new List<object> { filterKey };
parameters.AddRange(items.Cast<object>());
return (bool[])await db.ExecuteAsync("BF.MEXISTS", parameters.ToArray());
}
}
}
Integrating into a Redis Helper Class
Enhance the existing RedisHelper abstraction sothat Bloom operations are available alongside other Redis functionality.
using Microsoft.Extensions.Logging;
using RedisBloomExtensions;
using StackExchange.Redis;
using System;
using System.Threading.Tasks;
namespace RedisClientLibrary
{
public class RedisHelper
{
private readonly int _databaseIndex;
private readonly string _keyPrefix;
private static string _connectionString;
private static ConnectionMultiplexer? _connection;
private static ConnectionMultiplexer EnsureConnection(string connectionString)
{
if (_connection == null || !_connection.IsConnected)
{
var config = ConfigurationOptions.Parse(connectionString);
config.AbortOnConnectFail = false;
config.AllowAdmin = true;
config.AsyncTimeout = 5000;
config.ConnectTimeout = TimeSpan.FromSeconds(15);
config.KeepAlive = 180;
_connection = ConnectionMultiplexer.Connect(config);
}
return _connection;
}
public RedisHelper(string connectionString, string keyPrefix = "", int databaseIndex = 0)
{
_connectionString = connectionString;
_keyPrefix = keyPrefix;
_databaseIndex = databaseIndex;
}
private IDatabase GetDatabase()
{
return EnsureConnection(_connectionString).GetDatabase(_databaseIndex);
}
public async Task<bool> BloomAddAsync(string filter, string item)
{
var key = new RedisKey($"{_keyPrefix}{filter}");
return await RedisBloomCommands.AddToBloomAsync(GetDatabase(), key, item);
}
public async Task<bool> BloomContainsAsync(string filter, string item)
{
var key = new RedisKey($"{_keyPrefix}{filter}");
return await RedisBloomCommands.CheckBloomExistsAsync(GetDatabase(), key, item);
}
public async Task BloomSetupAsync(string filter, double errorRate, int expectedItems)
{
var key = new RedisKey($"{_keyPrefix}{filter}");
await RedisBloomCommands.ReserveBloomFilterAsync(GetDatabase(), key, errorRate, expectedItems);
}
}
}
Usage Example
Inject the helper into a service and call the methods naturally.
private readonly RedisHelper _redis;
public OrderValidator(RedisHelper redisHelper)
{
_redis = redisHelper;
}
public async Task ProcessOrderAsync()
{
await _redis.BloomAddAsync("processed:orders", "ORD-12345");
bool exists = await _redis.BloomContainsAsync("processed:orders", "ORD-12346");
if (!exists)
{
// Process the new order
}
}
Practical Notes on Bloom Filters
A Bloom filter reserves its bit array size based on the initial capacity and error rate supplied at creation time. The space it occupies and the maximum expected entries are fixed from that point.
False positives arise because multiple distinct values can set overlapping bits. As the number of stored elements grows, the probability of bit collisions increases. This is why you should determine the desired false positive rate and estimated cardinality upfront when calling BF.RESERVE. Redis Bloom filter implementations handle the capacity–accuracy trade-off internally, allowing you to declare acceptable error and volume parameters at initialization.