Bloom Filters · Valkey

Bloom Filters

Description

In Valkey, the bloom filter data type / commands are implemented in the valkey-bloom module which is an official valkey module compatible with versions 8.0 and above. Users will need to load this module onto their valkey server in order to use this feature.

Bloom filters are a space efficient probabilistic data structure that allows adding elements and checking whether elements exist. False positives are possible where a filter incorrectly indicates that an element exists, even though it was not added. However, Bloom Filters guarantee that false negatives (incorrectly indicating that an element does not exist, even though it was added) do not occur.

Basic Bloom commands

See the complete list of bloom filter commands.

Common use cases for bloom filters

Bloom filters can help e-commerce sites, streaming services, advertising networks, or marketing platforms answer the following questions:

Example: For each user, use a Bloom filter to store all the products they have purchased. The recommendation engine can then suggest a new product and check if it is present in the user’s Bloom filter.

Fraud detection

Bloom filters can be used to answer the question, “Has this card been flagged as stolen?”. To do this, use a bloom filter that contains cards reported as stolen. When a card is used, check whether it is present in the bloom filter. If the card is not found, it means it is not marked as stolen. If the card is present in the filter, a check can be made against the main database, or the purchase can be denied.

Filtering Spam / Harmful Content

Bloom filters provide an efficient way to screen content for potential threats and harmful material. Here’s how they can be effectively used:

Example: Bloom filters can answer the question “is a URL malicious?”. Any URL inputted would be checked against a malicious URL bloom filter.

Example: Bloom filters can answer the question is this content harmful or spam. Create a bloom filter that contains spam email addresses or spam phone numbers. When an email or text is received then check if the number or email is present in the bloom filter.

Check if a username is taken

Bloom filters can answer the question: Has this username/email/domain name/slug already been used?

In this username example, we can use a Bloom filter to track every username that has signed up. When a new user attempts to sign up with their desired username, the app checks if the username exists in the Bloom filter.

Tutorial

There are two ways to run valkey-bloom: - Docker: Follow the steps here for using the valkey bundle - Build from source: Follow the build instructions

Usage example with CLI

127.0.0.1:6379> BF.RESERVE usernames 0.001 10000
OK
# Add one user to the bloom filter

127.0.0.1:6379> BF.ADD usernames johnsmith
(integer) 1

# Add multiple users at once

127.0.0.1:6379> BF.MADD usernames JaneDoe valkeyFan bloomEnjoyer
1) (integer) 1
2) (integer) 1
3) (integer) 1
# Check if as single user has been added

127.0.0.1:6379> BF.EXISTS usernames johnsmith
(integer) 1
127.0.0.1:6379> BF.EXISTS usernames fake_user
(integer) 0

# Check if multiple users are present in the bloomfilter at once

127.0.0.1:6379> BF.MEXISTS usernames johnsmith fake_user JaneDoe
1) (integer) 1
2) (integer) 0
3) (integer) 1
127.0.0.1:6379> BF.CARD usernames
(integer) 4
127.0.0.1:6379> BF.INFO usernames
 1) Capacity
 2) (integer) 10000
 3) Size
 4) (integer) 23912
 5) Number of filters
 6) (integer) 1
 7) Number of items inserted
 8) (integer) 4
 9) Error rate
10) "0.001"
11) Expansion rate
12) (integer) 2
13) Tightening ratio
14) "0.5"
15) Max scaled capacity
16) (integer) 26214300
127.0.0.1:6379> BF.RESERVE limited_users 0.00001 1000 NONSCALING
OK
127.0.0.1:6379> BF.RESERVE expanding_users 0.001 10000 EXPANSION 4
OK

Scaling and non scaling bloom filters

The bloom filter data type can act either as a “scaling bloom filter” or “non scaling bloom filter” depending on user configuration.

The difference between scaling and non scaling bloom filters is that scaling bloom filters do not have a fixed capacity, instead they can grow. Non-scaling bloom filters will have a fixed capacity, meaning only a fixed number of items can be inserted to it. Scaling bloom filters consist of a vector of “Sub filters” with length >= 1, while non scaling will only contain 1 sub filter.

