Skip to content

Commit b84f97c

Browse files
committed
Add RedisBloom-compatible BF.* commands for address bloom filters.
Implements BF.RESERVE, BF.ADD, BF.MADD, BF.EXISTS, and BF.MEXISTS using fastbloom so Forja's multichain indexer can use RedVector instead of Redis Stack.
1 parent 5c590b4 commit b84f97c

4 files changed

Lines changed: 368 additions & 5 deletions

File tree

command/src/command.rs

Lines changed: 239 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -655,6 +655,7 @@ fn dbtype(parser: &mut ParsedCommand, db: &Database, dbindex: usize) -> Response
655655
Some(Value::Set(_)) => Response::Data("set".to_owned().into_bytes()),
656656
Some(Value::SortedSet(_)) => Response::Data("zset".to_owned().into_bytes()),
657657
Some(Value::Hash(_)) => Response::Data("hash".to_owned().into_bytes()),
658+
Some(Value::Bloom(_)) => Response::Data("MBbloom--".to_owned().into_bytes()),
658659
None => Response::Data("none".to_owned().into_bytes()),
659660
}
660661
}
@@ -1018,6 +1019,100 @@ fn pfmerge(parser: &ParsedCommand, db: &mut Database, dbindex: usize) -> Respons
10181019
r
10191020
}
10201021

