redis-bloomfilter 0.0.3 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +5 -13
- data/.travis.yml +4 -3
- data/Gemfile +3 -1
- data/Rakefile +6 -4
- data/benchmark/bf_100_000_flat.rb +14 -12
- data/benchmark/bf_10_000.rb +14 -12
- data/examples/basic.rb +15 -13
- data/lib/bloomfilter_driver/lua.rb +75 -77
- data/lib/bloomfilter_driver/ruby.rb +27 -31
- data/lib/bloomfilter_driver/ruby_test.rb +29 -31
- data/lib/redis-bloomfilter.rb +9 -7
- data/lib/redis/bloomfilter.rb +24 -27
- data/lib/redis/bloomfilter/version.rb +4 -2
- data/redis-bloomfilter.gemspec +19 -18
- data/spec/redis_bloomfilter_spec.rb +40 -47
- data/spec/spec_helper.rb +4 -4
- metadata +30 -36
checksums.yaml
CHANGED
@@ -1,15 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
|
5
|
-
data.tar.gz: !binary |-
|
6
|
-
Zjg5NDkwNDdmOWZiOThlMWYxZTkyOWI1OGE2ODcxZmU1ZjZkMTZiYQ==
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 03a3466f74223e60731f3054278118e7ce0d3745826469b3f752e0b916e664e1
|
4
|
+
data.tar.gz: 840df14ff183236d0c1dd78e954b43635c39469e82c845787b7741526edb4890
|
7
5
|
SHA512:
|
8
|
-
metadata.gz:
|
9
|
-
|
10
|
-
YjdjODBkM2QzN2NiNTgzNmUyMmY4OWNiNTBiNjhiYTU5OGRkNTI1NDNlOWNh
|
11
|
-
ZDJhYWNmMmZjOGQ5M2QxNjYyYzdmMmI4YmEzZjM4NzU0YjIwMzA=
|
12
|
-
data.tar.gz: !binary |-
|
13
|
-
YzkzMjg5ZTRhZDNlNGI1YTBlMzMwMjY4NDQ0N2RhMTM4MmE2ZmZmMTY1ZjQ3
|
14
|
-
NTg0MzM3NmM2MGE0N2JkYWMzMDI0NDdjZTFlMzMyMDMzNmU2ZmYzYTZkMjhl
|
15
|
-
ZjMxMTllZTVmZGY4YmMxZGUzYzExZGYyNWNmZmNlZTRiZTlmZGQ=
|
6
|
+
metadata.gz: 82d34182fd1599a6c88f87b6d04cfc0d1e01fe001920b8e7e6371e7bfb824549c21697853516b8f34b90c238616ed88a4b3d1e9c5e9e109430d87b20e0400f5e
|
7
|
+
data.tar.gz: 6595a1a50381052cd7bfdc4d4c73cf2710deab362168fd08aff1d1c14abb58a0a7ccfa219a04067b7b5f4175e351180d33f0d69f3477d591fad66a2cc168facf
|
data/.travis.yml
CHANGED
data/Gemfile
CHANGED
data/Rakefile
CHANGED
@@ -1,9 +1,11 @@
|
|
1
|
-
|
2
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'bundler/gem_tasks'
|
4
|
+
require 'rspec/core/rake_task'
|
3
5
|
|
4
6
|
RSpec::Core::RakeTask.new(:spec) do |spec|
|
5
7
|
spec.pattern = 'spec/*_spec.rb'
|
6
8
|
end
|
7
9
|
|
8
|
-
task :
|
9
|
-
task :
|
10
|
+
task default: :spec
|
11
|
+
task test: :spec
|
@@ -1,23 +1,25 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
$LOAD_PATH.push File.expand_path('../lib', __FILE__)
|
4
|
+
require 'redis-bloomfilter'
|
5
|
+
require 'benchmark'
|
4
6
|
items = 100_000
|
5
7
|
error_rate = 0.01
|
6
|
-
%w
|
8
|
+
%w[lua ruby].each do |driver|
|
7
9
|
bf = Redis::Bloomfilter.new(
|
8
10
|
{
|
9
|
-
:
|
10
|
-
:
|
11
|
-
:
|
12
|
-
:
|
11
|
+
size: items,
|
12
|
+
error_rate: error_rate,
|
13
|
+
key_name: "bloom-filter-bench-flat-#{driver}",
|
14
|
+
driver: driver
|
13
15
|
}
|
14
16
|
)
|
15
17
|
bf.clear
|
16
|
-
puts
|
18
|
+
puts '---------------------------------------------'
|
17
19
|
puts "Benchmarking #{driver} driver with #{items} items"
|
18
20
|
Benchmark.bm(7) do |x|
|
19
|
-
x.report(
|
20
|
-
x.report(
|
21
|
+
x.report('insert: ') { items.times { |_i| bf.insert(rand(items)) } }
|
22
|
+
x.report('include?:') { items.times { |_i| bf.include?(rand(items)) } }
|
21
23
|
end
|
22
24
|
puts
|
23
|
-
end
|
25
|
+
end
|
data/benchmark/bf_10_000.rb
CHANGED
@@ -1,11 +1,13 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
require
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
$LOAD_PATH.push File.expand_path('../lib', __FILE__)
|
4
|
+
require 'redis-bloomfilter'
|
5
|
+
require 'benchmark'
|
6
|
+
require 'set'
|
5
7
|
|
6
8
|
def rand_word(length = 8)
|
7
9
|
@charset ||= ('a'..'z').to_a
|
8
|
-
@charset.
|
10
|
+
@charset.sample(length).join
|
9
11
|
end
|
10
12
|
|
11
13
|
items = ARGV[0].nil? ? 10_000 : ARGV[0].to_i
|
@@ -16,10 +18,10 @@ error_rate = 0.01
|
|
16
18
|
|
17
19
|
bf = Redis::Bloomfilter.new(
|
18
20
|
{
|
19
|
-
:
|
20
|
-
:
|
21
|
-
:
|
22
|
-
:
|
21
|
+
size: items,
|
22
|
+
error_rate: error_rate,
|
23
|
+
key_name: "bloom-filter-bench-#{driver}",
|
24
|
+
driver: driver
|
23
25
|
}
|
24
26
|
)
|
25
27
|
bf.clear
|
@@ -27,7 +29,7 @@ error_rate = 0.01
|
|
27
29
|
first_error_at = 0
|
28
30
|
visited = Set.new
|
29
31
|
|
30
|
-
Benchmark.bm(7) do |x|
|
32
|
+
Benchmark.bm(7) do |x|
|
31
33
|
x.report do
|
32
34
|
items.times do |i|
|
33
35
|
item = rand_word
|
@@ -38,7 +40,7 @@ error_rate = 0.01
|
|
38
40
|
end
|
39
41
|
visited << item
|
40
42
|
bf.insert item
|
41
|
-
#print ".(#{"%.1f" % ((i.to_f/items.to_f) * 100)}%) " if i % 1000 == 0
|
43
|
+
# print ".(#{"%.1f" % ((i.to_f/items.to_f) * 100)}%) " if i % 1000 == 0
|
42
44
|
end
|
43
45
|
end
|
44
46
|
end
|
@@ -50,4 +52,4 @@ error_rate = 0.01
|
|
50
52
|
puts "Error found: #{error}"
|
51
53
|
puts "Error rate: #{(error.to_f / items)}"
|
52
54
|
puts
|
53
|
-
end
|
55
|
+
end
|
data/examples/basic.rb
CHANGED
@@ -1,4 +1,6 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'redis-bloomfilter'
|
2
4
|
|
3
5
|
# It creates a Bloom Filter using the default ruby driver
|
4
6
|
# Number of elements expected : 10000
|
@@ -6,16 +8,16 @@ require "redis-bloomfilter"
|
|
6
8
|
# Key name on Redis: my-bloom-filter
|
7
9
|
# Redis: 127.0.0.1:6379 or an already existing connection
|
8
10
|
@bf = Redis::Bloomfilter.new(
|
9
|
-
:
|
10
|
-
:
|
11
|
-
:
|
11
|
+
size: 10_000,
|
12
|
+
error_rate: 0.01,
|
13
|
+
key_name: 'my-bloom-filter'
|
12
14
|
)
|
13
15
|
|
14
16
|
# Insert an element
|
15
|
-
@bf.insert
|
17
|
+
@bf.insert 'foo'
|
16
18
|
# Check if an element exists
|
17
|
-
puts @bf.include?(
|
18
|
-
puts @bf.include?(
|
19
|
+
puts @bf.include?('foo') # => true
|
20
|
+
puts @bf.include?('bar') # => false
|
19
21
|
|
20
22
|
# Empty the BF and delete the key stored on redis
|
21
23
|
@bf.clear
|
@@ -23,16 +25,16 @@ puts @bf.include?("bar") # => false
|
|
23
25
|
# Using Lua's driver: only available on Redis >= 2.6.0
|
24
26
|
# This driver should be prefered because is faster
|
25
27
|
@bf = Redis::Bloomfilter.new(
|
26
|
-
:
|
27
|
-
:
|
28
|
-
:
|
29
|
-
:
|
28
|
+
size: 10_000,
|
29
|
+
error_rate: 0.01,
|
30
|
+
key_name: 'my-bloom-filter-lua',
|
31
|
+
driver: 'lua'
|
30
32
|
)
|
31
33
|
|
32
34
|
# Specify a redis connection:
|
33
35
|
# @bf = Redis::Bloomfilter.new(
|
34
|
-
# :size => 10_000,
|
35
|
-
# :error_rate => 0.01,
|
36
|
+
# :size => 10_000,
|
37
|
+
# :error_rate => 0.01,
|
36
38
|
# :key_name => 'my-bloom-filter-lua',
|
37
39
|
# :driver => 'lua',
|
38
40
|
# :redis => Redis.new(:host => "10.0.1.1", :port => 6380)
|
@@ -1,7 +1,8 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'digest/sha1'
|
2
4
|
class Redis
|
3
5
|
module BloomfilterDriver
|
4
|
-
|
5
6
|
# It loads lua script into redis.
|
6
7
|
# BF implementation is done by lua scripting
|
7
8
|
# The alghoritm is executed directly on redis
|
@@ -20,97 +21,94 @@ class Redis
|
|
20
21
|
set data, 1
|
21
22
|
end
|
22
23
|
|
23
|
-
def remove(data)
|
24
|
-
set data, 0
|
25
|
-
end
|
26
|
-
|
27
24
|
def include?(key)
|
28
|
-
r = @redis.evalsha(@check_fnc_sha, :
|
29
|
-
r == 1
|
25
|
+
r = @redis.evalsha(@check_fnc_sha, keys: [@options[:key_name]], argv: [@options[:size], @options[:error_rate], key])
|
26
|
+
r == 1
|
30
27
|
end
|
31
28
|
|
32
29
|
def clear
|
33
|
-
@redis.keys("#{@options[:key_name]}:*").each {|k
|
30
|
+
@redis.keys("#{@options[:key_name]}:*").each { |k| @redis.del k }
|
34
31
|
end
|
35
32
|
|
36
33
|
protected
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
34
|
+
|
35
|
+
# It loads the script inside Redis
|
36
|
+
# Taken from https://github.com/ErikDubbelboer/redis-lua-scaling-bloom-filter
|
37
|
+
# This is a scalable implementation of BF. It means the initial size can vary
|
38
|
+
def lua_load
|
39
|
+
add_fnc = "
|
40
|
+
local entries = ARGV[1]
|
41
|
+
local precision = ARGV[2]
|
42
|
+
local set_value = ARGV[4]
|
43
|
+
local index = math.ceil(redis.call('INCR', KEYS[1] .. ':count') / entries)
|
44
|
+
local key = KEYS[1] .. ':' .. index
|
45
|
+
local bits = math.floor(-(entries * math.log(precision * math.pow(0.5, index))) / 0.480453013)
|
46
|
+
local k = math.floor(0.693147180 * bits / entries)
|
47
|
+
local hash = redis.sha1hex(ARGV[3])
|
48
|
+
local h = { }
|
49
|
+
h[0] = tonumber(string.sub(hash, 0 , 8 ), 16)
|
50
|
+
h[1] = tonumber(string.sub(hash, 8 , 16), 16)
|
51
|
+
h[2] = tonumber(string.sub(hash, 16, 24), 16)
|
52
|
+
h[3] = tonumber(string.sub(hash, 24, 32), 16)
|
53
|
+
for i=1, k do
|
54
|
+
redis.call('SETBIT', key, (h[i % 2] + i * h[2 + (((i + (i % 2)) % 4) / 2)]) % bits, set_value)
|
55
|
+
end
|
56
|
+
"
|
57
|
+
|
58
|
+
check_fnc = "
|
59
|
+
|
60
|
+
local entries = ARGV[1]
|
61
|
+
local precision = ARGV[2]
|
62
|
+
local index = redis.call('GET', KEYS[1] .. ':count')
|
63
|
+
if not index then
|
64
|
+
return 0
|
65
|
+
end
|
66
|
+
index = math.ceil(redis.call('GET', KEYS[1] .. ':count') / entries)
|
67
|
+
local hash = redis.sha1hex(ARGV[3])
|
68
|
+
local h = { }
|
69
|
+
h[0] = tonumber(string.sub(hash, 0 , 8 ), 16)
|
70
|
+
h[1] = tonumber(string.sub(hash, 8 , 16), 16)
|
71
|
+
h[2] = tonumber(string.sub(hash, 16, 24), 16)
|
72
|
+
h[3] = tonumber(string.sub(hash, 24, 32), 16)
|
73
|
+
local maxk = math.floor(0.693147180 * math.floor((entries * math.log(precision * math.pow(0.5, index))) / -0.480453013) / entries)
|
74
|
+
local b = { }
|
75
|
+
for i=1, maxk do
|
76
|
+
table.insert(b, h[i % 2] + i * h[2 + (((i + (i % 2)) % 4) / 2)])
|
77
|
+
end
|
78
|
+
for n=1, index do
|
79
|
+
local key = KEYS[1] .. ':' .. n
|
80
|
+
local found = true
|
81
|
+
local bits = math.floor((entries * math.log(precision * math.pow(0.5, n))) / -0.480453013)
|
48
82
|
local k = math.floor(0.693147180 * bits / entries)
|
49
|
-
|
50
|
-
local h = { }
|
51
|
-
h[0] = tonumber(string.sub(hash, 0 , 8 ), 16)
|
52
|
-
h[1] = tonumber(string.sub(hash, 8 , 16), 16)
|
53
|
-
h[2] = tonumber(string.sub(hash, 16, 24), 16)
|
54
|
-
h[3] = tonumber(string.sub(hash, 24, 32), 16)
|
83
|
+
|
55
84
|
for i=1, k do
|
56
|
-
redis.call('
|
85
|
+
if redis.call('GETBIT', key, b[i] % bits) == 0 then
|
86
|
+
found = false
|
87
|
+
break
|
88
|
+
end
|
57
89
|
end
|
58
|
-
)
|
59
|
-
|
60
|
-
check_fnc = %q(
|
61
90
|
|
62
|
-
|
63
|
-
|
64
|
-
local index = redis.call('GET', KEYS[1] .. ':count')
|
65
|
-
if not index then
|
66
|
-
return 0
|
67
|
-
end
|
68
|
-
index = math.ceil(redis.call('GET', KEYS[1] .. ':count') / entries)
|
69
|
-
local hash = redis.sha1hex(ARGV[3])
|
70
|
-
local h = { }
|
71
|
-
h[0] = tonumber(string.sub(hash, 0 , 8 ), 16)
|
72
|
-
h[1] = tonumber(string.sub(hash, 8 , 16), 16)
|
73
|
-
h[2] = tonumber(string.sub(hash, 16, 24), 16)
|
74
|
-
h[3] = tonumber(string.sub(hash, 24, 32), 16)
|
75
|
-
local maxk = math.floor(0.693147180 * math.floor((entries * math.log(precision * math.pow(0.5, index))) / -0.480453013) / entries)
|
76
|
-
local b = { }
|
77
|
-
for i=1, maxk do
|
78
|
-
table.insert(b, h[i % 2] + i * h[2 + (((i + (i % 2)) % 4) / 2)])
|
91
|
+
if found then
|
92
|
+
return 1
|
79
93
|
end
|
80
|
-
|
81
|
-
local key = KEYS[1] .. ':' .. n
|
82
|
-
local found = true
|
83
|
-
local bits = math.floor((entries * math.log(precision * math.pow(0.5, n))) / -0.480453013)
|
84
|
-
local k = math.floor(0.693147180 * bits / entries)
|
85
|
-
|
86
|
-
for i=1, k do
|
87
|
-
if redis.call('GETBIT', key, b[i] % bits) == 0 then
|
88
|
-
found = false
|
89
|
-
break
|
90
|
-
end
|
91
|
-
end
|
94
|
+
end
|
92
95
|
|
93
|
-
|
94
|
-
|
95
|
-
end
|
96
|
-
end
|
96
|
+
return 0
|
97
|
+
"
|
97
98
|
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
@add_fnc_sha = Digest::SHA1.hexdigest(add_fnc)
|
102
|
-
@check_fnc_sha = Digest::SHA1.hexdigest(check_fnc)
|
99
|
+
@add_fnc_sha = Digest::SHA1.hexdigest(add_fnc)
|
100
|
+
@check_fnc_sha = Digest::SHA1.hexdigest(check_fnc)
|
103
101
|
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
end
|
102
|
+
loaded = @redis.script(:exists, [@add_fnc_sha, @check_fnc_sha]).uniq
|
103
|
+
if loaded.count != 1 || loaded.first != true
|
104
|
+
@add_fnc_sha = @redis.script(:load, add_fnc)
|
105
|
+
@check_fnc_sha = @redis.script(:load, check_fnc)
|
109
106
|
end
|
107
|
+
end
|
110
108
|
|
111
|
-
|
112
|
-
|
113
|
-
|
109
|
+
def set(data, val)
|
110
|
+
@redis.evalsha(@add_fnc_sha, keys: [@options[:key_name]], argv: [@options[:size], @options[:error_rate], data, val])
|
111
|
+
end
|
114
112
|
end
|
115
113
|
end
|
116
|
-
end
|
114
|
+
end
|
@@ -1,8 +1,9 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'digest/sha1'
|
2
4
|
class Redis
|
3
5
|
module BloomfilterDriver
|
4
6
|
class Ruby
|
5
|
-
|
6
7
|
# Faster Ruby version.
|
7
8
|
# This driver should be used if Redis version < 2.6
|
8
9
|
attr_accessor :redis
|
@@ -11,58 +12,53 @@ class Redis
|
|
11
12
|
end
|
12
13
|
|
13
14
|
# Insert a new element
|
14
|
-
def insert(data)
|
15
|
+
def insert(data)
|
15
16
|
set data, 1
|
16
17
|
end
|
17
18
|
|
18
19
|
# It checks if a key is part of the set
|
19
20
|
def include?(key)
|
20
|
-
|
21
21
|
indexes = []
|
22
22
|
indexes_for(key).each { |idx| indexes << idx }
|
23
|
-
return false if @redis.getbit(@options[:key_name], indexes.shift)
|
23
|
+
return false if @redis.getbit(@options[:key_name], indexes.shift).zero?
|
24
24
|
|
25
25
|
result = @redis.pipelined do
|
26
|
-
indexes.each {|idx| @redis.getbit(@options[:key_name], idx)}
|
26
|
+
indexes.each { |idx| @redis.getbit(@options[:key_name], idx) }
|
27
27
|
end
|
28
28
|
|
29
29
|
!result.include?(0)
|
30
30
|
end
|
31
31
|
|
32
|
-
# It removes an element from the filter
|
33
|
-
def remove(data)
|
34
|
-
set data, 0
|
35
|
-
end
|
36
|
-
|
37
32
|
# It deletes a bloomfilter
|
38
33
|
def clear
|
39
34
|
@redis.del @options[:key_name]
|
40
35
|
end
|
41
36
|
|
42
37
|
protected
|
43
|
-
# Hashing strategy:
|
44
|
-
# http://www.eecs.harvard.edu/~kirsch/pubs/bbbf/esa06.pdf
|
45
|
-
def indexes_for data
|
46
|
-
sha = Digest::SHA1.hexdigest(data.to_s)
|
47
|
-
h = []
|
48
|
-
h[0] = sha[0...8].to_i(16)
|
49
|
-
h[1] = sha[8...16].to_i(16)
|
50
|
-
h[2] = sha[16...24].to_i(16)
|
51
|
-
h[3] = sha[24...32].to_i(16)
|
52
|
-
idxs = []
|
53
38
|
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
39
|
+
# Hashing strategy:
|
40
|
+
# https://www.eecs.harvard.edu/~michaelm/postscripts/tr-02-05.pdf
|
41
|
+
def indexes_for(data)
|
42
|
+
sha = Digest::SHA1.hexdigest(data.to_s)
|
43
|
+
h = []
|
44
|
+
h[0] = sha[0...8].to_i(16)
|
45
|
+
h[1] = sha[8...16].to_i(16)
|
46
|
+
h[2] = sha[16...24].to_i(16)
|
47
|
+
h[3] = sha[24...32].to_i(16)
|
48
|
+
idxs = []
|
49
|
+
|
50
|
+
(@options[:hashes]).times do |i|
|
51
|
+
v = (h[i % 2] + i * h[2 + (((i + (i % 2)) % 4) / 2)]) % @options[:bits]
|
52
|
+
idxs << v
|
59
53
|
end
|
54
|
+
idxs
|
55
|
+
end
|
60
56
|
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
end
|
57
|
+
def set(key, val)
|
58
|
+
@redis.pipelined do
|
59
|
+
indexes_for(key).each { |i| @redis.setbit @options[:key_name], i, val }
|
65
60
|
end
|
61
|
+
end
|
66
62
|
end
|
67
63
|
end
|
68
|
-
end
|
64
|
+
end
|
@@ -1,6 +1,8 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
require
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'digest/md5'
|
4
|
+
require 'digest/sha1'
|
5
|
+
require 'zlib'
|
4
6
|
class Redis
|
5
7
|
module BloomfilterDriver
|
6
8
|
# It uses different hash strategy
|
@@ -13,24 +15,19 @@ class Redis
|
|
13
15
|
end
|
14
16
|
|
15
17
|
# Insert a new element
|
16
|
-
def insert(data)
|
18
|
+
def insert(data)
|
17
19
|
set data, 1
|
18
20
|
end
|
19
21
|
|
20
|
-
# Insert a new element
|
21
|
-
def remove(data)
|
22
|
-
set data, 0
|
23
|
-
end
|
24
|
-
|
25
22
|
# It checks if a key is part of the set
|
26
23
|
def include?(key)
|
27
24
|
indexes = []
|
28
25
|
indexes_for(key) { |idx| indexes << idx }
|
29
26
|
|
30
|
-
return false if @redis.getbit(@options[:key_name], indexes.shift)
|
27
|
+
return false if @redis.getbit(@options[:key_name], indexes.shift).zero?
|
31
28
|
|
32
29
|
result = @redis.pipelined do
|
33
|
-
indexes.each {|idx| @redis.getbit(@options[:key_name], idx)}
|
30
|
+
indexes.each { |idx| @redis.getbit(@options[:key_name], idx) }
|
34
31
|
end
|
35
32
|
|
36
33
|
!result.include?(0)
|
@@ -42,31 +39,32 @@ class Redis
|
|
42
39
|
end
|
43
40
|
|
44
41
|
protected
|
45
|
-
def indexes_for(key, engine = nil)
|
46
|
-
engine ||= @options[:hash_engine]
|
47
|
-
@options[:hashes].times do |i|
|
48
|
-
yield self.send("engine_#{engine}", key.to_s, i)
|
49
|
-
end
|
50
|
-
end
|
51
42
|
|
52
|
-
|
53
|
-
|
54
|
-
|
43
|
+
def indexes_for(key, engine = nil)
|
44
|
+
engine ||= @options[:hash_engine]
|
45
|
+
@options[:hashes].times do |i|
|
46
|
+
yield send("engine_#{engine}", key.to_s, i)
|
55
47
|
end
|
48
|
+
end
|
56
49
|
|
57
|
-
|
58
|
-
|
59
|
-
|
50
|
+
# A set of different hash functions
|
51
|
+
def engine_crc32(data, i)
|
52
|
+
Zlib.crc32("#{i}-#{data}").to_i(16) % @options[:bits]
|
53
|
+
end
|
60
54
|
|
61
|
-
|
62
|
-
|
63
|
-
|
55
|
+
def engine_md5(data, i)
|
56
|
+
Digest::MD5.hexdigest("#{i}-#{data}").to_i(16) % @options[:bits]
|
57
|
+
end
|
64
58
|
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
59
|
+
def engine_sha1(data, i)
|
60
|
+
Digest::SHA1.hexdigest("#{i}-#{data}").to_i(16) % @options[:bits]
|
61
|
+
end
|
62
|
+
|
63
|
+
def set(data, val)
|
64
|
+
@redis.pipelined do
|
65
|
+
indexes_for(data) { |i| @redis.setbit @options[:key_name], i, val }
|
69
66
|
end
|
67
|
+
end
|
70
68
|
end
|
71
69
|
end
|
72
|
-
end
|
70
|
+
end
|
data/lib/redis-bloomfilter.rb
CHANGED
@@ -1,7 +1,9 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
require
|
4
|
-
require
|
5
|
-
require
|
6
|
-
require
|
7
|
-
require
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'redis'
|
4
|
+
require 'redis/connection/hiredis'
|
5
|
+
require 'redis/bloomfilter'
|
6
|
+
require 'redis/bloomfilter/version'
|
7
|
+
require 'bloomfilter_driver/ruby'
|
8
|
+
require 'bloomfilter_driver/lua'
|
9
|
+
require 'bloomfilter_driver/ruby_test'
|
data/lib/redis/bloomfilter.rb
CHANGED
@@ -1,6 +1,7 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
class Redis
|
2
4
|
class Bloomfilter
|
3
|
-
|
4
5
|
attr_reader :options
|
5
6
|
attr_reader :driver
|
6
7
|
|
@@ -8,17 +9,17 @@ class Redis
|
|
8
9
|
# It creates a bloomfilter with a capacity of 1000 items and an error rate of 1%
|
9
10
|
def initialize(options = {})
|
10
11
|
@options = {
|
11
|
-
:
|
12
|
-
:
|
13
|
-
:
|
14
|
-
:
|
15
|
-
:
|
16
|
-
:
|
12
|
+
size: 1000,
|
13
|
+
error_rate: 0.01,
|
14
|
+
key_name: 'redis-bloomfilter',
|
15
|
+
hash_engine: 'md5',
|
16
|
+
redis: Redis.current,
|
17
|
+
driver: nil
|
17
18
|
}.merge options
|
18
19
|
|
19
|
-
raise ArgumentError,
|
20
|
+
raise ArgumentError, 'options[:size] && options[:error_rate] cannot be nil' if options[:error_rate].nil? || options[:size].nil?
|
20
21
|
|
21
|
-
#Size provided, compute hashes and bits
|
22
|
+
# Size provided, compute hashes and bits
|
22
23
|
|
23
24
|
@options[:size] = options[:size]
|
24
25
|
@options[:error_rate] = options[:error_rate] ? options[:error_rate] : @options[:error_rate]
|
@@ -31,27 +32,27 @@ class Redis
|
|
31
32
|
if @options[:driver].nil?
|
32
33
|
ver = @redis.info['redis_version']
|
33
34
|
|
34
|
-
if Gem::Version.new(ver) >= Gem::Version.new('2.6.0')
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
35
|
+
@options[:driver] = if Gem::Version.new(ver) >= Gem::Version.new('2.6.0')
|
36
|
+
'lua'
|
37
|
+
else
|
38
|
+
'ruby'
|
39
|
+
end
|
39
40
|
end
|
40
41
|
|
41
42
|
driver_class = Redis::BloomfilterDriver.const_get(driver_name)
|
42
43
|
@driver = driver_class.new @options
|
43
|
-
@driver.redis = @redis
|
44
|
+
@driver.redis = @redis
|
44
45
|
end
|
45
46
|
|
46
47
|
# Methods used to calculate M and K
|
47
48
|
# Taken from http://en.wikipedia.org/wiki/Bloom_filter#Probability_of_false_positives
|
48
|
-
def self.optimal_m
|
49
|
-
(-1 *
|
49
|
+
def self.optimal_m(num_of_elements, false_positive_rate = 0.01)
|
50
|
+
(-1 * num_of_elements * Math.log(false_positive_rate) / (Math.log(2)**2)).round
|
50
51
|
end
|
51
52
|
|
52
|
-
def self.optimal_k
|
53
|
+
def self.optimal_k(num_of_elements, bf_size)
|
53
54
|
h = (Math.log(2) * (bf_size / num_of_elements)).round
|
54
|
-
h+=1 if h
|
55
|
+
h += 1 if h.zero?
|
55
56
|
h
|
56
57
|
end
|
57
58
|
|
@@ -65,19 +66,15 @@ class Redis
|
|
65
66
|
@driver.include?(key)
|
66
67
|
end
|
67
68
|
|
68
|
-
def remove(key)
|
69
|
-
@driver.remove key if @driver.respond_to? :remove
|
70
|
-
end
|
71
|
-
|
72
69
|
# It deletes a bloomfilter
|
73
70
|
def clear
|
74
71
|
@driver.clear
|
75
72
|
end
|
76
73
|
|
77
74
|
protected
|
78
|
-
def driver_name
|
79
|
-
@options[:driver].downcase.split('-').collect{|t| t.gsub(/(\w+)/){|s|s.capitalize}}.join
|
80
|
-
end
|
81
75
|
|
76
|
+
def driver_name
|
77
|
+
@options[:driver].downcase.split('-').collect { |t| t.gsub(/(\w+)/, &:capitalize) }.join
|
78
|
+
end
|
82
79
|
end
|
83
|
-
end
|
80
|
+
end
|
data/redis-bloomfilter.gemspec
CHANGED
@@ -1,31 +1,32 @@
|
|
1
1
|
# -*- encoding: utf-8 -*-
|
2
|
-
|
3
|
-
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
$LOAD_PATH.push File.expand_path('../lib', __FILE__)
|
5
|
+
require 'redis/bloomfilter/version'
|
4
6
|
|
5
7
|
Gem::Specification.new do |s|
|
6
|
-
s.name =
|
8
|
+
s.name = 'redis-bloomfilter'
|
7
9
|
s.version = Redis::Bloomfilter::VERSION
|
8
|
-
s.authors = [
|
9
|
-
s.email = [
|
10
|
-
s.homepage =
|
11
|
-
s.summary =
|
12
|
-
s.description =
|
10
|
+
s.authors = ['Francesco Laurita']
|
11
|
+
s.email = ['francesco.laurita@gmail.com']
|
12
|
+
s.homepage = 'https://github.com/taganaka/redis-bloomfilter'
|
13
|
+
s.summary = 'Distributed Bloom Filter implementation on Redis'
|
14
|
+
s.description = '
|
13
15
|
Adds Redis::Bloomfilter class which can be used as ditributed bloom filter implementation on Redis.
|
14
16
|
A Bloom filter is a space-efficient probabilistic data structure that is used to test whether an element is a member of a set.
|
15
|
-
|
17
|
+
'
|
16
18
|
s.licenses = ['MIT']
|
17
|
-
s.rubyforge_project =
|
19
|
+
s.rubyforge_project = 'redis-bloomfilter'
|
18
20
|
|
19
21
|
s.files = `git ls-files`.split("\n")
|
20
22
|
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
21
|
-
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
22
|
-
s.require_paths = [
|
23
|
-
|
24
|
-
s.add_runtime_dependency 'hiredis', '~> 0.5', '>= 0.5.2'
|
25
|
-
s.add_runtime_dependency 'redis', '~> 3.0', '>= 3.0.4'
|
23
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map { |f| File.basename(f) }
|
24
|
+
s.require_paths = ['lib']
|
26
25
|
|
27
|
-
s.
|
28
|
-
s.
|
29
|
-
s.add_development_dependency "rake"
|
26
|
+
s.add_runtime_dependency 'hiredis', '~> 0.6.1'
|
27
|
+
s.add_runtime_dependency 'redis', '~> 4.0', '>= 4.0.1'
|
30
28
|
|
29
|
+
s.add_development_dependency 'flexmock'
|
30
|
+
s.add_development_dependency 'rake'
|
31
|
+
s.add_development_dependency 'rspec'
|
31
32
|
end
|
@@ -1,11 +1,12 @@
|
|
1
|
-
|
2
|
-
require "set"
|
1
|
+
# frozen_string_literal: true
|
3
2
|
|
3
|
+
require 'spec_helper'
|
4
|
+
require 'set'
|
4
5
|
|
5
|
-
def test_error_rate(bf,elems)
|
6
|
+
def test_error_rate(bf, elems)
|
6
7
|
visited = Set.new
|
7
8
|
error = 0
|
8
|
-
elems.times do |
|
9
|
+
elems.times do |_i|
|
9
10
|
a = rand(elems)
|
10
11
|
error += 1 if bf.include?(a) != visited.include?(a)
|
11
12
|
visited << a
|
@@ -14,86 +15,78 @@ def test_error_rate(bf,elems)
|
|
14
15
|
error.to_f / elems
|
15
16
|
end
|
16
17
|
|
17
|
-
def factory
|
18
|
+
def factory(options, driver)
|
18
19
|
options[:driver] = driver
|
19
20
|
Redis::Bloomfilter.new options
|
20
21
|
end
|
21
22
|
|
22
23
|
describe Redis::Bloomfilter do
|
23
|
-
|
24
24
|
it 'should return the right version' do
|
25
|
-
Redis::Bloomfilter.version.
|
25
|
+
expect(Redis::Bloomfilter.version).to eq "redis-bloomfilter version #{Redis::Bloomfilter::VERSION}"
|
26
26
|
end
|
27
27
|
|
28
28
|
it 'should check for the initialize options' do
|
29
29
|
expect { Redis::Bloomfilter.new }.to raise_error(ArgumentError)
|
30
|
-
expect { Redis::Bloomfilter.new :
|
31
|
-
expect { Redis::Bloomfilter.new :
|
32
|
-
expect { Redis::Bloomfilter.new :
|
30
|
+
expect { Redis::Bloomfilter.new size: 123 }.to raise_error(ArgumentError)
|
31
|
+
expect { Redis::Bloomfilter.new error_rate: 0.01 }.to raise_error(ArgumentError)
|
32
|
+
expect { Redis::Bloomfilter.new size: 123, error_rate: 0.01, driver: 'bibu' }.to raise_error(NameError)
|
33
33
|
end
|
34
34
|
|
35
35
|
it 'should choose the right driver based on the Redis version' do
|
36
|
-
|
37
|
-
redis_mock
|
38
|
-
redis_mock.should_receive(:info).and_return({'redis_version' => '2.6.0'})
|
36
|
+
redis_mock = flexmock('redis')
|
37
|
+
redis_mock.should_receive(:info).and_return({ 'redis_version' => '2.6.0' })
|
39
38
|
redis_mock.should_receive(:script).and_return([true, true])
|
40
|
-
redis_mock_2_5 = flexmock(
|
41
|
-
redis_mock_2_5.should_receive(:info).and_return({'redis_version' => '2.5.0'})
|
39
|
+
redis_mock_2_5 = flexmock('redis_2_5')
|
40
|
+
redis_mock_2_5.should_receive(:info).and_return({ 'redis_version' => '2.5.0' })
|
42
41
|
|
43
|
-
bf = factory({:
|
44
|
-
bf.driver.
|
42
|
+
bf = factory({ size: 1000, error_rate: 0.01, key_name: 'ossom', redis: redis_mock }, nil)
|
43
|
+
expect(bf.driver).to be_kind_of(Redis::BloomfilterDriver::Lua)
|
45
44
|
|
46
|
-
bf = factory({:
|
47
|
-
bf.driver.
|
45
|
+
bf = factory({ size: 1000, error_rate: 0.01, key_name: 'ossom', redis: redis_mock_2_5 }, nil)
|
46
|
+
expect(bf.driver).to be_kind_of(Redis::BloomfilterDriver::Ruby)
|
48
47
|
end
|
49
48
|
|
50
49
|
it 'should create a Redis::Bloomfilter object' do
|
51
|
-
bf = factory({:
|
52
|
-
bf.
|
53
|
-
bf.options[:size].
|
54
|
-
bf.options[:bits].
|
55
|
-
bf.options[:hashes].
|
56
|
-
bf.options[:key_name].
|
50
|
+
bf = factory({ size: 1000, error_rate: 0.01, key_name: 'ossom' }, 'ruby')
|
51
|
+
expect(bf).to be
|
52
|
+
expect(bf.options[:size]).to eq 1000
|
53
|
+
expect(bf.options[:bits]).to eq 9585
|
54
|
+
expect(bf.options[:hashes]).to eq 6
|
55
|
+
expect(bf.options[:key_name]).to eq 'ossom'
|
57
56
|
bf.clear
|
58
57
|
end
|
59
58
|
|
60
|
-
%w
|
59
|
+
%w[ruby lua ruby-test].each do |driver|
|
61
60
|
it 'should work' do
|
62
|
-
bf = factory({:
|
61
|
+
bf = factory({ size: 1000, error_rate: 0.01, key_name: '__test_bf' }, driver)
|
63
62
|
bf.clear
|
64
|
-
bf.include?(
|
65
|
-
bf.insert
|
66
|
-
bf.include?(
|
63
|
+
expect(bf.include?('asdlol')).to be false
|
64
|
+
bf.insert 'asdlol'
|
65
|
+
expect(bf.include?('asdlol')).to be true
|
67
66
|
bf.clear
|
68
|
-
bf.include?(
|
67
|
+
expect(bf.include?('asdlol')).to be false
|
69
68
|
end
|
70
69
|
|
71
70
|
it 'should honor the error rate' do
|
72
|
-
bf = factory({:
|
71
|
+
bf = factory({ size: 100, error_rate: 0.02, key_name: '__test_bf' }, driver)
|
73
72
|
bf.clear
|
74
73
|
e = test_error_rate bf, 180
|
75
|
-
e.
|
74
|
+
expect(e.round(2)).to be <= bf.options[:error_rate].round(2)
|
76
75
|
bf.clear
|
77
76
|
end
|
78
77
|
|
79
|
-
it 'should
|
80
|
-
|
81
|
-
bf
|
82
|
-
bf.
|
83
|
-
bf.include?("asdlolol").should be true
|
84
|
-
bf.remove "asdlolol"
|
85
|
-
bf.include?("asdlolol").should be false
|
86
|
-
|
78
|
+
it 'should add an element to the filter' do
|
79
|
+
bf = factory({ size: 100, error_rate: 0.01, key_name: '__test_bf' }, driver)
|
80
|
+
bf.insert 'asdlolol'
|
81
|
+
expect(bf.include?('asdlolol')).to be true
|
87
82
|
end
|
88
83
|
end
|
89
84
|
|
90
85
|
it 'should be a scalable bloom filter' do
|
91
|
-
bf = factory({:
|
86
|
+
bf = factory({ size: 100, error_rate: 0.02, key_name: '__test_bf' }, 'lua')
|
92
87
|
bf.clear
|
93
|
-
e = test_error_rate(bf,
|
94
|
-
e.
|
88
|
+
e = test_error_rate(bf, 150)
|
89
|
+
expect(e).to be <= bf.options[:error_rate]
|
95
90
|
bf.clear
|
96
|
-
|
97
91
|
end
|
98
|
-
|
99
|
-
end
|
92
|
+
end
|
data/spec/spec_helper.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
# This file was generated by the `rspec --init` command. Conventionally, all
|
2
4
|
# specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
|
3
5
|
# Require this file using `require "spec_helper"` to ensure that it is only
|
@@ -5,7 +7,6 @@
|
|
5
7
|
#
|
6
8
|
# See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
|
7
9
|
RSpec.configure do |config|
|
8
|
-
config.treat_symbols_as_metadata_keys_with_true_values = true
|
9
10
|
config.run_all_when_everything_filtered = true
|
10
11
|
config.filter_run :focus
|
11
12
|
config.mock_with :flexmock
|
@@ -17,6 +18,5 @@ RSpec.configure do |config|
|
|
17
18
|
config.order = 'random'
|
18
19
|
end
|
19
20
|
|
20
|
-
|
21
|
-
require
|
22
|
-
|
21
|
+
$LOAD_PATH.unshift File.join(File.dirname(__FILE__), '..', 'lib')
|
22
|
+
require 'redis-bloomfilter'
|
metadata
CHANGED
@@ -1,98 +1,92 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: redis-bloomfilter
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0
|
4
|
+
version: 1.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Francesco Laurita
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2018-04-05 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: hiredis
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
16
16
|
requirements:
|
17
|
-
- - ~>
|
17
|
+
- - "~>"
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version:
|
20
|
-
- - ! '>='
|
21
|
-
- !ruby/object:Gem::Version
|
22
|
-
version: 0.5.2
|
19
|
+
version: 0.6.1
|
23
20
|
type: :runtime
|
24
21
|
prerelease: false
|
25
22
|
version_requirements: !ruby/object:Gem::Requirement
|
26
23
|
requirements:
|
27
|
-
- - ~>
|
28
|
-
- !ruby/object:Gem::Version
|
29
|
-
version: '0.5'
|
30
|
-
- - ! '>='
|
24
|
+
- - "~>"
|
31
25
|
- !ruby/object:Gem::Version
|
32
|
-
version: 0.
|
26
|
+
version: 0.6.1
|
33
27
|
- !ruby/object:Gem::Dependency
|
34
28
|
name: redis
|
35
29
|
requirement: !ruby/object:Gem::Requirement
|
36
30
|
requirements:
|
37
|
-
- - ~>
|
31
|
+
- - "~>"
|
38
32
|
- !ruby/object:Gem::Version
|
39
|
-
version: '
|
40
|
-
- -
|
33
|
+
version: '4.0'
|
34
|
+
- - ">="
|
41
35
|
- !ruby/object:Gem::Version
|
42
|
-
version:
|
36
|
+
version: 4.0.1
|
43
37
|
type: :runtime
|
44
38
|
prerelease: false
|
45
39
|
version_requirements: !ruby/object:Gem::Requirement
|
46
40
|
requirements:
|
47
|
-
- - ~>
|
41
|
+
- - "~>"
|
48
42
|
- !ruby/object:Gem::Version
|
49
|
-
version: '
|
50
|
-
- -
|
43
|
+
version: '4.0'
|
44
|
+
- - ">="
|
51
45
|
- !ruby/object:Gem::Version
|
52
|
-
version:
|
46
|
+
version: 4.0.1
|
53
47
|
- !ruby/object:Gem::Dependency
|
54
|
-
name:
|
48
|
+
name: flexmock
|
55
49
|
requirement: !ruby/object:Gem::Requirement
|
56
50
|
requirements:
|
57
|
-
- -
|
51
|
+
- - ">="
|
58
52
|
- !ruby/object:Gem::Version
|
59
53
|
version: '0'
|
60
54
|
type: :development
|
61
55
|
prerelease: false
|
62
56
|
version_requirements: !ruby/object:Gem::Requirement
|
63
57
|
requirements:
|
64
|
-
- -
|
58
|
+
- - ">="
|
65
59
|
- !ruby/object:Gem::Version
|
66
60
|
version: '0'
|
67
61
|
- !ruby/object:Gem::Dependency
|
68
|
-
name:
|
62
|
+
name: rake
|
69
63
|
requirement: !ruby/object:Gem::Requirement
|
70
64
|
requirements:
|
71
|
-
- -
|
65
|
+
- - ">="
|
72
66
|
- !ruby/object:Gem::Version
|
73
67
|
version: '0'
|
74
68
|
type: :development
|
75
69
|
prerelease: false
|
76
70
|
version_requirements: !ruby/object:Gem::Requirement
|
77
71
|
requirements:
|
78
|
-
- -
|
72
|
+
- - ">="
|
79
73
|
- !ruby/object:Gem::Version
|
80
74
|
version: '0'
|
81
75
|
- !ruby/object:Gem::Dependency
|
82
|
-
name:
|
76
|
+
name: rspec
|
83
77
|
requirement: !ruby/object:Gem::Requirement
|
84
78
|
requirements:
|
85
|
-
- -
|
79
|
+
- - ">="
|
86
80
|
- !ruby/object:Gem::Version
|
87
81
|
version: '0'
|
88
82
|
type: :development
|
89
83
|
prerelease: false
|
90
84
|
version_requirements: !ruby/object:Gem::Requirement
|
91
85
|
requirements:
|
92
|
-
- -
|
86
|
+
- - ">="
|
93
87
|
- !ruby/object:Gem::Version
|
94
88
|
version: '0'
|
95
|
-
description:
|
89
|
+
description: "\n Adds Redis::Bloomfilter class which can be used as ditributed
|
96
90
|
bloom filter implementation on Redis.\n A Bloom filter is a space-efficient probabilistic
|
97
91
|
data structure that is used to test whether an element is a member of a set.\n "
|
98
92
|
email:
|
@@ -101,9 +95,9 @@ executables: []
|
|
101
95
|
extensions: []
|
102
96
|
extra_rdoc_files: []
|
103
97
|
files:
|
104
|
-
- .gitignore
|
105
|
-
- .rspec
|
106
|
-
- .travis.yml
|
98
|
+
- ".gitignore"
|
99
|
+
- ".rspec"
|
100
|
+
- ".travis.yml"
|
107
101
|
- Gemfile
|
108
102
|
- LICENSE.txt
|
109
103
|
- README.md
|
@@ -130,17 +124,17 @@ require_paths:
|
|
130
124
|
- lib
|
131
125
|
required_ruby_version: !ruby/object:Gem::Requirement
|
132
126
|
requirements:
|
133
|
-
- -
|
127
|
+
- - ">="
|
134
128
|
- !ruby/object:Gem::Version
|
135
129
|
version: '0'
|
136
130
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
137
131
|
requirements:
|
138
|
-
- -
|
132
|
+
- - ">="
|
139
133
|
- !ruby/object:Gem::Version
|
140
134
|
version: '0'
|
141
135
|
requirements: []
|
142
136
|
rubyforge_project: redis-bloomfilter
|
143
|
-
rubygems_version: 2.
|
137
|
+
rubygems_version: 2.7.2
|
144
138
|
signing_key:
|
145
139
|
specification_version: 4
|
146
140
|
summary: Distributed Bloom Filter implementation on Redis
|