redis-bitops 0.2
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 +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +193 -0
- data/lib/redis/bitops.rb +26 -0
- data/lib/redis/bitops/bitmap.rb +107 -0
- data/lib/redis/bitops/configuration.rb +38 -0
- data/lib/redis/bitops/queries/binary_operator.rb +71 -0
- data/lib/redis/bitops/queries/lazy_evaluation.rb +47 -0
- data/lib/redis/bitops/queries/materialization_helpers.rb +53 -0
- data/lib/redis/bitops/queries/tree_building_helpers.rb +46 -0
- data/lib/redis/bitops/queries/unary_operator.rb +48 -0
- data/lib/redis/bitops/sparse_bitmap.rb +125 -0
- data/spec/redis/bitops/bitmap_spec.rb +9 -0
- data/spec/redis/bitops/queries/binary_operator_spec.rb +24 -0
- data/spec/redis/bitops/queries/unary_operator_spec.rb +27 -0
- data/spec/redis/bitops/sparse_bitmap_spec.rb +99 -0
- data/spec/spec_helper.rb +16 -0
- data/spec/support/bitmap_examples.rb +313 -0
- metadata +173 -0
@@ -0,0 +1,47 @@
|
|
1
|
+
require 'forwardable'
|
2
|
+
|
3
|
+
class Redis
|
4
|
+
module Bitops
|
5
|
+
module Queries
|
6
|
+
|
7
|
+
# Support for materializing expressions when one of the supported bitmap methods is called.
|
8
|
+
#
|
9
|
+
# Example
|
10
|
+
#
|
11
|
+
# a = $redis.sparse_bitmap("a")
|
12
|
+
# b = $redis.sparse_bitmap("b")
|
13
|
+
# a[0] = true
|
14
|
+
# result = a | b
|
15
|
+
# puts result[0] => true
|
16
|
+
#
|
17
|
+
module LazyEvaluation
|
18
|
+
extend Forwardable
|
19
|
+
|
20
|
+
def_delegators :dest, :bitcount, :[], :[]=, :<<, :delete!, :root_key
|
21
|
+
|
22
|
+
def dest
|
23
|
+
if @dest.nil?
|
24
|
+
@dest = temp_bitmap
|
25
|
+
do_evaluate(@dest)
|
26
|
+
end
|
27
|
+
@dest
|
28
|
+
end
|
29
|
+
|
30
|
+
# Optimizes the expression and materializes it into bitmap 'dest'.
|
31
|
+
#
|
32
|
+
def evaluate(dest_bitmap)
|
33
|
+
if @dest
|
34
|
+
@dest.copy_to(dest_bitmap)
|
35
|
+
else
|
36
|
+
do_evaluate(dest_bitmap)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
protected def do_evaluate(dest_bitmap)
|
41
|
+
optimize!
|
42
|
+
materialize(dest_bitmap)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
class Redis
|
2
|
+
module Bitops
|
3
|
+
module Queries
|
4
|
+
|
5
|
+
# Helpers for materialization, i.e. running a BITOP command or, possibly, another command
|
6
|
+
# and saving its results to a Redis database in 'intermediate'.
|
7
|
+
#
|
8
|
+
module MaterializationHelpers
|
9
|
+
|
10
|
+
# Materializes the operand 'o' saving the result in 'redis'.
|
11
|
+
# If the operand can be materialized, it does so storing the result in 'intermediate'
|
12
|
+
# unless the latter is nil. In that case, a temp intermediate bitmap is created to hold
|
13
|
+
# the result (and the bitmap is added to 'temp_intermediates').
|
14
|
+
#
|
15
|
+
def resolve_operand(o, intermediate, temp_intermediates)
|
16
|
+
if o.respond_to?(:materialize)
|
17
|
+
if intermediate.nil?
|
18
|
+
new_intermediate = temp_bitmap
|
19
|
+
temp_intermediates << new_intermediate
|
20
|
+
end
|
21
|
+
intermediate ||= new_intermediate
|
22
|
+
o.materialize(intermediate)
|
23
|
+
[intermediate, nil]
|
24
|
+
else
|
25
|
+
[o, intermediate]
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
# Creates a temp bitmap.
|
30
|
+
#
|
31
|
+
def temp_bitmap
|
32
|
+
bitmap = bitmap_factory.call(unique_key)
|
33
|
+
bitmap
|
34
|
+
end
|
35
|
+
|
36
|
+
# Generates a random unique key.
|
37
|
+
#
|
38
|
+
# TODO: The key _should_ be unique and not repeat in the
|
39
|
+
# database but this isn't guaranteed. Considering the intended usage though
|
40
|
+
# (creation of temporary intermediate bitmaps while materializing
|
41
|
+
# queries), it should be sufficient.
|
42
|
+
#
|
43
|
+
def unique_key
|
44
|
+
"redis:bitops:#{SecureRandom.hex(20)}"
|
45
|
+
end
|
46
|
+
|
47
|
+
def bitmap_factory
|
48
|
+
raise "Override in the class using the module to return the bitmap factory."
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
class Redis
|
2
|
+
module Bitops
|
3
|
+
module Queries
|
4
|
+
# Helpers for expression tree building.
|
5
|
+
#
|
6
|
+
# Add new logical operators here as/if they become supported by Redis' BITOP command.
|
7
|
+
#
|
8
|
+
module TreeBuildingHelpers
|
9
|
+
|
10
|
+
# Logical AND operator.
|
11
|
+
#
|
12
|
+
def & (rhs)
|
13
|
+
BinaryOperator.new(:and, self, rhs)
|
14
|
+
end
|
15
|
+
|
16
|
+
# Logical OR operator.
|
17
|
+
#
|
18
|
+
def | (rhs)
|
19
|
+
BinaryOperator.new(:or, self, rhs)
|
20
|
+
end
|
21
|
+
|
22
|
+
# Logical XOR operator.
|
23
|
+
#
|
24
|
+
def ^ (rhs)
|
25
|
+
BinaryOperator.new(:xor, self, rhs)
|
26
|
+
end
|
27
|
+
|
28
|
+
# Logical NOT operator.
|
29
|
+
#
|
30
|
+
# IMPORTANT: It inverts bits padding with zeroes till the nearest byte boundary.
|
31
|
+
# It means that setting the left-most bit to 1 and inverting will result in 01111111 not 0.
|
32
|
+
#
|
33
|
+
# Corresponding Redis commands:
|
34
|
+
#
|
35
|
+
# SETBIT "a" 0 1
|
36
|
+
# BITOP NOT "b" "a"
|
37
|
+
# BITCOUNT "b"
|
38
|
+
# => (integer) 7
|
39
|
+
#
|
40
|
+
def ~
|
41
|
+
UnaryOperator.new(:not, self)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
class Redis
|
2
|
+
module Bitops
|
3
|
+
module Queries
|
4
|
+
|
5
|
+
# A unary bitwise operator. Currently only NOT is supported by redis.
|
6
|
+
#
|
7
|
+
class UnaryOperator
|
8
|
+
include MaterializationHelpers
|
9
|
+
include TreeBuildingHelpers
|
10
|
+
include LazyEvaluation
|
11
|
+
|
12
|
+
# Create a new bitwise operator 'op' with one argument 'arg'.
|
13
|
+
#
|
14
|
+
def initialize(op, arg)
|
15
|
+
@op = op
|
16
|
+
@arg = arg
|
17
|
+
end
|
18
|
+
|
19
|
+
# Runs the expression tree against the redis database, saving the results
|
20
|
+
# in bitmap 'dest'.
|
21
|
+
#
|
22
|
+
def materialize(dest)
|
23
|
+
temp_intermediates = []
|
24
|
+
result, = resolve_operand(@arg, dest, temp_intermediates)
|
25
|
+
result.bitop(@op, dest)
|
26
|
+
ensure
|
27
|
+
temp_intermediates.each(&:delete!)
|
28
|
+
end
|
29
|
+
|
30
|
+
# Optimizes the expression tree by combining operands for neighboring identical operators.
|
31
|
+
# Because NOT operator in Redis can only accept one operand, no optimization is made
|
32
|
+
# for the operand but the children are optimized recursively.
|
33
|
+
#
|
34
|
+
def optimize!(parent_op = nil)
|
35
|
+
@arg.optimize!(@op) if @arg.respond_to?(:optimize!)
|
36
|
+
self
|
37
|
+
end
|
38
|
+
|
39
|
+
# Finds the first bitmap factory in the expression tree.
|
40
|
+
# Required by LazyEvaluation and MaterializationHelpers.
|
41
|
+
#
|
42
|
+
def bitmap_factory
|
43
|
+
@arg.bitmap_factory or raise "Internal error. Cannot get redis connection."
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,125 @@
|
|
1
|
+
require 'set'
|
2
|
+
|
3
|
+
class Redis
|
4
|
+
module Bitops
|
5
|
+
|
6
|
+
# A sparse bitmap using multiple key to store its data to save memory.
|
7
|
+
#
|
8
|
+
# Note: When adding new public methods, revise the LazyEvaluation module.
|
9
|
+
#
|
10
|
+
class SparseBitmap < Bitmap
|
11
|
+
|
12
|
+
# Creates a new sparse bitmap stored in 'redis' under 'root_key'.
|
13
|
+
#
|
14
|
+
def initialize(root_key, redis, bytes_per_chunk = nil)
|
15
|
+
@bytes_per_chunk = bytes_per_chunk || Redis::Bitops.configuration.default_bytes_per_chunk
|
16
|
+
super(root_key, redis)
|
17
|
+
end
|
18
|
+
|
19
|
+
# Returns the number of set bits.
|
20
|
+
#
|
21
|
+
def bitcount
|
22
|
+
chunk_keys.map { |key| @redis.bitcount(key) }.reduce(:+) || 0
|
23
|
+
end
|
24
|
+
|
25
|
+
# Deletes the bitmap and all its keys.
|
26
|
+
#
|
27
|
+
def delete!
|
28
|
+
chunk_keys.each do |key|
|
29
|
+
@redis.del(key)
|
30
|
+
end
|
31
|
+
super
|
32
|
+
end
|
33
|
+
|
34
|
+
# Redis BITOP operator 'op' (one of :and, :or, :xor or :not) on operands
|
35
|
+
# (bitmaps). The result is stored in 'result'.
|
36
|
+
#
|
37
|
+
def bitop(op, *operands, result)
|
38
|
+
# TODO: Optimization is possible for AND. We can use an intersection of each operand
|
39
|
+
# chunk numbers to minimize the number of database accesses.
|
40
|
+
|
41
|
+
all_keys = self.chunk_keys + (operands.map(&:chunk_keys).flatten! || [])
|
42
|
+
unique_chunk_numbers = Set.new(chunk_numbers(all_keys))
|
43
|
+
|
44
|
+
maybe_multi(level: :bitmap, watch: all_keys) do
|
45
|
+
unique_chunk_numbers.each do |i|
|
46
|
+
@redis.bitop(op, result.chunk_key(i), self.chunk_key(i), *operands.map { |o| o.chunk_key(i) })
|
47
|
+
end
|
48
|
+
end
|
49
|
+
result
|
50
|
+
end
|
51
|
+
|
52
|
+
def chunk_keys
|
53
|
+
@redis.keys("#{@root_key}:chunk:*")
|
54
|
+
end
|
55
|
+
|
56
|
+
def chunk_key(i)
|
57
|
+
"#{@root_key}:chunk:#{i}"
|
58
|
+
end
|
59
|
+
|
60
|
+
# Returns lambda creating SparseBitmap objects using @redis as the connection.
|
61
|
+
#
|
62
|
+
def bitmap_factory
|
63
|
+
lambda { |key| @redis.sparse_bitmap(key, @bytes_per_chunk) }
|
64
|
+
end
|
65
|
+
|
66
|
+
# Copy this bitmap to 'dest' bitmap.
|
67
|
+
#
|
68
|
+
def copy_to(dest)
|
69
|
+
|
70
|
+
# Copies all source chunks to destination chunks and deletes remaining destination chunk keys.
|
71
|
+
|
72
|
+
source_keys = self.chunk_keys
|
73
|
+
dest_keys = dest.chunk_keys
|
74
|
+
|
75
|
+
maybe_multi(level: :bitmap, watch: source_keys + dest_keys) do
|
76
|
+
source_chunks = Set.new(chunk_numbers(source_keys))
|
77
|
+
source_chunks.each do |i|
|
78
|
+
copy(chunk_key(i), dest.chunk_key(i))
|
79
|
+
end
|
80
|
+
dest_chunks = Set.new(chunk_numbers(dest_keys))
|
81
|
+
(dest_chunks - source_chunks).each do |i|
|
82
|
+
@redis.del(dest.chunk_key(i))
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
protected
|
88
|
+
|
89
|
+
def bits_per_chunk
|
90
|
+
@bytes_per_chunk * 8
|
91
|
+
end
|
92
|
+
|
93
|
+
def key(pos)
|
94
|
+
chunk_key(chunk_number(pos))
|
95
|
+
end
|
96
|
+
|
97
|
+
def offset(pos)
|
98
|
+
pos.modulo bits_per_chunk
|
99
|
+
end
|
100
|
+
|
101
|
+
def chunk_number(pos)
|
102
|
+
(pos / bits_per_chunk).to_i
|
103
|
+
end
|
104
|
+
|
105
|
+
def chunk_numbers(keys)
|
106
|
+
keys.map { |key| key.split(":").last.to_i }
|
107
|
+
end
|
108
|
+
|
109
|
+
# Maybe pipeline/make atomic based on the configuration.
|
110
|
+
#
|
111
|
+
def maybe_multi(options = {}, &block)
|
112
|
+
current_level = options[:level] or raise "Specify the current transaction level."
|
113
|
+
|
114
|
+
if Redis::Bitops.configuration.transaction_level == current_level
|
115
|
+
watched_keys = options[:watch]
|
116
|
+
@redis.watch(watched_keys) if watched_keys
|
117
|
+
@redis.multi(&block)
|
118
|
+
else
|
119
|
+
block.call
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
@@ -0,0 +1,9 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Redis::Bitops::Bitmap, redis_cleanup: true, redis_key_prefix: "rsb:" do
|
4
|
+
it_should_behave_like "a bitmap", :bitmap
|
5
|
+
end
|
6
|
+
|
7
|
+
describe "Redis#bitmap" do
|
8
|
+
it_should_behave_like "a bitmap factory method", :bitmap, Redis::Bitops::Bitmap
|
9
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Redis::Bitops::Queries::BinaryOperator do
|
4
|
+
let(:a) { double("a") }
|
5
|
+
let(:b) { double("b") }
|
6
|
+
let(:c) { double("c") }
|
7
|
+
let(:d) { double("d") }
|
8
|
+
let(:e) { double("e") }
|
9
|
+
|
10
|
+
let(:redis) { double("redis", del: nil) }
|
11
|
+
let(:result) { double("output", redis: redis) }
|
12
|
+
|
13
|
+
it "optimizes the expression tree" do
|
14
|
+
a.should_receive(:bitop).with(:and, result, d, e, result)
|
15
|
+
b.should_receive(:bitop).with(:or, c, result)
|
16
|
+
expr = Redis::Bitops::Queries::BinaryOperator.new(:and,
|
17
|
+
a,
|
18
|
+
Redis::Bitops::Queries::BinaryOperator.new(:and,
|
19
|
+
Redis::Bitops::Queries::BinaryOperator.new(:or, b, c),
|
20
|
+
Redis::Bitops::Queries::BinaryOperator.new(:and, d, e)))
|
21
|
+
expr.optimize!
|
22
|
+
expr.materialize(result)
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Redis::Bitops::Queries::UnaryOperator do
|
4
|
+
let(:a) { double("a") }
|
5
|
+
let(:b) { double("b") }
|
6
|
+
let(:c) { double("c") }
|
7
|
+
let(:d) { double("d") }
|
8
|
+
let(:e) { double("e") }
|
9
|
+
|
10
|
+
let(:redis) { double("redis", del: nil) }
|
11
|
+
let(:result) { double("result", redis: redis) }
|
12
|
+
|
13
|
+
it "optimizes the expression tree" do
|
14
|
+
a.should_receive(:bitop).with(:and, result, d, e, result)
|
15
|
+
b.should_receive(:bitop).with(:or, c, result)
|
16
|
+
result.should_receive(:bitop).with(:not, result)
|
17
|
+
expr =
|
18
|
+
Redis::Bitops::Queries::UnaryOperator.new(:not,
|
19
|
+
Redis::Bitops::Queries::BinaryOperator.new(:and,
|
20
|
+
a,
|
21
|
+
Redis::Bitops::Queries::BinaryOperator.new(:and,
|
22
|
+
Redis::Bitops::Queries::BinaryOperator.new(:or, b, c),
|
23
|
+
Redis::Bitops::Queries::BinaryOperator.new(:and, d, e))))
|
24
|
+
expr.optimize!
|
25
|
+
expr.materialize(result)
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Redis::Bitops::SparseBitmap, redis_cleanup: true, redis_key_prefix: "rsb:" do
|
4
|
+
it_should_behave_like "a bitmap", :sparse_bitmap
|
5
|
+
|
6
|
+
let(:redis) { Redis.new }
|
7
|
+
let(:bytes_per_chunk) { 4 }
|
8
|
+
let(:bits_per_chunk) { bytes_per_chunk * 8 }
|
9
|
+
let(:a) { redis.sparse_bitmap("rsb:a", bytes_per_chunk) }
|
10
|
+
let(:b) { redis.sparse_bitmap("rsb:b", bytes_per_chunk) }
|
11
|
+
let(:c) { redis.sparse_bitmap("rsb:c", bytes_per_chunk) }
|
12
|
+
let(:result) { redis.sparse_bitmap("rsb:output", bytes_per_chunk) }
|
13
|
+
|
14
|
+
describe "edge cases" do
|
15
|
+
before do
|
16
|
+
a[0] = true
|
17
|
+
a[bits_per_chunk - 1] = true
|
18
|
+
a[bits_per_chunk] = true
|
19
|
+
a[2 * bits_per_chunk - 1] = true
|
20
|
+
a[2 * bits_per_chunk] = true
|
21
|
+
|
22
|
+
b[0] = true
|
23
|
+
b[bits_per_chunk] = true
|
24
|
+
b[2 * bits_per_chunk - 1] = true
|
25
|
+
end
|
26
|
+
|
27
|
+
describe "#[]" do
|
28
|
+
it "handles bits arround chunk boundaries" do
|
29
|
+
a.chunk_keys.should have(3).items
|
30
|
+
set_bits =
|
31
|
+
(0..(3 * bits_per_chunk)).inject([]) { |acc, i|
|
32
|
+
acc << i if a[i]
|
33
|
+
acc
|
34
|
+
}
|
35
|
+
set_bits.should match_array([
|
36
|
+
0,
|
37
|
+
bits_per_chunk - 1,
|
38
|
+
bits_per_chunk,
|
39
|
+
2 * bits_per_chunk - 1,
|
40
|
+
2 * bits_per_chunk])
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
describe "#bitcount" do
|
45
|
+
it "handles bits around chunk boundaries" do
|
46
|
+
a.bitcount.should == 5
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
describe "#operator |" do
|
51
|
+
it "handles bits around chunk boundaries" do
|
52
|
+
result << (a | b)
|
53
|
+
result.bitcount.should == 5
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
describe "#operator &" do
|
58
|
+
it "handles bits around chunk boundaries" do
|
59
|
+
result << (a & b)
|
60
|
+
result.bitcount.should == 3
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
describe "#operator ~" do
|
65
|
+
it "handles bits around chunk boundaries" do
|
66
|
+
result << (~(a & b))
|
67
|
+
result[0].should be_false
|
68
|
+
result[1].should be_true
|
69
|
+
result[bits_per_chunk - 1].should be_true
|
70
|
+
result[bits_per_chunk].should be_false
|
71
|
+
result[bits_per_chunk + 1].should be_true
|
72
|
+
result[2 * bits_per_chunk - 2].should be_true
|
73
|
+
result[2 * bits_per_chunk - 1].should be_false
|
74
|
+
result[2 * bits_per_chunk].should be_true
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
describe "#operator ^" do
|
79
|
+
it "handles bits around chunk boundaries" do
|
80
|
+
result << (a ^ b)
|
81
|
+
result.bitcount.should == 2
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
describe "#copy_to" do
|
86
|
+
it "overrides all chunks in the target bitmap" do
|
87
|
+
# Fix expression with bits set using [] after evaluation doesn't materialize the newly set bits.
|
88
|
+
result[4*bits_per_chunk + 1] = true
|
89
|
+
a.copy_to(result)
|
90
|
+
result.bitcount.should == a.bitcount
|
91
|
+
result[4*bits_per_chunk + 1].should be_false
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
describe "Redis#sparse_bitmap" do
|
98
|
+
it_should_behave_like "a bitmap factory method", :sparse_bitmap, Redis::Bitops::SparseBitmap
|
99
|
+
end
|