1022+
fn bf_reserve(parser: &mut ParsedCommand, db: &mut Database, dbindex: usize) -> Response {
1023+
validate_arguments_exact!(parser, 4);
1024+
let key = try_validate!(parser.get_vec(1), "ERR invalid key");
1025+
let error_rate = try_validate!(parser.get_f64(2), "ERR invalid error rate");
1026+
let capacity = try_validate!(parser.get_i64(3), "ERR invalid capacity");
1027+
1028+
if capacity <= 0 {
1029+
return Response::Error("ERR invalid bloom filter parameters".to_owned());
1030+
}
1031+
1032+
match db
1033+
.get_or_create(dbindex, &key)
1034+
.bf_reserve(error_rate, capacity as usize)
1035+
{
1036+
Ok(()) => {
1037+
db.key_updated(dbindex, &key);
1038+
Response::Status("OK".to_owned())
1039+
}
1040+
Err(e) => Response::Error(e.to_string()),
1041+
}
1042+
}
1043+
1044+
fn bf_add(parser: &mut ParsedCommand, db: &mut Database, dbindex: usize) -> Response {
1045+
validate_arguments_exact!(parser, 3);
1046+
let key = try_validate!(parser.get_vec(1), "ERR invalid key");
1047+
let item = try_validate!(parser.get_slice(2), "ERR invalid item").to_vec();
1048+
1049+
match db.get_or_create(dbindex, &key).bf_add(&item) {
1050+
Ok(added) => {
1051+
db.key_updated(dbindex, &key);
1052+
Response::Integer(if added { 1 } else { 0 })
1053+
}
1054+
Err(e) => Response::Error(e.to_string()),
1055+
}
1056+
}
1057+
1058+
fn bf_madd(parser: &mut ParsedCommand, db: &mut Database, dbindex: usize) -> Response {
1059+
validate_arguments_gte!(parser, 3);
1060+
let key = try_validate!(parser.get_vec(1), "ERR invalid key");
1061+
let mut items = Vec::with_capacity(parser.argv.len() - 2);
1062+
for i in 2..parser.argv.len() {
1063+
items.push(try_validate!(parser.get_slice(i), "ERR invalid item").to_vec());
1064+
}
1065+
1066+
let value = db.get_or_create(dbindex, &key);
1067+
let mut result = Vec::with_capacity(items.len());
1068+
for item in &items {
1069+
match value.bf_add(item) {
1070+
Ok(added) => result.push(Response::Integer(if added { 1 } else { 0 })),
1071+
Err(e) => return Response::Error(e.to_string()),
1072+
}
1073+
}
1074+
1075+
db.key_updated(dbindex, &key);
1076+
Response::Array(result)
1077+
}
1078+
1079+
fn bf_exists(parser: &mut ParsedCommand, db: &Database, dbindex: usize) -> Response {
1080+
validate_arguments_exact!(parser, 3);
1081+
let key = try_validate!(parser.get_vec(1), "ERR invalid key");
1082+
let item = try_validate!(parser.get_slice(2), "ERR invalid item");
1083+
1084+
match db.get(dbindex, &key) {
1085+
Some(value) => match value.bf_exists(item) {
1086+
Ok(exists) => Response::Integer(if exists { 1 } else { 0 }),
1087+
Err(e) => Response::Error(e.to_string()),
1088+
},
1089+
None => Response::Integer(0),
1090+
}
1091+
}
1092+
1093+
fn bf_mexists(parser: &mut ParsedCommand, db: &Database, dbindex: usize) -> Response {
1094+
validate_arguments_gte!(parser, 3);
1095+
let key = try_validate!(parser.get_vec(1), "ERR invalid key");
1096+
let mut items = Vec::with_capacity(parser.argv.len() - 2);
1097+
for i in 2..parser.argv.len() {
1098+
items.push(try_validate!(parser.get_slice(i), "ERR invalid item").to_vec());
1099+
}
1100+
1101+
match db.get(dbindex, &key) {
1102+
Some(value) => {
1103+
let mut result = Vec::with_capacity(items.len());
1104+
for item in &items {
1105+
match value.bf_exists(item) {
1106+
Ok(exists) => result.push(Response::Integer(if exists { 1 } else { 0 })),
1107+
Err(e) => return Response::Error(e.to_string()),
1108+
}
1109+
}
1110+
Response::Array(result)
1111+
}
1112+
None => Response::Array(items.iter().map(|_| Response::Integer(0)).collect()),
1113+
}
1114+
}
1115+
10211116
fn generic_push(
10221117
parser: &mut ParsedCommand,
10231118
db: &mut Database,
@@ -3081,6 +3176,7 @@ fn object(parser: &mut ParsedCommand, db: &mut Database, dbindex: usize) -> Resp
30813176
ValueHash::ZipList(_) => "ziplist",
30823177
ValueHash::HashMap(_) => "hashtable",
30833178
},
3179+
Value::Bloom(_) => "bloom",
30843180
Value::Nil => return Response::Nil,
30853181
};
30863182
Response::Data(encoding.to_string().into_bytes())
@@ -4249,7 +4345,8 @@ fn command_cmd(parser: &mut ParsedCommand, _db: &Database) -> Response {
42494345
"ttl", "pttl", "persist", "slaveof", "role", "config", "subscribe", "unsubscribe",
42504346
"psubscribe", "punsubscribe", "publish", "pubsub", "watch", "unwatch", "restore",
42514347
"dump", "object", "client", "time", "bitop", "bitcount", "bitpos", "wait", "command",
4252-
"pfadd", "pfcount", "pfmerge",
4348+
"pfadd", "pfcount", "pfmerge", "bf.reserve", "bf.add", "bf.madd", "bf.exists",
4349+
"bf.mexists",
42534350
];
42544351
#[cfg(feature = "vector-search")]
42554352
let mut commands = commands;
@@ -4718,6 +4815,11 @@ fn command_properties(command_name: &str) -> CommandProperties {
47184815
"pfadd" => (-2, wmf, 1, 1, 1),
47194816
"pfcount" => (-2, READONLY, 1, -1, 1),
47204817
"pfmerge" => (-2, wm, 1, -1, 1),
4818+
"bf.reserve" => (4, wmf, 1, 1, 1),
4819+
"bf.add" => (3, wmf, 1, 1, 1),
4820+
"bf.madd" => (-3, wmf, 1, 1, 1),
4821+
"bf.exists" => (3, fr, 1, 1, 1),
4822+
"bf.mexists" => (-3, fr, 1, 1, 1),
47214823
"pfdebug" => (-3, WRITE, 0, 0, 0),
47224824
"latency" => (-2, ars | ls, 0, 0, 0),
47234825
// RediSearch (FT.*) commands (only when vector-search feature is enabled)
@@ -4771,17 +4873,17 @@ fn execute_command(
47714873
let raw_command = try_opt_validate!(parser.get_str(0), "Invalid command");
47724874
let lower_command = raw_command.to_ascii_lowercase();
47734875

4774-
// For FT.* commands, bypass mapped_command to ensure they're allowed
4775-
// Check both lowercase and original case to handle any edge cases
4876+
// For module-style dotted commands, bypass mapped_command to ensure they're allowed.
47764877
let is_ft_command = lower_command.starts_with("ft.");
4878+
let is_bf_command = lower_command.starts_with("bf.");
47774879

47784880
// Debug logging
47794881
if raw_command.to_uppercase().starts_with("FT.") || lower_command.starts_with("ft.") {
47804882
eprintln!("DEBUG: FT command detected: raw='{}', lower='{}', is_ft={}", raw_command, lower_command, is_ft_command);
47814883
}
47824884

4783-
let command_name = if is_ft_command {
4784-
// FT.* commands - use directly, don't go through mapped_command
4885+
let command_name = if is_ft_command || is_bf_command {
4886+
// Module commands - use directly, don't go through mapped_command.
47854887
// Normalize to lowercase for consistency
47864888
&*lower_command
47874889
} else {
@@ -4906,6 +5008,11 @@ fn execute_command(
49065008
"pfadd" => pfadd(parser, db, dbindex),
49075009
"pfcount" => pfcount(parser, db, dbindex),
49085010
"pfmerge" => pfmerge(parser, db, dbindex),
5011+
"bf.reserve" => bf_reserve(parser, db, dbindex),
5012+
"bf.add" => bf_add(parser, db, dbindex),
5013+
"bf.madd" => bf_madd(parser, db, dbindex),
5014+
"bf.exists" => bf_exists(parser, db, dbindex),
5015+
"bf.mexists" => bf_mexists(parser, db, dbindex),
49095016
"pfselftest" => pfselftest(parser, db, dbindex),
49105017
"pfdebug" => pfdebug(parser, db, dbindex),
49115018
"exists" => exists(parser, db, dbindex),
@@ -5805,6 +5912,133 @@ mod test_command {
58055912
);
58065913
}
58075914

5915+
#[test]
5916+
fn bf_reserve_add_exists_command() {
5917+
let mut db = Database::new(Config::new(Logger::new(Level::Warning)));
5918+
assert_eq!(
5919+
command(
5920+
parser!(b"BF.RESERVE wallet_bloom 0.01 1000"),
5921+
&mut db,
5922+
&mut Client::mock()
5923+
)
5924+
.unwrap(),
5925+
Response::Status("OK".to_owned())
5926+
);
5927+
assert_eq!(
5928+
command(
5929+
parser!(b"BF.EXISTS wallet_bloom 0x742d35Cc6634C0532925a3b8D4C9db96C4b4d8b6"),
5930+
&mut db,
5931+
&mut Client::mock()
5932+
)
5933+
.unwrap(),
5934+
Response::Integer(0)
5935+
);
5936+
assert_eq!(
5937+
command(
5938+
parser!(b"BF.ADD wallet_bloom 0x742d35Cc6634C0532925a3b8D4C9db96C4b4d8b6"),
5939+
&mut db,
5940+
&mut Client::mock()
5941+
)
5942+
.unwrap(),
5943+
Response::Integer(1)
5944+
);
5945+
assert_eq!(
5946+
command(
5947+
parser!(b"BF.ADD wallet_bloom 0x742d35Cc6634C0532925a3b8D4C9db96C4b4d8b6"),
5948+
&mut db,
5949+
&mut Client::mock()
5950+
)
5951+
.unwrap(),
5952+
Response::Integer(0)
5953+
);
5954+
assert_eq!(
5955+
command(
5956+
parser!(b"BF.EXISTS wallet_bloom 0x742d35Cc6634C0532925a3b8D4C9db96C4b4d8b6"),
5957+
&mut db,
5958+
&mut Client::mock()
5959+
)
5960+
.unwrap(),
5961+
Response::Integer(1)
5962+
);
5963+
}
5964+
5965+
#[test]
5966+
fn bf_madd_and_mexists_command() {
5967+
let mut db = Database::new(Config::new(Logger::new(Level::Warning)));
5968+
assert_eq!(
5969+
command(
5970+
parser!(b"BF.MADD wallet_bloom evm_address tron_address evm_address"),
5971+
&mut db,
5972+
&mut Client::mock()
5973+
)
5974+
.unwrap(),
5975+
Response::Array(vec![
5976+
Response::Integer(1),
5977+
Response::Integer(1),
5978+
Response::Integer(0),
5979+
])
5980+
);
5981+
assert_eq!(
5982+
command(
5983+
parser!(b"BF.MEXISTS wallet_bloom evm_address missing tron_address"),
5984+
&mut db,
5985+
&mut Client::mock()
5986+
)
5987+
.unwrap(),
5988+
Response::Array(vec![
5989+
Response::Integer(1),
5990+
Response::Integer(0),
5991+
Response::Integer(1),
5992+
])
5993+
);
5994+
}
5995+
5996+
#[test]
5997+
fn bf_exists_missing_key_returns_zero() {
5998+
let mut db = Database::new(Config::new(Logger::new(Level::Warning)));
5999+
assert_eq!(
6000+
command(
6001+
parser!(b"BF.EXISTS missing_bloom value"),
6002+
&mut db,
6003+
&mut Client::mock()
6004+
)
6005+
.unwrap(),
6006+
Response::Integer(0)
6007+
);
6008+
assert_eq!(
6009+
command(
6010+
parser!(b"BF.MEXISTS missing_bloom a b"),
6011+
&mut db,
6012+
&mut Client::mock()
6013+
)
6014+
.unwrap(),
6015+
Response::Array(vec![Response::Integer(0), Response::Integer(0)])
6016+
);
6017+
}
6018+
6019+
#[test]
6020+
fn bf_wrong_type_and_existing_key_errors() {
6021+
let mut db = Database::new(Config::new(Logger::new(Level::Warning)));
6022+
assert_eq!(
6023+
command(parser!(b"SET key value"), &mut db, &mut Client::mock()).unwrap(),
6024+
Response::Status("OK".to_owned())
6025+
);
6026+
assert!(
6027+
command(parser!(b"BF.ADD key value"), &mut db, &mut Client::mock())
6028+
.unwrap()
6029+
.is_error()
6030+
);
6031+
assert!(
6032+
command(
6033+
parser!(b"BF.RESERVE key 0.01 1000"),
6034+
&mut db,
6035+
&mut Client::mock()
6036+
)
6037+
.unwrap()
6038+
.is_error()
6039+
);
6040+
}
6041+
58086042
#[test]
58096043
fn pfcount1_command() {
58106044
let mut db = Database::new(Config::new(Logger::new(Level::Warning)));

database/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ version = "0.1.0"
55

66
[dependencies]
77
crc64 = "0.2"
8+
fastbloom = { version = "0.17", default-features = false }
89
rand = "0.3"
910
rehashinghashmap = "0.1"
1011
skiplist = "0.3"

database/src/bloom.rs

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
use fastbloom::BloomFilter;
2+
3+
use crate::error::OperationError;
4+
5+
const DEFAULT_ERROR_RATE: f64 = 0.01;
6+
const DEFAULT_CAPACITY: usize = 100;
7+
8+
#[derive(PartialEq, Debug, Clone)]
9+
pub struct ValueBloom {
10+
filter: BloomFilter,
11+
error_rate: f64,
12+
capacity: usize,
13+
insertions: usize,
14+
}
15+
16+
impl ValueBloom {
17+
pub fn new(error_rate: f64, capacity: usize) -> Result<Self, OperationError> {
18+
if !(error_rate > 0.0 && error_rate < 1.0) || capacity == 0 {
19+
return Err(OperationError::ValueError(
20+
"ERR invalid bloom filter parameters".to_owned(),
21+
));
22+
}
23+
24+
Ok(ValueBloom {
25+
filter: BloomFilter::with_false_pos(error_rate).expected_items(capacity),
26+
error_rate,
27+
capacity,
28+
insertions: 0,
29+
})
30+
}
31+
32+
pub fn default_filter() -> Self {
33+
ValueBloom::new(DEFAULT_ERROR_RATE, DEFAULT_CAPACITY)
34+
.expect("default bloom filter parameters are valid")
35+
}
36+
37+
pub fn add(&mut self, item: &[u8]) -> bool {
38+
let may_have_existed = self.filter.insert(item);
39+
if !may_have_existed {
40+
self.insertions += 1;
41+
}
42+
!may_have_existed
43+
}
44+
45+
pub fn exists(&self, item: &[u8]) -> bool {
46+
self.filter.contains(item)
47+
}
48+
49+
pub fn capacity(&self) -> usize {
50+
self.capacity
51+
}
52+
53+
pub fn error_rate(&self) -> f64 {
54+
self.error_rate
55+
}
56+
57+
pub fn insertions(&self) -> usize {
58+
self.insertions
59+
}
60+
61+
pub fn memory_bytes(&self) -> u64 {
62+
(self.filter.as_slice().len() * std::mem::size_of::<u64>()) as u64
63+
}
64+
65+
pub fn debug_object(&self) -> String {
66+
format!(
67+
"Value at:0x0000000000 refcount:0 encoding:bloom capacity:{} error_rate:{} \
68+
insertions:{} bits:{} hashes:{}",
69+
self.capacity,
70+
self.error_rate,
71+
self.insertions,
72+
self.filter.num_bits(),
73+
self.filter.num_hashes()
74+
)
75+
}
76+
}

0 commit comments

Comments
 (0)