From 723829ff2ba3a65683296e3a50954126763ec00b Mon Sep 17 00:00:00 2001 From: Leo Date: Fri, 22 Sep 2023 15:58:20 +0800 Subject: [PATCH] Add the support of the BF.INSERT command (#1768) --- src/commands/cmd_bloom_filter.cc | 136 +++++++++++++++++++-- src/types/redis_bloom_chain.cc | 23 ++-- src/types/redis_bloom_chain.h | 20 ++- tests/gocase/unit/type/bloom/bloom_test.go | 94 ++++++++++++-- 4 files changed, 242 insertions(+), 31 deletions(-) diff --git a/src/commands/cmd_bloom_filter.cc b/src/commands/cmd_bloom_filter.cc index ded318572d6..611d1b9d9bd 100644 --- a/src/commands/cmd_bloom_filter.cc +++ b/src/commands/cmd_bloom_filter.cc @@ -24,6 +24,18 @@ #include "server/server.h" #include "types/redis_bloom_chain.h" +namespace { + +constexpr const char *errBadErrorRate = "Bad error rate"; +constexpr const char *errBadCapacity = "Bad capacity"; +constexpr const char *errBadExpansion = "Bad expansion"; +constexpr const char *errInvalidErrorRate = "error rate should be between 0 and 1"; +constexpr const char *errInvalidCapacity = "capacity should be larger than 0"; +constexpr const char *errInvalidExpansion = "expansion should be greater or equal to 1"; +constexpr const char *errNonscalingButExpand = "nonscaling filters cannot expand"; +constexpr const char *errFilterFull = "ERR nonscaling filter is full"; +} // namespace + namespace redis { class CommandBFReserve : public Commander { @@ -31,20 +43,20 @@ class CommandBFReserve : public Commander { Status Parse(const std::vector &args) override { auto parse_error_rate = ParseFloat(args[2]); if (!parse_error_rate) { - return {Status::RedisParseErr, errValueIsNotFloat}; + return {Status::RedisParseErr, errBadErrorRate}; } error_rate_ = *parse_error_rate; if (error_rate_ >= 1 || error_rate_ <= 0) { - return {Status::RedisParseErr, "error rate should be between 0 and 1"}; + return {Status::RedisParseErr, errInvalidErrorRate}; } auto parse_capacity = ParseInt(args[3], 10); if (!parse_capacity) { - return {Status::RedisParseErr, errValueNotInteger}; + return {Status::RedisParseErr, errBadCapacity}; } capacity_ = *parse_capacity; if (capacity_ <= 0) { - return {Status::RedisParseErr, "capacity should be larger than 0"}; + return {Status::RedisParseErr, errInvalidCapacity}; } CommandParser parser(args, 4); @@ -56,9 +68,13 @@ class CommandBFReserve : public Commander { expansion_ = 0; } else if (parser.EatEqICase("expansion")) { has_expansion = true; - expansion_ = GET_OR_RET(parser.TakeInt()); + auto parse_expansion = parser.TakeInt(); + if (!parse_expansion.IsOK()) { + return {Status::RedisParseErr, errBadExpansion}; + } + expansion_ = parse_expansion.GetValue(); if (expansion_ < 1) { - return {Status::RedisParseErr, "expansion should be greater or equal to 1"}; + return {Status::RedisParseErr, errInvalidExpansion}; } } else { return {Status::RedisParseErr, errInvalidSyntax}; @@ -66,7 +82,7 @@ class CommandBFReserve : public Commander { } if (is_nonscaling && has_expansion) { - return {Status::RedisParseErr, "nonscaling filters cannot expand"}; + return {Status::RedisParseErr, errNonscalingButExpand}; } return Commander::Parse(args); @@ -103,7 +119,7 @@ class CommandBFAdd : public Commander { *output = redis::Integer(0); break; case BloomFilterAddResult::kFull: - *output = redis::Error("ERR nonscaling filter is full"); + *output = redis::Error(errFilterFull); break; } return Status::OK(); @@ -136,7 +152,103 @@ class CommandBFMAdd : public Commander { *output += redis::Integer(0); break; case BloomFilterAddResult::kFull: - *output += redis::Error("ERR nonscaling filter is full"); + *output += redis::Error(errFilterFull); + break; + } + } + return Status::OK(); + } + + private: + std::vector items_; +}; + +class CommandBFInsert : public Commander { + public: + Status Parse(const std::vector &args) override { + CommandParser parser(args, 2); + bool is_nonscaling = false; + bool has_expansion = false; + bool has_items = false; + while (parser.Good()) { + if (parser.EatEqICase("capacity")) { + auto parse_capacity = parser.TakeInt(); + if (!parse_capacity.IsOK()) { + return {Status::RedisParseErr, errBadCapacity}; + } + insert_options_.capacity = parse_capacity.GetValue(); + if (insert_options_.capacity <= 0) { + return {Status::RedisParseErr, errInvalidCapacity}; + } + } else if (parser.EatEqICase("error")) { + auto parse_error_rate = parser.TakeFloat(); + if (!parse_error_rate.IsOK()) { + return {Status::RedisParseErr, errBadErrorRate}; + } + insert_options_.error_rate = parse_error_rate.GetValue(); + if (insert_options_.error_rate >= 1 || insert_options_.error_rate <= 0) { + return {Status::RedisParseErr, errInvalidErrorRate}; + } + } else if (parser.EatEqICase("nocreate")) { + insert_options_.auto_create = false; + } else if (parser.EatEqICase("nonscaling")) { + is_nonscaling = true; + insert_options_.expansion = 0; + } else if (parser.EatEqICase("expansion")) { + has_expansion = true; + auto parse_expansion = parser.TakeInt(); + if (!parse_expansion.IsOK()) { + return {Status::RedisParseErr, errBadExpansion}; + } + insert_options_.expansion = parse_expansion.GetValue(); + if (insert_options_.expansion < 1) { + return {Status::RedisParseErr, errInvalidExpansion}; + } + } else if (parser.EatEqICase("items")) { + has_items = true; + break; + } else { + return {Status::RedisParseErr, errInvalidSyntax}; + } + } + + if (is_nonscaling && has_expansion) { + return {Status::RedisParseErr, errNonscalingButExpand}; + } + + if (not has_items) { + return {Status::RedisParseErr, errInvalidSyntax}; + } + + while (parser.Good()) { + items_.emplace_back(GET_OR_RET(parser.TakeStr())); + } + + if (items_.size() == 0) { + return {Status::RedisParseErr, "num of items should be greater than 0"}; + } + + return Commander::Parse(args); + } + + Status Execute(Server *svr, Connection *conn, std::string *output) override { + redis::BloomChain bloom_db(svr->storage, conn->GetNamespace()); + std::vector rets(items_.size(), BloomFilterAddResult::kOk); + auto s = bloom_db.InsertCommon(args_[1], items_, insert_options_, &rets); + if (s.IsNotFound()) return {Status::RedisExecErr, "key is not found"}; + if (!s.ok()) return {Status::RedisExecErr, s.ToString()}; + + *output = redis::MultiLen(items_.size()); + for (size_t i = 0; i < items_.size(); ++i) { + switch (rets[i]) { + case BloomFilterAddResult::kOk: + *output += redis::Integer(1); + break; + case BloomFilterAddResult::kExist: + *output += redis::Integer(0); + break; + case BloomFilterAddResult::kFull: + *output += redis::Error(errFilterFull); break; } } @@ -144,7 +256,8 @@ class CommandBFMAdd : public Commander { } private: - std::vector items_; + std::vector items_; + BloomFilterInsertOptions insert_options_; }; class CommandBFExists : public Commander { @@ -184,7 +297,7 @@ class CommandBFMExists : public Commander { } private: - std::vector items_; + std::vector items_; }; class CommandBFInfo : public Commander { @@ -277,6 +390,7 @@ class CommandBFCard : public Commander { REDIS_REGISTER_COMMANDS(MakeCmdAttr("bf.reserve", -4, "write", 1, 1, 1), MakeCmdAttr("bf.add", 3, "write", 1, 1, 1), MakeCmdAttr("bf.madd", -3, "write", 1, 1, 1), + MakeCmdAttr("bf.insert", -4, "write", 1, 1, 1), MakeCmdAttr("bf.exists", 3, "read-only", 1, 1, 1), MakeCmdAttr("bf.mexists", -3, "read-only", 1, 1, 1), MakeCmdAttr("bf.info", -2, "read-only", 1, 1, 1), diff --git a/src/types/redis_bloom_chain.cc b/src/types/redis_bloom_chain.cc index 592b6eec430..aa5131c7194 100644 --- a/src/types/redis_bloom_chain.cc +++ b/src/types/redis_bloom_chain.cc @@ -60,7 +60,7 @@ rocksdb::Status BloomChain::getBFDataList(const std::vector &bf_key return rocksdb::Status::OK(); } -void BloomChain::getItemHashList(const std::vector &items, std::vector *item_hash_list) { +void BloomChain::getItemHashList(const std::vector &items, std::vector *item_hash_list) { item_hash_list->reserve(items.size()); for (const auto &item : items) { item_hash_list->push_back(BlockSplitBloomFilter::Hash(item.data(), item.size())); @@ -132,23 +132,31 @@ rocksdb::Status BloomChain::Reserve(const Slice &user_key, uint32_t capacity, do return createBloomChain(ns_key, error_rate, capacity, expansion, &bloom_chain_metadata); } -rocksdb::Status BloomChain::Add(const Slice &user_key, const Slice &item, BloomFilterAddResult *ret) { +rocksdb::Status BloomChain::Add(const Slice &user_key, const std::string &item, BloomFilterAddResult *ret) { std::vector tmp{BloomFilterAddResult::kOk}; rocksdb::Status s = MAdd(user_key, {item}, &tmp); *ret = tmp[0]; return s; } -rocksdb::Status BloomChain::MAdd(const Slice &user_key, const std::vector &items, +rocksdb::Status BloomChain::MAdd(const Slice &user_key, const std::vector &items, std::vector *rets) { + BloomFilterInsertOptions insert_options; + return InsertCommon(user_key, items, insert_options, rets); +} + +rocksdb::Status BloomChain::InsertCommon(const Slice &user_key, const std::vector &items, + const BloomFilterInsertOptions &insert_options, + std::vector *rets) { std::string ns_key = AppendNamespacePrefix(user_key); LockGuard guard(storage_->GetLockManager(), ns_key); BloomChainMetadata metadata; rocksdb::Status s = getBloomChainMetadata(ns_key, &metadata); - if (s.IsNotFound()) { - s = createBloomChain(ns_key, kBFDefaultErrorRate, kBFDefaultInitCapacity, kBFDefaultExpansion, &metadata); + if (s.IsNotFound() && insert_options.auto_create) { + s = createBloomChain(ns_key, insert_options.error_rate, insert_options.capacity, insert_options.expansion, + &metadata); } if (!s.ok()) return s; @@ -207,14 +215,15 @@ rocksdb::Status BloomChain::MAdd(const Slice &user_key, const std::vector return storage_->Write(storage_->DefaultWriteOptions(), batch->GetWriteBatch()); } -rocksdb::Status BloomChain::Exists(const Slice &user_key, const Slice &item, bool *exist) { +rocksdb::Status BloomChain::Exists(const Slice &user_key, const std::string &item, bool *exist) { std::vector tmp{false}; rocksdb::Status s = MExists(user_key, {item}, &tmp); *exist = tmp[0]; return s; } -rocksdb::Status BloomChain::MExists(const Slice &user_key, const std::vector &items, std::vector *exists) { +rocksdb::Status BloomChain::MExists(const Slice &user_key, const std::vector &items, + std::vector *exists) { std::string ns_key = AppendNamespacePrefix(user_key); BloomChainMetadata metadata; diff --git a/src/types/redis_bloom_chain.h b/src/types/redis_bloom_chain.h index b2d537749ae..7190a6ffe58 100644 --- a/src/types/redis_bloom_chain.h +++ b/src/types/redis_bloom_chain.h @@ -45,6 +45,13 @@ enum class BloomFilterAddResult { kFull, }; +struct BloomFilterInsertOptions { + double error_rate = kBFDefaultErrorRate; + uint32_t capacity = kBFDefaultInitCapacity; + uint16_t expansion = kBFDefaultExpansion; + bool auto_create = true; +}; + struct BloomFilterInfo { uint32_t capacity; uint32_t bloom_bytes; @@ -57,10 +64,13 @@ class BloomChain : public Database { public: BloomChain(engine::Storage *storage, const std::string &ns) : Database(storage, ns) {} rocksdb::Status Reserve(const Slice &user_key, uint32_t capacity, double error_rate, uint16_t expansion); - rocksdb::Status Add(const Slice &user_key, const Slice &item, BloomFilterAddResult *ret); - rocksdb::Status MAdd(const Slice &user_key, const std::vector &items, std::vector *rets); - rocksdb::Status Exists(const Slice &user_key, const Slice &item, bool *exist); - rocksdb::Status MExists(const Slice &user_key, const std::vector &items, std::vector *exists); + rocksdb::Status Add(const Slice &user_key, const std::string &item, BloomFilterAddResult *ret); + rocksdb::Status MAdd(const Slice &user_key, const std::vector &items, + std::vector *rets); + rocksdb::Status InsertCommon(const Slice &user_key, const std::vector &items, + const BloomFilterInsertOptions &insert_options, std::vector *rets); + rocksdb::Status Exists(const Slice &user_key, const std::string &item, bool *exist); + rocksdb::Status MExists(const Slice &user_key, const std::vector &items, std::vector *exists); rocksdb::Status Info(const Slice &user_key, BloomFilterInfo *info); private: @@ -68,7 +78,7 @@ class BloomChain : public Database { std::string getBFKey(const Slice &ns_key, const BloomChainMetadata &metadata, uint16_t filters_index); void getBFKeyList(const Slice &ns_key, const BloomChainMetadata &metadata, std::vector *bf_key_list); rocksdb::Status getBFDataList(const std::vector &bf_key_list, std::vector *bf_data_list); - static void getItemHashList(const std::vector &items, std::vector *item_hash_list); + static void getItemHashList(const std::vector &items, std::vector *item_hash_list); rocksdb::Status createBloomChain(const Slice &ns_key, double error_rate, uint32_t capacity, uint16_t expansion, BloomChainMetadata *metadata); diff --git a/tests/gocase/unit/type/bloom/bloom_test.go b/tests/gocase/unit/type/bloom/bloom_test.go index 153296d22ac..ebfd952e08f 100644 --- a/tests/gocase/unit/type/bloom/bloom_test.go +++ b/tests/gocase/unit/type/bloom/bloom_test.go @@ -44,16 +44,16 @@ func TestBloom(t *testing.T) { t.Run("Reserve a bloom filter with wrong error_rate", func(t *testing.T) { require.NoError(t, rdb.Del(ctx, key).Err()) - require.ErrorContains(t, rdb.Do(ctx, "bf.reserve", key, "abc", "1000").Err(), "ERR value is not a valid float") + require.ErrorContains(t, rdb.Do(ctx, "bf.reserve", key, "abc", "1000").Err(), "ERR Bad error rate") require.ErrorContains(t, rdb.Do(ctx, "bf.reserve", key, "-0.03", "1000").Err(), "ERR error rate should be between 0 and 1") require.ErrorContains(t, rdb.Do(ctx, "bf.reserve", key, "1", "1000").Err(), "ERR error rate should be between 0 and 1") }) t.Run("Reserve a bloom filter with wrong capacity", func(t *testing.T) { require.NoError(t, rdb.Del(ctx, key).Err()) - require.ErrorContains(t, rdb.Do(ctx, "bf.reserve", key, "0.01", "qwe").Err(), "ERR value is not an integer") + require.ErrorContains(t, rdb.Do(ctx, "bf.reserve", key, "0.01", "qwe").Err(), "ERR Bad capacity") // capacity stored in uint32_t, if input is negative, the parser will make an error. - require.ErrorContains(t, rdb.Do(ctx, "bf.reserve", key, "0.01", "-1000").Err(), "ERR value is not an integer or out of range") + require.ErrorContains(t, rdb.Do(ctx, "bf.reserve", key, "0.01", "-1000").Err(), "ERR Bad capacity") require.ErrorContains(t, rdb.Do(ctx, "bf.reserve", key, "0.01", "0").Err(), "ERR capacity should be larger than 0") }) @@ -69,12 +69,12 @@ func TestBloom(t *testing.T) { t.Run("Reserve a bloom filter with wrong expansion", func(t *testing.T) { require.NoError(t, rdb.Del(ctx, key).Err()) - require.ErrorContains(t, rdb.Do(ctx, "bf.reserve", key, "0.01", "1000", "expansion").Err(), "ERR no more item to parse") + require.ErrorContains(t, rdb.Do(ctx, "bf.reserve", key, "0.01", "1000", "expansion").Err(), "Bad expansion") require.ErrorContains(t, rdb.Do(ctx, "bf.reserve", key, "0.01", "1000", "expansion", "0").Err(), "ERR expansion should be greater or equal to 1") - require.ErrorContains(t, rdb.Do(ctx, "bf.reserve", key, "0.01", "1000", "expansion", "asd").Err(), "ERR not started as an integer") - require.ErrorContains(t, rdb.Do(ctx, "bf.reserve", key, "0.01", "1000", "expansion", "-1").Err(), "ERR out of range of integer type") - require.ErrorContains(t, rdb.Do(ctx, "bf.reserve", key, "0.01", "1000", "expansion", "1.5").Err(), "ERR encounter non-integer characters") - require.ErrorContains(t, rdb.Do(ctx, "bf.reserve", key, "0.01", "1000", "expansion", "123asd").Err(), "ERR encounter non-integer characters") + require.ErrorContains(t, rdb.Do(ctx, "bf.reserve", key, "0.01", "1000", "expansion", "asd").Err(), "ERR Bad expansion") + require.ErrorContains(t, rdb.Do(ctx, "bf.reserve", key, "0.01", "1000", "expansion", "-1").Err(), "ERR Bad expansion") + require.ErrorContains(t, rdb.Do(ctx, "bf.reserve", key, "0.01", "1000", "expansion", "1.5").Err(), "ERR Bad expansion") + require.ErrorContains(t, rdb.Do(ctx, "bf.reserve", key, "0.01", "1000", "expansion", "123asd").Err(), "ERR Bad expansion") }) t.Run("Reserve a bloom filter with nonscaling and expansion", func(t *testing.T) { @@ -227,6 +227,84 @@ func TestBloom(t *testing.T) { require.Equal(t, []interface{}{"Capacity", int64(210), "Size", int64(896), "Number of filters", int64(3), "Number of items inserted", int64(91), "Expansion rate", int64(2)}, rdb.Do(ctx, "bf.info", key).Val()) }) + t.Run("Insert but not create", func(t *testing.T) { + require.NoError(t, rdb.Del(ctx, key).Err()) + require.ErrorContains(t, rdb.Do(ctx, "bf.insert", key, "nocreate", "items", "items1").Err(), "key is not found") + + }) + + t.Run("Insert with error_rate", func(t *testing.T) { + require.NoError(t, rdb.Del(ctx, key).Err()) + require.ErrorContains(t, rdb.Do(ctx, "bf.insert", key, "error", "abc", "items", "items1").Err(), "ERR Bad error rate") + require.ErrorContains(t, rdb.Do(ctx, "bf.insert", key, "error", "-0.03", "items", "items1").Err(), "ERR error rate should be between 0 and 1") + require.ErrorContains(t, rdb.Do(ctx, "bf.insert", key, "error", "1", "items", "items1").Err(), "ERR error rate should be between 0 and 1") + + require.NoError(t, rdb.Do(ctx, "bf.insert", key, "error", "0.0001", "items", "items1").Err()) + require.Equal(t, []interface{}{"Capacity", int64(100), "Size", int64(512), "Number of filters", int64(1), "Number of items inserted", int64(1), "Expansion rate", int64(2)}, rdb.Do(ctx, "bf.info", key).Val()) + }) + + t.Run("Insert with capacity", func(t *testing.T) { + require.NoError(t, rdb.Del(ctx, key).Err()) + require.ErrorContains(t, rdb.Do(ctx, "bf.insert", key, "capacity", "qwe", "items", "items1").Err(), "ERR Bad capacity") + require.ErrorContains(t, rdb.Do(ctx, "bf.insert", key, "capacity", "-1000", "items", "items1").Err(), "ERR Bad capacity") + require.ErrorContains(t, rdb.Do(ctx, "bf.insert", key, "capacity", "0", "items", "items1").Err(), "ERR capacity should be larger than 0") + + require.NoError(t, rdb.Do(ctx, "bf.insert", key, "capacity", "200", "items", "items1").Err()) + require.Equal(t, []interface{}{"Capacity", int64(200), "Size", int64(256), "Number of filters", int64(1), "Number of items inserted", int64(1), "Expansion rate", int64(2)}, rdb.Do(ctx, "bf.info", key).Val()) + }) + + t.Run("Insert with nonscaling", func(t *testing.T) { + require.NoError(t, rdb.Del(ctx, key).Err()) + require.NoError(t, rdb.Do(ctx, "bf.insert", key, "nonscaling", "items", "items1").Err()) + + require.Equal(t, redis.Nil, rdb.Do(ctx, "bf.info", key, "expansion").Err()) + }) + + t.Run("Insert with expansion", func(t *testing.T) { + require.NoError(t, rdb.Del(ctx, key).Err()) + require.ErrorContains(t, rdb.Do(ctx, "bf.insert", key, "expansion", "items", "items1").Err(), "ERR Bad expansion") + require.ErrorContains(t, rdb.Do(ctx, "bf.insert", key, "expansion", "0", "items", "items1").Err(), "ERR expansion should be greater or equal to 1") + require.ErrorContains(t, rdb.Do(ctx, "bf.insert", key, "expansion", "asd", "items", "items1").Err(), "ERR Bad expansion") + require.ErrorContains(t, rdb.Do(ctx, "bf.insert", key, "expansion", "-1", "items", "items1").Err(), "ERR Bad expansion") + require.ErrorContains(t, rdb.Do(ctx, "bf.insert", key, "expansion", "1.5", "items", "items1").Err(), "ERR Bad expansion") + require.ErrorContains(t, rdb.Do(ctx, "bf.insert", key, "expansion", "123asd", "items", "items1").Err(), "ERR Bad expansion") + + require.NoError(t, rdb.Do(ctx, "bf.insert", key, "expansion", "3", "items", "items1").Err()) + require.Equal(t, []interface{}{"Capacity", int64(100), "Size", int64(128), "Number of filters", int64(1), "Number of items inserted", int64(1), "Expansion rate", int64(3)}, rdb.Do(ctx, "bf.info", key).Val()) + }) + + t.Run("Insert with nonscaling and expansion", func(t *testing.T) { + require.NoError(t, rdb.Del(ctx, key).Err()) + require.ErrorContains(t, rdb.Do(ctx, "bf.insert", key, "expansion", "1", "nonscaling", "items", "items1").Err(), "ERR nonscaling filters cannot expand") + require.ErrorContains(t, rdb.Do(ctx, "bf.insert", key, "nonscaling", "expansion", "1", "items", "items1").Err(), "ERR nonscaling filters cannot expand") + }) + + t.Run("Insert items", func(t *testing.T) { + require.NoError(t, rdb.Del(ctx, key).Err()) + require.ErrorContains(t, rdb.Do(ctx, "bf.insert", key, "capacity", "100", "items").Err(), "ERR num of items should be greater than 0") + + require.Equal(t, []interface{}{int64(0), int64(0), int64(0)}, rdb.Do(ctx, "bf.mexists", key, "xxx", "yyy", "zzz").Val()) + + require.Equal(t, []interface{}{int64(1), int64(1)}, rdb.Do(ctx, "bf.insert", key, "items", "xxx", "zzz").Val()) + require.Equal(t, int64(2), rdb.Do(ctx, "bf.card", key).Val()) + require.Equal(t, []interface{}{int64(1), int64(0), int64(1)}, rdb.Do(ctx, "bf.mexists", key, "xxx", "yyy", "zzz").Val()) + + // add the existed value + require.Equal(t, []interface{}{int64(0)}, rdb.Do(ctx, "bf.insert", key, "items", "zzz").Val()) + require.Equal(t, []interface{}{int64(1), int64(0), int64(1)}, rdb.Do(ctx, "bf.mexists", key, "xxx", "yyy", "zzz").Val()) + + // add the same value + require.Equal(t, []interface{}{int64(1), int64(0)}, rdb.Do(ctx, "bf.insert", key, "items", "yyy", "yyy").Val()) + require.Equal(t, []interface{}{int64(1), int64(1), int64(1)}, rdb.Do(ctx, "bf.mexists", key, "xxx", "yyy", "zzz").Val()) + }) + + t.Run("Insert would not change existed bloom filter", func(t *testing.T) { + require.NoError(t, rdb.Del(ctx, key).Err()) + require.NoError(t, rdb.Do(ctx, "bf.reserve", key, "0.02", "1000", "expansion", "3").Err()) + require.NoError(t, rdb.Do(ctx, "bf.insert", key, "error", "0.01", "capacity", "2000", "expansion", "4", "items", "xxx", "zzz").Err()) + require.Equal(t, []interface{}{"Capacity", int64(1000), "Size", int64(2048), "Number of filters", int64(1), "Number of items inserted", int64(2), "Expansion rate", int64(3)}, rdb.Do(ctx, "bf.info", key).Val()) + }) + t.Run("MExists Basic Test", func(t *testing.T) { require.NoError(t, rdb.Del(ctx, key).Err()) require.Equal(t, []interface{}{int64(0), int64(0), int64(0)}, rdb.Do(ctx, "bf.mexists", key, "xxx", "yyy", "zzz").Val())