queris 0.8.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (61) hide show
  1. checksums.yaml +7 -0
  2. data/Gemfile +4 -0
  3. data/Gemfile.lock +34 -0
  4. data/README.md +53 -0
  5. data/Rakefile +1 -0
  6. data/data/redis_scripts/add_low_ttl.lua +10 -0
  7. data/data/redis_scripts/copy_key_if_absent.lua +13 -0
  8. data/data/redis_scripts/copy_ttl.lua +13 -0
  9. data/data/redis_scripts/create_page_if_absent.lua +24 -0
  10. data/data/redis_scripts/debuq.lua +20 -0
  11. data/data/redis_scripts/delete_if_string.lua +8 -0
  12. data/data/redis_scripts/delete_matching_keys.lua +7 -0
  13. data/data/redis_scripts/expire_temp_query_keys.lua +7 -0
  14. data/data/redis_scripts/make_rangehack_if_needed.lua +30 -0
  15. data/data/redis_scripts/master_expire.lua +15 -0
  16. data/data/redis_scripts/match_key_type.lua +9 -0
  17. data/data/redis_scripts/move_key.lua +11 -0
  18. data/data/redis_scripts/multisize.lua +19 -0
  19. data/data/redis_scripts/paged_query_ready.lua +35 -0
  20. data/data/redis_scripts/periodic_zremrangebyscore.lua +9 -0
  21. data/data/redis_scripts/persist_reusable_temp_query_keys.lua +14 -0
  22. data/data/redis_scripts/query_ensure_existence.lua +23 -0
  23. data/data/redis_scripts/query_intersect_optimization.lua +31 -0
  24. data/data/redis_scripts/remove_from_keyspace.lua +27 -0
  25. data/data/redis_scripts/remove_from_sets.lua +13 -0
  26. data/data/redis_scripts/results_from_hash.lua +54 -0
  27. data/data/redis_scripts/results_with_ttl.lua +20 -0
  28. data/data/redis_scripts/subquery_intersect_optimization.lua +25 -0
  29. data/data/redis_scripts/subquery_intersect_optimization_cleanup.lua +5 -0
  30. data/data/redis_scripts/undo_add_low_ttl.lua +8 -0
  31. data/data/redis_scripts/unpaged_query_ready.lua +17 -0
  32. data/data/redis_scripts/unpersist_reusable_temp_query_keys.lua +11 -0
  33. data/data/redis_scripts/update_live_expiring_presence_index.lua +20 -0
  34. data/data/redis_scripts/update_query.lua +126 -0
  35. data/data/redis_scripts/update_rangehacks.lua +94 -0
  36. data/data/redis_scripts/zrangestore.lua +12 -0
  37. data/lib/queris.rb +400 -0
  38. data/lib/queris/errors.rb +8 -0
  39. data/lib/queris/indices.rb +735 -0
  40. data/lib/queris/mixin/active_record.rb +74 -0
  41. data/lib/queris/mixin/object.rb +398 -0
  42. data/lib/queris/mixin/ohm.rb +81 -0
  43. data/lib/queris/mixin/queris_model.rb +59 -0
  44. data/lib/queris/model.rb +455 -0
  45. data/lib/queris/profiler.rb +275 -0
  46. data/lib/queris/query.rb +1215 -0
  47. data/lib/queris/query/operations.rb +398 -0
  48. data/lib/queris/query/page.rb +101 -0
  49. data/lib/queris/query/timer.rb +42 -0
  50. data/lib/queris/query/trace.rb +108 -0
  51. data/lib/queris/query_store.rb +137 -0
  52. data/lib/queris/version.rb +3 -0
  53. data/lib/rails/log_subscriber.rb +22 -0
  54. data/lib/rails/request_timing.rb +29 -0
  55. data/lib/tasks/queris.rake +138 -0
  56. data/queris.gemspec +41 -0
  57. data/test.rb +39 -0
  58. data/test/current.rb +74 -0
  59. data/test/dsl.rb +35 -0
  60. data/test/ohm.rb +37 -0
  61. metadata +161 -0
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 3308c207243318c193dcc8557c6b5120bff0fb85
4
+ data.tar.gz: 0b81f79d85f4adc0fc801b671d92b342b6ebdf8f
5
+ SHA512:
6
+ metadata.gz: a59b24e659d0652dd1403682720fc75b8de4e91f57ba0013fe1da3a254de2aaa562cc9e627515e1e81272793dae2a85ab27eb101c349df624cf98c4ba3859530
7
+ data.tar.gz: 0ef348c9862862fd5a0159b57e0efa64c9479427050a67911ec8d83c87bf4c0ba1a3d4592fc2904b7e368f8c320a537bd4ec704cec23eb8240577a6ec027508e
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in queris.gemspec
4
+ gemspec
@@ -0,0 +1,34 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ queris (0.8.1)
5
+ diffy
6
+ hiredis
7
+ redis (~> 3.2.1)
8
+
9
+ GEM
10
+ remote: http://rubygems.org/
11
+ specs:
12
+ coderay (1.1.0)
13
+ diffy (3.0.7)
14
+ hiredis (0.6.0)
15
+ method_source (0.8.2)
16
+ pry (0.10.2)
17
+ coderay (~> 1.1.0)
18
+ method_source (~> 0.8.1)
19
+ slop (~> 3.4)
20
+ pry-debundle (0.8)
21
+ pry
22
+ redis (3.2.1)
23
+ slop (3.6.0)
24
+
25
+ PLATFORMS
26
+ ruby
27
+
28
+ DEPENDENCIES
29
+ pry
30
+ pry-debundle
31
+ queris!
32
+
33
+ BUNDLED WITH
34
+ 1.10.6
@@ -0,0 +1,53 @@
1
+ Nutshell
2
+ ========
3
+ A Ruby object indexing and querying tool using Redis to store data and perform set operations.
4
+
5
+ What's all this about?
6
+ ====
7
+ Redis is a flexible data structures server. It's pretty fast, but if you want to index something, you have to do it manually.
8
+ Queris can be bolted onto some Ruby objects (hashes, ActiveRecord models, Ohm objects, etc.) to maintain indices and perform queries. Attributes can be indexed into regular or sorted sets, allowing set selection on arbitrary data, and range selection on numeric data (if desired). More complex indexing, like n-gram text search, can be implemented client-side (and will be included in some future Queris version.)
9
+ Queries can perform set operations on all indices and other queries, and can be sorted. Each query can be cached for a custom duration. Queries can also be live (reflect realtime changes to complex set operations) in O(log(n)) server time.
10
+ Thus Queris offers customizeable indexing, and nestable, cacheable, optionally realtime, set queries (with a few more bells and whistles).
11
+
12
+ Learning By Example
13
+ ===================
14
+ First connect to redis. Let's assume a Redis server on localhost at standard port 6379:
15
+ ```ruby
16
+ Queris.add_redis Redis.new
17
+ ```
18
+ Let's say you have a User ActiveRecord model with some obvious attributes - id, name, email, age. Let's also assume a UserTags model, linked to User by its userId attribute.
19
+ ```ruby
20
+ class User < ActiveRecord::Base
21
+ include Queris
22
+
23
+ #declare which attributes to index, and how
24
+ index_attributes :name, :email #simple set index
25
+ index_range_attribute :age #sorted set index
26
+ index_attribute_from model: UserTags, attribute: :tag, key: :userId #index from a different model
27
+ end
28
+ ```
29
+ To build indices:
30
+ ```ruby
31
+ User.build_redis_indices
32
+ ```
33
+ This will load all current User objects into memory and index said objects. You currently need enough space to keep the entire dataset in memory for building indices. (Future versions will be able to import data incrementally.)
34
+
35
+ You can now query Users:
36
+ ```ruby
37
+ young_bob_and_steve = User.query(:ttl=>2.days).union(:name, ["Steve", "Bob"]).intersect(:age, 0..30).diff(:email, "steve@example.org") #query expires in 2 days
38
+ #you can have subqueries, too:
39
+ bob_and_steve_and_bill = User.query.union(young_bob_and_steve).union(:name, "Bill")
40
+ #now get the results
41
+ bob_and_steve_and_bill.results #all results
42
+ bob_and_steve_and_bill.count #result count
43
+ bob_and_steve_and_bill.results(3..5) # results # 3 through 5
44
+ ```
45
+ Queries can be expressed in set notation:
46
+ ```ruby
47
+ bob_and_steve_and_bill.to_s # same as .explain
48
+ # ((name<["Steve", "Bob"]> ∩ age<0..30> ∖ email<steve@example.org>) ∪ name<Bill>)
49
+ ```
50
+
51
+ Note that query operations are applied sequentially (with no complex operator precedence rules), but can be arranged or grouped by the use of subqueries.
52
+
53
+ ...more to follow...
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -0,0 +1,10 @@
1
+ local min_ttl=tonumber(ARGV[1])
2
+ local keys=KEYS
3
+ local state=table.remove(keys) --last key is state key
4
+ for i, k in ipairs(keys) do
5
+ local ttl = tonumber(redis.call('ttl', k))
6
+ if ttl>0 and ttl < min_ttl then
7
+ redis.call('expire', k, ttl + min_ttl)
8
+ table.sadd(state, k)
9
+ end
10
+ end
@@ -0,0 +1,13 @@
1
+ local dst, src = KEYS[1], KEYS[2]
2
+ local t = redis.call('type', src).ok
3
+ if t == 'zset' then
4
+ redis.call('zunionstore', dst, 1, src)
5
+ elseif t == 'set' then
6
+ redis.call('sunionstore', dst, 1, src)
7
+ elseif t == 'string' then
8
+ redis.call('set', dst, redis.call('get', src))
9
+ elseif t == 'none' then
10
+ --noop
11
+ else
12
+ redis.log(redis.LOG_WARNING, "copy_key_if_absent: unsupported key type " .. t .. ".")
13
+ end
@@ -0,0 +1,13 @@
1
+ if redis.call('exists', KEYS[2]) == 0 then
2
+ return nil
3
+ end
4
+ local ttl, delete_if_absent = redis.call("ttl", KEYS[1]), (ARGV[1] or false)
5
+ if ttl ~= -1 then
6
+ redis.call("expire", KEYS[2], ttl)
7
+ return ttl
8
+ elseif delete_if_absent and redis.call('exists', KEYS[1]) == 0 then
9
+ if redis.call("del", KEYS[2]) == 1 then
10
+ return "DELETED"
11
+ end
12
+ end
13
+ return 'OK'
@@ -0,0 +1,24 @@
1
+ local dst, source = KEYS[1], KEYS[2]
2
+ local min, max = ARGV[1], ARGV[2]
3
+ local dst_type = redis.call('type', dst).ok
4
+ redis.log(redis.LOG_WARNING, "want a page at " .. dst .. " from " .. source)
5
+ if dst_type == 'string' then
6
+ redis.call('del', dst)
7
+ elseif dst_type == 'zset' then
8
+ redis.log(redis.LOG_WARNING, 'page ' .. dst .. ' already exists with size ' .. redis.call('zcard', dst))
9
+ return redis.call('zcard', dst)
10
+ end
11
+
12
+ local res = redis.call("zrange", source, min, max, "withscores")
13
+ local id, score
14
+ if next(res) ~= nil then
15
+ redis.call('del', dst)
16
+ end
17
+ for i,v in ipairs(res) do
18
+ if i%2==1 then id=v; else
19
+ score=v
20
+ redis.call('zadd', dst, score, id)
21
+ end
22
+ end
23
+ redis.log(redis.LOG_WARNING, 'pagesize of ' .. dst .. ': ' .. redis.call('zcard', dst))
24
+ return #res/2
@@ -0,0 +1,20 @@
1
+ local cmd=ARGV[1]
2
+ local keys = KEYS
3
+
4
+ local info = function(k)
5
+ local t = redis.call('type', k).ok
6
+ if t == 'zset' then
7
+ return "zset size=" .. redis.call('zcard', k)
8
+ elseif t== 'set' then
9
+ return "set size=" .. redis.call('scard', k)
10
+ elseif t== 'string' then
11
+ return "string = " .. redis.call('get', k)
12
+ else
13
+ return t
14
+ end
15
+ end
16
+
17
+ redis.log(redis.LOG_WARNING, "info for " ..cmd.. ": " .. #KEYS .. " keys")
18
+ for k,v in pairs(keys) do
19
+ redis.log(redis.LOG_WARNING, (" " .. v .. ": " .. info(v)))
20
+ end
@@ -0,0 +1,8 @@
1
+ local count = 0
2
+ for i,key in ipairs(KEYS) do
3
+ if redis.call('type', key).ok == "string" then
4
+ redis.call('del', key)
5
+ count = count + 1
6
+ end
7
+ end
8
+ return count
@@ -0,0 +1,7 @@
1
+ local keymatch = ARGV[1]
2
+ local keys = redis.call('keys', keymatch)
3
+ local deleted = 0
4
+ for i,key in ipairs(keys) do
5
+ deleted = deleted + redis.call('del', key)
6
+ end
7
+ return deleted
@@ -0,0 +1,7 @@
1
+ local ttl = ARGV[1]
2
+ for i,k in ipairs(KEYS) do
3
+ if redis.call('exists', k) ~= 1 then
4
+ redis.call('setnx', k, 1337)
5
+ redis.call('expire', k, ttl)
6
+ end
7
+ end
@@ -0,0 +1,30 @@
1
+ local dst, src, rangehack_set_key, not_needed_key = KEYS[1], KEYS[2], KEYS[3], KEYS[4]
2
+ local min, max, exclude_max = tonumber(ARGV[1]), tonumber(ARGV[2]), ARGV
3
+ redis.log(redis.LOG_WARNING, "want rangehack ("..min..","..max..") for " .. src)
4
+ if redis.call('exists', not_needed_key) == 1 then
5
+ --nevermind, we don't need to run this
6
+ redis.log(redis.LOG_WARNING, "nevermind, the query doesn't need it..")
7
+ return
8
+ end
9
+ local opf = "(%s" --)
10
+ local t = redis.call('type', dst).ok
11
+ if t == 'zset' then
12
+ --already exists, nothing to do
13
+ redis.log(redis.LOG_WARNING, "it's already here.")
14
+ return
15
+ else
16
+ redis.call('zunionstore', dst, 1, src) --slow as a fat, barbed turd
17
+ --remove inverse range
18
+ redis.call('zremrangebyscore', dst, '-inf', opf:format(min))
19
+ redis.call('zremrangebyscore', dst, (exclude_max and max or opf:format(max)), 'inf')
20
+
21
+ --add to rangehack 'index'
22
+ redis.call('sadd', rangehack_set_key, dst)
23
+
24
+ redis.log(redis.LOG_WARNING, "made one at " .. dst .. " with size " .. redis.call('zcard', dst))
25
+ if redis.call('zcard', dst) == 0 then
26
+ --dummy one-element zset
27
+ redis.call('zadd', dst, 0, "=-{*_*}-= <- the nullset ghost")
28
+ end
29
+ redis.log(redis.LOG_WARNING, "made one at " .. dst .. " with size " .. redis.call('zcard', dst))
30
+ end
@@ -0,0 +1,15 @@
1
+ local ttl, renew, dummy = ARGV[1], ARGV[2], ARGV[3]
2
+ redis.log(redis.LOG_WARNING, "master_expire keys [" .. table.concat(KEYS, ', ') .. "] with ttl " .. ttl .. " renew: " .. (renew and "yes" or "no") .. " dummy: " .. (dummy and "yes" or "no"))
3
+ for i,k in ipairs(KEYS) do
4
+ local exists = redis.call('exists', k) == 1
5
+ if not exists and dummy then
6
+ exists = 1
7
+ redis.call('setnx', k, 'dummy')
8
+ end
9
+ if exists then
10
+ local current_ttl = redis.call('ttl', k)
11
+ if current_ttl < 0 or renew then
12
+ redis.call('expire', k, ttl)
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,9 @@
1
+ local key = KEYS[1]
2
+ local acceptable_types = ARGV
3
+ local keytype = redis.call('type', key).ok
4
+ for i, t in ipairs(acceptable_types) do
5
+ if t == keytype then
6
+ return true
7
+ end
8
+ end
9
+ return false
@@ -0,0 +1,11 @@
1
+ local oldkey, newkey = KEYS[1], KEYS[2]
2
+ if redis.call('exists', oldkey) == 1 then
3
+ redis.call('rename', oldkey, newkey)
4
+ return 1
5
+ else
6
+ --oldkey is empty, so we move that emptiness to newkey
7
+ if redis.call('exists', newkey) == 1 then
8
+ redis.call('del', newkey) --clear newkey
9
+ end
10
+ return 0
11
+ end
@@ -0,0 +1,19 @@
1
+ local key = KEYS[1]
2
+ local keytype = redis.call('type', key).ok
3
+
4
+ if keytype=='set' then
5
+ return redis.call('scard', key)
6
+ elseif keytype=='zset' then
7
+ return redis.call('zcard', key)
8
+ elseif keytype=='list' then
9
+ return redis.call('llen', key)
10
+ elseif keytype=='hash' then
11
+ return redis.call('hlen', key)
12
+ elseif keytype=='none' then
13
+ return 0
14
+ elseif keytype=='string' then
15
+ return redis.call('strlen', key)
16
+ else
17
+ redis.log(redis.LOG_WARNING, "I wonder what the size of key " .. key .. "(" .. keytype .. ")".. "is?")
18
+ return "WTF for " .. keytype
19
+ end
@@ -0,0 +1,35 @@
1
+ local results_key, exists_key, sort_key, page_key, runstate_key = KEYS[1], KEYS[2], KEYS[3], KEYS[4], KEYS[5]
2
+ local pagesize, min, max = tonumber(ARGV[1]), tonumber(ARGV[2]), tonumber(ARGV[3])
3
+
4
+ if redis.call('exists', exists_key) ~= 1 then
5
+ return nil
6
+ end
7
+
8
+ local last_loaded_page = redis.call('get', page_key)
9
+
10
+ if not last_loaded_page then
11
+ return nil
12
+ end
13
+
14
+ local t = redis.call('type', results_key).ok
15
+ local current_count
16
+ if t == 'zset' then
17
+ current_count = redis.call('zcard', results_key)
18
+ elseif t=='set' then
19
+ current_count = redis.call('scard', results_key)
20
+ else
21
+ current_count = 0
22
+ end
23
+
24
+ if current_count > max then
25
+ if runstate_key then
26
+ redis.call('set', runstate_key, 1)
27
+ end
28
+ return true
29
+ elseif (last_loaded_page + 1) * pagesize >= redis.call('zcard', sort_key) then
30
+ --we've reached the end
31
+ if runstate_key then
32
+ redis.call('set', runstate_key, 1)
33
+ end
34
+ return true
35
+ end
@@ -0,0 +1,9 @@
1
+ local ttl = tonumber(ARGV[1])
2
+ local key = KEYS[1]
3
+ local timer_key = key .. ":timer"
4
+ if redis.call('exists', timer_key) == 1 then
5
+ return 0
6
+ else
7
+ redis.call('setex', timer_key, ttl, "don't do it as long as i exist")
8
+ end
9
+ return redis.call('zremrangebyscore', key, ARGV[2], ARGV[3])
@@ -0,0 +1,14 @@
1
+ local dst=table.remove(KEYS, 1)
2
+ if redis.call('exists', dst) == 1 then
3
+ redis.log(redis.LOG_WARNING, "persist_reusable_temp_query_keys storage key " .. dst .. " already exist. WTF?")
4
+ redis.call('del', dst)
5
+ end
6
+ for i,k in ipairs(KEYS) do
7
+ local t= redis.call('type', k).ok
8
+ if t ~= 'none' then
9
+ ttl = redis.call('ttl', k)
10
+ redis.call('persist', k)
11
+ redis.zadd(dst, ttl, k)
12
+ redis.log(redis.LOG_WARNING, "persisted key " .. k .. " ttl=" .. ttl)
13
+ end
14
+ end
@@ -0,0 +1,23 @@
1
+ local existence_key, results_key = KEYS[1], KEYS[2]
2
+ local ttl, min_ttl, master_only = ARGV[1], tonumber(ARGV[2]), ARGV[3]=='true'
3
+ if redis.call('exists', existence_key) == 1 then
4
+ for i, k in ipairs(KEYS) do
5
+ if min_ttl then
6
+ local key_ttl = redis.call('ttl', k)
7
+ if key_ttl < min_ttl then
8
+ redis.call('expire', k, min_ttl)
9
+ end
10
+ else
11
+ redis.call('expire', k, ttl)
12
+ end
13
+ end
14
+ if not master_only then
15
+ return true
16
+ else
17
+ return redis.call('type', results_key).ok ~= 'string'
18
+ end
19
+ else
20
+ redis.call('setnx', results_key, 1)
21
+ redis.call('expire', results_key, ttl)
22
+ return false
23
+ end
@@ -0,0 +1,31 @@
1
+ local dstkey, key, smallkey = KEYS[1], KEYS[2], KEYS[3]
2
+ local ttl = ARGV[1] or 120
3
+ local t=redis.call('type', dstkey).ok
4
+
5
+ local multisize = function(key)
6
+ local keytype = redis.call('type', key).ok
7
+ if keytype=='set' then
8
+ return "set card=" .. redis.call('scard', key)
9
+ elseif keytype=='zset' then
10
+ return "zset card=" .. redis.call('zcard', key)
11
+ elseif keytype=='list' then
12
+ return "list card=" .. redis.call('llen', key)
13
+ elseif keytype=='hash' then
14
+ return "hash card=" .. redis.call('hlen', key)
15
+ elseif keytype=='none' then
16
+ return "none"
17
+ elseif keytype=='string' then
18
+ return "string length=" .. redis.call('strlen', key)
19
+ else
20
+ return "WTF for " .. keytype
21
+ end
22
+ end
23
+ if t ~= 'zset' then
24
+ redis.call('zinterstore', dstkey, 2, key, smallkey, 'weights', 1, 0)
25
+ redis.call('expire', dstkey, ttl)
26
+ for i,v in pairs({dstkey, key, smallkey}) do
27
+ redis.log(redis.LOG_WARNING, " inter: " .. v .. ": " .. multisize(v))
28
+ end
29
+ else
30
+ redis.log(redis.LOG_WARNING, "intersectoptimization " .. dstkey .. " already exists")
31
+ end
@@ -0,0 +1,27 @@
1
+ local element, keysearch, ttl = ARGV[1], ARGV[2], tonumber(ARGV[3])
2
+ local keys_key = "Queris:removing_from_keyspace:" .. keysearch
3
+ local keys
4
+
5
+ --relevant keys
6
+ if redis.call('zcard', keys_key) > 0 then
7
+ keys = redis.call('zrange', keys_key, 0, -1)
8
+ else
9
+ keys = redis.call('keys', keysearch)
10
+ if #keys > 0 then
11
+ for i,v in ipairs(keys) do
12
+ redis.call('zadd', keys_key, 0, v)
13
+ end
14
+ redis.call('expire', keys_key, ttl)
15
+ end
16
+ end
17
+
18
+ local removed = 0
19
+ for i,key in ipairs(keys) do
20
+ local keytype= redis.call('type', key).ok
21
+ if keytype == 'zset' then
22
+ removed = removed + redis.call('zrem', key, element)
23
+ elseif keytype == 'set' then
24
+ removed = removed + redis.call('srem', key, element)
25
+ end
26
+ end
27
+ return removed