When a scaling bloom filter reaches its capacity, adding a new unique item will trigger a scale out and a new sub filter is created and added to the vector of sub filters. This new sub filter will have a larger capacity (previous bloom filter’s capacity * expansion rate of the bloom object).

After a non scaling bloom filter reaches its capacity, if a user tries to add a new unique item, an error will be returned

The expansion rate is the rate that a scaling bloom filter’s capacity is increased by upon scale out. For example, we have a bloom filter with capacity 100 at creation with an expansion rate of 2. After adding 101 unique items, it will scale out and create a new sub filter with capacity 200. Then, after adding 200 more unique items (301 items total), a new sub filter of capacity 400 is added upon scale out and so on.

When should you use scaling vs non-scaling filters

If the capacity (number of items we want to add) is known and fixed, using a non-scaling bloom filter is preferred. Likewise the reverse case, if the capacity is unknown / dynamically calculated, using a scaling bloom filters is ideal.

There are a few benefits for using non scaling filters. A non scaling filter will have better performance than a filter that has scaled out several times (e.g. > 100). Also, non scaling filters in general use less memory for a scaling filter that has scaled out several times to hold the same capacity.

However, to ensure you do not hit any capacity related errors, and want use-as-you-go capacity, scaling is better.

Bloom properties

Advanced Properties

The following two properties can be specified in the BF.INSERT command:

Default bloom properties

These are the default bloom properties along with the commands and configs which allow customizing.

Property Default Value Command Name Configuration name
Capacity 100 BF.INSERT, BF.RESERVE BF.BLOOM-CAPACITY
False Positive Rate 0.01 BF.INSERT, BF.RESERVE BF.BLOOM-FP-RATE
Scaling / Non Scaling Scaling BF.INSERT, BF.RESERVE BF.BLOOM-EXPANSION
Expansion Rate 2 BF.INSERT, BF.RESERVE BF.BLOOM-EXPANSION
Tightening Ratio 0.5 BF.INSERT BF.BLOOM-TIGHTENING-RATIO
Seed Random Seed BF.INSERT BF.BLOOM-USE-RANDOM-SEED

Since bloom filters have a default expansion of 2, this means any default creation as a result of BF.ADD, BF.MADD, BF.INSERT will be a scalable bloom filter. Users can create a non scaling bloom filter using BF.RESERVE <filter-name> <error-rate> <capacity> NONSCALING or by specifying NONSCALING in BF.INSERT. Additionally, the other default properties of a bloom filter creation can be seen in the table above and BF.INFO command response below. These default properties can be configured through configs on the bloom module.

Example of default bloom filter information:

127.0.0.1:6379> BF.ADD default_filter item
1
127.0.0.1:6379> BF.INFO default_filter
 1) Capacity
 2) (integer) 100
 3) Size
 4) (integer) 384
 5) Number of filters
 6) (integer) 1
 7) Number of items inserted
 8) (integer) 2
 9) Error rate
10) "0.01"
11) Expansion rate
12) (integer) 2
13) Tightening ratio
14) "0.5"
15) Max scaled capacity
16) (integer) 26214300

When to adjust default configurations

Adjusting the default configurations below can be used to update the behavior of the default bloom object created and are beneficial in different scenarios:

You can modify these default values using the CONFIG SET command. However, note that the effect of the configuration change is only applicable to the bloom objects created after the configuration change where the property is not specified through the command already.

Example usage of changing all the different properties:

127.0.0.1:6379> CONFIG SET bf.bloom-fp-rate 0.001
127.0.0.1:6379> CONFIG SET bf.bloom-capacity 1000
127.0.0.1:6379> CONFIG SET bf.bloom-expansion 4
127.0.0.1:6379> CONFIG SET bf.bloom-tightening-ratio 0.6
127.0.0.1:6379> CONFIG SET bf.bloom-use-random-seed false

Memory usage limit

The bf.bloom-memory-usage-limit configuration (default 128MB) controls the maximum memory that a single bloom filter can use:

Example usage of increasing the limit:

127.0.0.1:6379> CONFIG SET bf.bloom-memory-usage-limit 268435456

Having a limit on the max memory per bloom object prevents them from growing unbounded, thus maintaining server performance during serialization.

