redis-bitops 0.2
Sign up to get free protection for your applications and to get access to all the features.
- 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
|