redi_set 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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.
@@ -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!
@@ -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,9 @@
1
+ module RediSet
2
+ class Attribute
3
+ attr_reader :name
4
+
5
+ def initialize(name:)
6
+ @name = name
7
+ end
8
+ end
9
+ end
@@ -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: []