redi_set 0.1.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 +7 -0
- data/LICENSE +20 -0
- data/README.md +23 -0
- data/lib/redi_set.rb +31 -0
- data/lib/redi_set/attribute.rb +9 -0
- data/lib/redi_set/client.rb +35 -0
- data/lib/redi_set/constraint.rb +46 -0
- data/lib/redi_set/quality.rb +41 -0
- data/lib/redi_set/query.rb +25 -0
- metadata +80 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: d8c7c9190ff4f3fa2008d58feb0293fc5f6edb094875919a0f7fa97e6a811994
|
4
|
+
data.tar.gz: 35bcd5d99938b3638caa6760a971e4e093639bcca4f98b22c609597b8acbea76
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: a481586ab8389c7fc09eed24d58f1db163a87e60b890cafeab477e6c2719d5d19070df7fd9f03c756490055ae798e649c6df7196404b0fc5ad987797e6a849a8
|
7
|
+
data.tar.gz: 8dde81c7e5f25f77a37ae25df6e406ee0e076a8dbbf6d069136aa64beb20db1ef07320ccfc8046e2a1ec9fad50fccaa71323ec26bcbf2f03bb64b0060e71e9f4
|
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2020 Eric Mueller
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
## What Is It?
|
2
|
+
|
3
|
+
RediSet is a redis-backed library that makes it easy to find members within a population
|
4
|
+
satisfying sets of attribute constraints. Given information about a large population of
|
5
|
+
cats for example, we could quickly find the set of cats that are male, calico, short-hair,
|
6
|
+
and live in Oregon.
|
7
|
+
|
8
|
+
## But Why?
|
9
|
+
|
10
|
+
This problem is easy to solve for most situations - if you just need to generate a list,
|
11
|
+
you can process a csv with a very simple script or a perl one-liner. If you only have to
|
12
|
+
handle a few thousand cats, any database will do the trick - table-scans aren't *that*
|
13
|
+
costly.
|
14
|
+
|
15
|
+
But if you have millions of records, they receive frequent updates (because some of your
|
16
|
+
attributes are mutable), records are added and removed on a regular basis, and you need to
|
17
|
+
perform a continuous stream of varied requests, you may find that your queries aren't scaling
|
18
|
+
well - a *set* querying engine is one straightforward solution to that problem.
|
19
|
+
|
20
|
+
With hash-backed sets, we can perform intersections very quickly - if we simply model every
|
21
|
+
attribute as multiple sets (the set of male cats and the set of female cats, for example),
|
22
|
+
we can easily construct lists of records that match complex constraints with pure set union
|
23
|
+
and intersection operations. And Redis has that data structure ready to go!
|
data/lib/redi_set.rb
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
module RediSet
|
2
|
+
def self.prefix
|
3
|
+
configuration.prefix
|
4
|
+
end
|
5
|
+
|
6
|
+
def self.configuration
|
7
|
+
@configuration ||= Configuration.new
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.configure
|
11
|
+
yield(configuration)
|
12
|
+
end
|
13
|
+
|
14
|
+
class Configuration
|
15
|
+
attr_accessor :prefix
|
16
|
+
|
17
|
+
def initialize
|
18
|
+
@prefix = "rs"
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
require "redis"
|
24
|
+
require "hiredis"
|
25
|
+
require "redis/connection/hiredis"
|
26
|
+
|
27
|
+
require_relative "redi_set/attribute"
|
28
|
+
require_relative "redi_set/quality"
|
29
|
+
require_relative "redi_set/constraint"
|
30
|
+
require_relative "redi_set/query"
|
31
|
+
require_relative "redi_set/client"
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module RediSet
|
2
|
+
class Client
|
3
|
+
attr_reader :redis
|
4
|
+
|
5
|
+
def initialize(redis: nil, redis_config: nil)
|
6
|
+
if redis
|
7
|
+
@redis = redis
|
8
|
+
elsif redis_config
|
9
|
+
@redis = Redis.new(redis_config)
|
10
|
+
else
|
11
|
+
fail ArgumentError, "redis or redis_config must be supplied to the RediSet::Client"
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def match(constraint_hash)
|
16
|
+
RediSet::Query.from_hash(constraint_hash).execute(@redis)
|
17
|
+
end
|
18
|
+
|
19
|
+
def set_all!(attribute_name, quality_name, ids)
|
20
|
+
attribute = Attribute.new(name: attribute_name)
|
21
|
+
quality = Quality.new(attribute: attribute, name: quality_name)
|
22
|
+
quality.set_all!(@redis, ids)
|
23
|
+
end
|
24
|
+
|
25
|
+
# details here is a three-layer hash: attribute_name => quality_name => boolean.
|
26
|
+
# only specified attribute/qualities will be updated.
|
27
|
+
def set_details!(id, details)
|
28
|
+
possessed_qualities, lacked_qualities = Quality.collect_from_details(details)
|
29
|
+
@redis.multi do
|
30
|
+
possessed_qualities.each { |q| @redis.sadd(q.key, id) }
|
31
|
+
lacked_qualities.each { |q| @redis.srem(q.key, id) }
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
require "securerandom"
|
2
|
+
|
3
|
+
module RediSet
|
4
|
+
class Constraint
|
5
|
+
UNION_EXPIRATION_PERIOD = 60 # seconds
|
6
|
+
|
7
|
+
def initialize(qualities:)
|
8
|
+
@qualities = qualities
|
9
|
+
|
10
|
+
if qualities.length < 1
|
11
|
+
raise ArgumentError, "A constraint must have at least one quality"
|
12
|
+
elsif qualities.map(&:attribute).uniq.length != 1
|
13
|
+
raise ArgumentError, "All qualities in a constraint must have matching attributes"
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
attr_reader :qualities
|
18
|
+
|
19
|
+
def attribute
|
20
|
+
qualities.first.attribute
|
21
|
+
end
|
22
|
+
|
23
|
+
def requires_union?
|
24
|
+
qualities.length > 1
|
25
|
+
end
|
26
|
+
|
27
|
+
def intersection_key
|
28
|
+
if requires_union?
|
29
|
+
union_key
|
30
|
+
else
|
31
|
+
qualities.first.key
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def store_union(redis)
|
36
|
+
redis.sunionstore union_key, qualities.map(&:key)
|
37
|
+
redis.expire(union_key, UNION_EXPIRATION_PERIOD)
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
def union_key
|
43
|
+
@_key ||= "#{RediSet.prefix}.union:#{SecureRandom.uuid}"
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module RediSet
|
2
|
+
class Quality
|
3
|
+
attr_reader :attribute, :name
|
4
|
+
|
5
|
+
# parse a layered hash like "attribute_name => { quality_name => possessed? }" into a pair
|
6
|
+
# of Arrays of quality objects, one full of qualities they should possess, the other full
|
7
|
+
# of qualities they should not possess.
|
8
|
+
def self.collect_from_details(details)
|
9
|
+
qheld = []
|
10
|
+
qlacked = []
|
11
|
+
details.each_pair do |attribute_name, attribute_details|
|
12
|
+
attribute = Attribute.new(name: attribute_name)
|
13
|
+
attribute_details.each_pair do |quality_name, is_held|
|
14
|
+
quality = Quality.new(attribute: attribute, name: quality_name)
|
15
|
+
if is_held
|
16
|
+
qheld << quality
|
17
|
+
else
|
18
|
+
qlacked << quality
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
[qheld, qlacked]
|
23
|
+
end
|
24
|
+
|
25
|
+
def initialize(attribute:, name:)
|
26
|
+
@attribute = attribute
|
27
|
+
@name = name
|
28
|
+
end
|
29
|
+
|
30
|
+
def key
|
31
|
+
@_key ||= "#{RediSet.prefix}.attr:#{attribute.name}:#{name}"
|
32
|
+
end
|
33
|
+
|
34
|
+
def set_all!(redis, ids)
|
35
|
+
redis.multi do
|
36
|
+
redis.del(key)
|
37
|
+
redis.sadd(key, ids)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module RediSet
|
2
|
+
class Query
|
3
|
+
attr_reader :constraints
|
4
|
+
|
5
|
+
def self.from_hash(constraint_hash)
|
6
|
+
constraints = constraint_hash.map do |key, val|
|
7
|
+
attribute = Attribute.new(name: key)
|
8
|
+
qualities = Array(val).map { |v| Quality.new(attribute: attribute, name: v) }
|
9
|
+
Constraint.new(qualities: qualities)
|
10
|
+
end
|
11
|
+
RediSet::Query.new(constraints)
|
12
|
+
end
|
13
|
+
|
14
|
+
def initialize(constraints)
|
15
|
+
@constraints = constraints
|
16
|
+
end
|
17
|
+
|
18
|
+
def execute(redis)
|
19
|
+
redis.multi do
|
20
|
+
constraints.select(&:requires_union?).each { |c| c.store_union(redis) }
|
21
|
+
redis.sinter(constraints.map(&:intersection_key))
|
22
|
+
end.last
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
metadata
ADDED
@@ -0,0 +1,80 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: redi_set
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Eric Mueller
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2020-10-09 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: redis
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: hiredis
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
description: |
|
42
|
+
A redis-backed library that makes it easy to find members of a population that
|
43
|
+
satisfy a set of attribute constraints using redis set operations.
|
44
|
+
email: nevinera@gmail.com
|
45
|
+
executables: []
|
46
|
+
extensions: []
|
47
|
+
extra_rdoc_files: []
|
48
|
+
files:
|
49
|
+
- LICENSE
|
50
|
+
- README.md
|
51
|
+
- lib/redi_set.rb
|
52
|
+
- lib/redi_set/attribute.rb
|
53
|
+
- lib/redi_set/client.rb
|
54
|
+
- lib/redi_set/constraint.rb
|
55
|
+
- lib/redi_set/quality.rb
|
56
|
+
- lib/redi_set/query.rb
|
57
|
+
homepage: https://github.com/nevinera/redi_set
|
58
|
+
licenses:
|
59
|
+
- MIT
|
60
|
+
metadata: {}
|
61
|
+
post_install_message:
|
62
|
+
rdoc_options: []
|
63
|
+
require_paths:
|
64
|
+
- lib
|
65
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
66
|
+
requirements:
|
67
|
+
- - ">="
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: '0'
|
70
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
71
|
+
requirements:
|
72
|
+
- - ">="
|
73
|
+
- !ruby/object:Gem::Version
|
74
|
+
version: '0'
|
75
|
+
requirements: []
|
76
|
+
rubygems_version: 3.1.2
|
77
|
+
signing_key:
|
78
|
+
specification_version: 4
|
79
|
+
summary: A set-based redis query engine
|
80
|
+
test_files: []
|