If your use case requires bloom filters with capacity beyond what this limit supports, you can increase this configuration value. However, be aware it can impact overall system performance and memory availability for other operations.

When a bloom filter reaches this memory limit, any operation that would cause it to exceed the limit will fail with an error message indicating that the memory limit would be exceeded.

Example of error returned:

127.0.0.1:6379> bf.add full_filter ne_item
1) (error) ERR operation exceeds bloom object memory limit

Performance

The bloom commands which involve adding items or checking the existence of items have a time complexity of O(N * K) where N is the number of hash functions used by the bloom filter and K is the number of elements being inserted. This means that both BF.ADD and BF.EXISTS are both O(N) as they only operate on one item.

In case of scalable bloom filters, with every scale out, we increase the number of checks (using hash functions of each sub filter) performed during any add / exists operation. For this reason, it is recommended that users choose a capacity and expansion rate after evaluating the use case / workload to avoid several scale outs and reduce the number of checks.

The other bloom filter commands are O(1) time complexity: BF.CARD, BF.INFO, BF.RESERVE, and BF.INSERT (when no items are provided).

Monitoring

To check the server’s overall bloom filter metrics, you can use the INFO BF or the INFO MODULES command.

Example of INFO BF calls in different scenarios:

127.0.0.1:6379> INFO BF
# bf_bloom_core_metrics

bf_bloom_total_memory_bytes:0
bf_bloom_num_objects:0
bf_bloom_num_filters_across_objects:0
bf_bloom_num_items_across_objects:0
bf_bloom_capacity_across_objects:0

# bf_bloom_defrag_metrics

bf_bloom_defrag_hits:0
bf_bloom_defrag_misses:0
127.0.0.1:6379> bf.add key value
(integer) 1
127.0.0.1:6379> info bf
# bf_bloom_core_metrics

bf_bloom_total_memory_bytes:384
bf_bloom_num_objects:1
bf_bloom_num_filters_across_objects:1
bf_bloom_num_items_across_objects:1
bf_bloom_capacity_across_objects:100

# bf_bloom_defrag_metrics

bf_bloom_defrag_hits:0
bf_bloom_defrag_misses:0

Bloom filter core metrics

Bloom filter defrag metrics

Handling Large Bloom Filters

There are two notable validations bloom filters faces.

  1. Memory Usage:

    The memory usage limit per bloom filter by default is defined by the BF.BLOOM-MEMORY-USAGE-LIMIT module configuration which has a default value of 128 MB. If a command results in a creation / scale out causing the overall memory usage to exceed this limit, the command is rejected. This config is modifiable and can be increased as needed.

The BF.INFO command’s SIZE field can be used to find out the current size of a bloom filter.

127.0.0.1:6379> BF.INFO validate_scale_valid SIZE
(integer) 384
  1. Number of sub filters (in case of scalable bloom filters):

    When a bloom filter scales out, a new sub filter is added. The limit on the number of sub filters depends on the false positive rate and tightening ratio. Each sub filter has a stricter false positive, and this is controlled by the tightening ratio. If a command attempting a scale out results in the sub filter reaching a false positive of 0, the command is rejected.

You can use VALIDATESCALETO as an optional arg of BF.INSERT to help determine whether the bloom filter can scale out to the reach the specified capacity without hitting either limits mentioned above. It will reject the command otherwise.

As seen below, when trying to create a bloom filter with a capacity that cannot be achieved through scale outs (given the memory limits), the command is rejected. However, if the capacity can be achieved through scale out (even with the limits), then the creation of the bloom filter will succeed.

Example:

127.0.0.1:6379> BF.INSERT validate_scale_fail VALIDATESCALETO 26214301
(error) ERR provided VALIDATESCALETO causes bloom object to exceed memory limit
127.0.0.1:6379> BF.INSERT validate_scale_valid VALIDATESCALETO 26214300
[]

The BF.INFO command’s MAXSCALEDCAPACITY field can be used to find out the maximum capacity that the scalable bloom filter can expand to hold.

127.0.0.1:6379> BF.INFO validate_scale_valid MAXSCALEDCAPACITY
(integer) 26214300