queris 0.8.1
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/Gemfile +4 -0
- data/Gemfile.lock +34 -0
- data/README.md +53 -0
- data/Rakefile +1 -0
- data/data/redis_scripts/add_low_ttl.lua +10 -0
- data/data/redis_scripts/copy_key_if_absent.lua +13 -0
- data/data/redis_scripts/copy_ttl.lua +13 -0
- data/data/redis_scripts/create_page_if_absent.lua +24 -0
- data/data/redis_scripts/debuq.lua +20 -0
- data/data/redis_scripts/delete_if_string.lua +8 -0
- data/data/redis_scripts/delete_matching_keys.lua +7 -0
- data/data/redis_scripts/expire_temp_query_keys.lua +7 -0
- data/data/redis_scripts/make_rangehack_if_needed.lua +30 -0
- data/data/redis_scripts/master_expire.lua +15 -0
- data/data/redis_scripts/match_key_type.lua +9 -0
- data/data/redis_scripts/move_key.lua +11 -0
- data/data/redis_scripts/multisize.lua +19 -0
- data/data/redis_scripts/paged_query_ready.lua +35 -0
- data/data/redis_scripts/periodic_zremrangebyscore.lua +9 -0
- data/data/redis_scripts/persist_reusable_temp_query_keys.lua +14 -0
- data/data/redis_scripts/query_ensure_existence.lua +23 -0
- data/data/redis_scripts/query_intersect_optimization.lua +31 -0
- data/data/redis_scripts/remove_from_keyspace.lua +27 -0
- data/data/redis_scripts/remove_from_sets.lua +13 -0
- data/data/redis_scripts/results_from_hash.lua +54 -0
- data/data/redis_scripts/results_with_ttl.lua +20 -0
- data/data/redis_scripts/subquery_intersect_optimization.lua +25 -0
- data/data/redis_scripts/subquery_intersect_optimization_cleanup.lua +5 -0
- data/data/redis_scripts/undo_add_low_ttl.lua +8 -0
- data/data/redis_scripts/unpaged_query_ready.lua +17 -0
- data/data/redis_scripts/unpersist_reusable_temp_query_keys.lua +11 -0
- data/data/redis_scripts/update_live_expiring_presence_index.lua +20 -0
- data/data/redis_scripts/update_query.lua +126 -0
- data/data/redis_scripts/update_rangehacks.lua +94 -0
- data/data/redis_scripts/zrangestore.lua +12 -0
- data/lib/queris.rb +400 -0
- data/lib/queris/errors.rb +8 -0
- data/lib/queris/indices.rb +735 -0
- data/lib/queris/mixin/active_record.rb +74 -0
- data/lib/queris/mixin/object.rb +398 -0
- data/lib/queris/mixin/ohm.rb +81 -0
- data/lib/queris/mixin/queris_model.rb +59 -0
- data/lib/queris/model.rb +455 -0
- data/lib/queris/profiler.rb +275 -0
- data/lib/queris/query.rb +1215 -0
- data/lib/queris/query/operations.rb +398 -0
- data/lib/queris/query/page.rb +101 -0
- data/lib/queris/query/timer.rb +42 -0
- data/lib/queris/query/trace.rb +108 -0
- data/lib/queris/query_store.rb +137 -0
- data/lib/queris/version.rb +3 -0
- data/lib/rails/log_subscriber.rb +22 -0
- data/lib/rails/request_timing.rb +29 -0
- data/lib/tasks/queris.rake +138 -0
- data/queris.gemspec +41 -0
- data/test.rb +39 -0
- data/test/current.rb +74 -0
- data/test/dsl.rb +35 -0
- data/test/ohm.rb +37 -0
- metadata +161 -0
@@ -0,0 +1,735 @@
|
|
1
|
+
require "securerandom"
|
2
|
+
module Queris
|
3
|
+
# These here are various queries 'indices'. All indices must subclass
|
4
|
+
# Queris::Index. Some are straight-up indices, mapping values
|
5
|
+
# (hashed when possible) to object ids.
|
6
|
+
# others perform caching functions, yet others maintain some state
|
7
|
+
# (and cannot be rebuilt)
|
8
|
+
|
9
|
+
class Index
|
10
|
+
|
11
|
+
class Error < StandardError
|
12
|
+
end
|
13
|
+
|
14
|
+
attr_accessor :name, :redis, :model, :attribute, :live, :delta_ttl
|
15
|
+
#live queries is implemented through a time-indexed sorted set of changed objects as they relate to a given live index.
|
16
|
+
alias :live? :live
|
17
|
+
DELTA_TTL = 172800 #max time to keep old live query changeset elements around
|
18
|
+
def initialize(arg={})
|
19
|
+
arg.each do |opt, val|
|
20
|
+
instance_variable_set "@#{opt}".to_sym, val
|
21
|
+
end
|
22
|
+
@redis = arg[:redis]
|
23
|
+
@name ||= @attribute #user-facing index name
|
24
|
+
@attribute ||= @name #attribute or method, as long as it exists
|
25
|
+
@attribute = @attribute.to_sym unless !@attribute
|
26
|
+
@name = @name.to_sym
|
27
|
+
@key ||= :id #object's key attribute (default is 'id') used to generate redis key
|
28
|
+
@keyf ||= "%s#{self.class.name.sub(/^.*::/, "")}:#{@name}=%s"
|
29
|
+
@delta_ttl = (arg[:delta_ttl] || arg[:delta_element_ttl] || arg[:changeset_ttl] || self.class::DELTA_TTL).to_i
|
30
|
+
live_delta_key
|
31
|
+
if block_given?
|
32
|
+
yield self, arg
|
33
|
+
end
|
34
|
+
raise ArgumentError, "Index must have a name" unless @name
|
35
|
+
raise ArgumentError, "Index must have a model" unless @model
|
36
|
+
@model.add_redis_index self
|
37
|
+
end
|
38
|
+
def key_attr #object's key (usually 'id')
|
39
|
+
@key
|
40
|
+
end
|
41
|
+
|
42
|
+
def poke(rds=nil, arg=nil)
|
43
|
+
self
|
44
|
+
end
|
45
|
+
|
46
|
+
#needed for server-side query storage
|
47
|
+
def json_redis_dump(hash={})
|
48
|
+
hash
|
49
|
+
end
|
50
|
+
def redis(obj=nil)
|
51
|
+
r=@redis || @model.redis || Queris.redis(:index, :slave, :master) || (!obj.nil? && obj.redis)
|
52
|
+
raise ClientError, "No redis connection found for Queris Index #{name}" unless r
|
53
|
+
r
|
54
|
+
end
|
55
|
+
|
56
|
+
#MAINTENANCE OPERATION -- DO NOT USE IN PRODUCTION CODE
|
57
|
+
#get all keys associated with an index
|
58
|
+
def keys
|
59
|
+
if keypattern
|
60
|
+
mykeys = (redis || model.redis).keys keypattern
|
61
|
+
mykeys << live_delta_key if live
|
62
|
+
mykeys
|
63
|
+
else
|
64
|
+
[]
|
65
|
+
end
|
66
|
+
end
|
67
|
+
def keypattern
|
68
|
+
@keypattern ||= key('*', nil, true) if respond_to? :key
|
69
|
+
end
|
70
|
+
|
71
|
+
def temp_keys(val) #any temporary keys for index at given value?
|
72
|
+
[]
|
73
|
+
end
|
74
|
+
|
75
|
+
#info about data distribution in an index
|
76
|
+
def distribution
|
77
|
+
k = keys
|
78
|
+
counts = (redis || model.redis).multi do |r|
|
79
|
+
k.each do |thiskey|
|
80
|
+
r.evalsha Queris.script_hash(:multisize), [thiskey]
|
81
|
+
end
|
82
|
+
end
|
83
|
+
Hash[k.zip counts]
|
84
|
+
end
|
85
|
+
def info
|
86
|
+
keycounts = distribution.values
|
87
|
+
ret="#{name}: #{keycounts.reduce(0){|a,b| a+b if Numeric === a && Numeric === b}} ids in #{keycounts.count} redis keys."
|
88
|
+
if live?
|
89
|
+
#get delta set size
|
90
|
+
delta_size = (redis || model.redis).zcard live_delta_key
|
91
|
+
ret << " |live delta set|=#{delta_size}"
|
92
|
+
if delta_size > 0
|
93
|
+
last_el = (redis || model.redis).zrevrange(live_delta_key, 0, 0, :with_scores => true).first
|
94
|
+
ret << " updated at: #{last_el.last}"
|
95
|
+
end
|
96
|
+
end
|
97
|
+
ret
|
98
|
+
end
|
99
|
+
|
100
|
+
def exists?; keys.count > 0 ? keys.count : nil; end
|
101
|
+
def erase!
|
102
|
+
mykeys = keys
|
103
|
+
model.redis.multi do |r|
|
104
|
+
mykeys.each {|k| r.del k}
|
105
|
+
end
|
106
|
+
mykeys.count
|
107
|
+
end
|
108
|
+
def self.skip_create?; false; end
|
109
|
+
def skip_create?
|
110
|
+
@skip_create || self.class.skip_create?
|
111
|
+
end
|
112
|
+
def skip_update?
|
113
|
+
@skip_update
|
114
|
+
end
|
115
|
+
def skip_delete?
|
116
|
+
@skip_delete
|
117
|
+
end
|
118
|
+
#is it possible to update this index incrementally?
|
119
|
+
def incremental?
|
120
|
+
false
|
121
|
+
end
|
122
|
+
def stateless?
|
123
|
+
true
|
124
|
+
end
|
125
|
+
def usable_as_results?(val=nil)
|
126
|
+
not (Enumerable === val)
|
127
|
+
end
|
128
|
+
#can the index correctly be ranged over many values for q query?
|
129
|
+
def handle_range?
|
130
|
+
false
|
131
|
+
end
|
132
|
+
#processed index value
|
133
|
+
def val(value)
|
134
|
+
@value.nil? ? value : @value.call(value)
|
135
|
+
end
|
136
|
+
#processed indexed value, applied only when indexing objects and never applied to query values
|
137
|
+
def index_val(value, obj=nil)
|
138
|
+
(@index_value || @value).nil? ? value : (@index_value || @value).call(value, obj)
|
139
|
+
end
|
140
|
+
def digest(value)
|
141
|
+
Queris.digest(value.to_s)
|
142
|
+
end
|
143
|
+
def value_is(obj)
|
144
|
+
obj.send @attribute
|
145
|
+
end
|
146
|
+
def value_was(obj)
|
147
|
+
msg = "#{@attribute}_was"
|
148
|
+
obj.send msg if obj.respond_to? msg
|
149
|
+
end
|
150
|
+
def value_diff(obj)
|
151
|
+
obj.attribute_diff @attribute if obj.respond_to? :attribute_diff
|
152
|
+
end
|
153
|
+
def live_delta_key
|
154
|
+
@live_delta_key||="#{@redis_prefix||@model.prefix}#{self.class.name.split('::').last}:#{@name}:live_delta"
|
155
|
+
end
|
156
|
+
def no_live_update
|
157
|
+
@skip_live_update = true
|
158
|
+
ret = yield
|
159
|
+
@skip_live_update = nil
|
160
|
+
ret
|
161
|
+
end
|
162
|
+
def update_live_delta(obj, r=nil)
|
163
|
+
r ||= redis
|
164
|
+
if live? && !@skip_live_update
|
165
|
+
okey=obj.send @key
|
166
|
+
r.zadd live_delta_key, Time.now.utc.to_f, obj.send(@key)
|
167
|
+
r.expire live_delta_key, @delta_ttl
|
168
|
+
Queris.run_script :periodic_zremrangebyscore, r, [live_delta_key], [(@delta_ttl/2), '-inf', (Time.now.utc.to_f - @delta_ttl)]
|
169
|
+
end
|
170
|
+
end
|
171
|
+
def update(obj)
|
172
|
+
val_is, val_was = value_is(obj), value_was(obj)
|
173
|
+
if(val_is != val_was)
|
174
|
+
no_live_update do
|
175
|
+
remove(obj, val_was) unless val_was.nil?
|
176
|
+
add(obj) unless val_is.nil?
|
177
|
+
end
|
178
|
+
update_live_delta obj
|
179
|
+
end
|
180
|
+
end
|
181
|
+
def create(obj)
|
182
|
+
add(obj)
|
183
|
+
end
|
184
|
+
def delete(obj)
|
185
|
+
remove(obj, value_was(obj))
|
186
|
+
end
|
187
|
+
#remove from all possible index keys, instead of relying on current value. uses KEYS command, is slow.
|
188
|
+
def eliminate(obj)
|
189
|
+
redis.evalsha Queris.script_hash(:remove_from_keyspace), [], [obj.send(@key), keypattern, 480]
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
class HashCache < Index
|
194
|
+
#maintains a cached copy of an object with separetely marshaled attributes in redis
|
195
|
+
def initialize(arg={})
|
196
|
+
@name= "#{arg[:attribute] || "all_attribute"}_hashcache"
|
197
|
+
super arg
|
198
|
+
@attribute= arg[:attribute]
|
199
|
+
raise ClientError, "Model not passed to index." unless @model
|
200
|
+
@name=@model.to_s #whatever, name's not important.
|
201
|
+
end
|
202
|
+
|
203
|
+
#don't add this index to the list of indices to be built when calling Queris.rebuild!
|
204
|
+
def self.skip_create?; true; end
|
205
|
+
|
206
|
+
def hash_key(obj, prefix=nil, raw_val=false)
|
207
|
+
if raw_val
|
208
|
+
id = obj
|
209
|
+
else
|
210
|
+
id = obj.kind_of?(@model) ? obj.send(@key) : obj
|
211
|
+
end
|
212
|
+
(@keyf) %[prefix || @redis_prefix || @model.prefix, id]
|
213
|
+
end
|
214
|
+
alias :key :hash_key
|
215
|
+
def update(obj)
|
216
|
+
changed_attrs = obj.changed_cacheable_attributes
|
217
|
+
if @attribute.nil?
|
218
|
+
cache_attributes obj, changed_attrs unless changed_attrs.length == 0
|
219
|
+
elsif changed_attrs.member? @attribute
|
220
|
+
cache_attributes obj, @attribute => send(@attribute)
|
221
|
+
end
|
222
|
+
end
|
223
|
+
def create(obj)
|
224
|
+
if @attriute.nil?
|
225
|
+
cache_attributes obj, obj.all_cacheable_attributes
|
226
|
+
elsif not obj.call(@attribute).nil?
|
227
|
+
cache_attributes obj, @attribute => send(@attribute)
|
228
|
+
end
|
229
|
+
|
230
|
+
end
|
231
|
+
|
232
|
+
def delete(obj)
|
233
|
+
redis(obj).del hash_key obj
|
234
|
+
end
|
235
|
+
|
236
|
+
def fetch(id, opt={})
|
237
|
+
if @attribute.nil?
|
238
|
+
hash = (opt[:redis] || Queris.redis(:slave, :master)).hgetall hash_key id
|
239
|
+
|
240
|
+
loaded = load_cached hash
|
241
|
+
if hash && loaded.nil? #cache data invalid. delete.
|
242
|
+
Queris.redis(:master).del(hash_key id)
|
243
|
+
end
|
244
|
+
loaded
|
245
|
+
else
|
246
|
+
return (opt[:redis] || Queris.redis(:slave, :master)).hget hash_key(id), @attribute
|
247
|
+
end
|
248
|
+
end
|
249
|
+
|
250
|
+
def load_cached(marshaled_hash)
|
251
|
+
@cached_attr_count ||= (not @attribute.nil?) ? 1 : @model.new.all_cacheable_attributes.length #this line could be a problem if more cacheable attributes are added after the first fetch.
|
252
|
+
if marshaled_hash.length >= @cached_attr_count
|
253
|
+
unmarshaled = {}
|
254
|
+
marshaled_hash.each_with_index do |v|
|
255
|
+
unmarshaled[v.first.to_sym]=Marshal.load v.last
|
256
|
+
end
|
257
|
+
obj= @model.new
|
258
|
+
begin
|
259
|
+
obj.assign_attributes(unmarshaled, :without_protection => true)
|
260
|
+
rescue Exception => e
|
261
|
+
#cache load failed because the data was invalid.
|
262
|
+
return nil
|
263
|
+
end
|
264
|
+
obj.instance_eval do
|
265
|
+
@new_record= false
|
266
|
+
@changed_attributes={}
|
267
|
+
end
|
268
|
+
obj
|
269
|
+
else
|
270
|
+
nil
|
271
|
+
end
|
272
|
+
end
|
273
|
+
|
274
|
+
alias :load :fetch
|
275
|
+
|
276
|
+
def info
|
277
|
+
keycounts = distribution.values
|
278
|
+
"HashCache: #{keycounts.count} objects cached."
|
279
|
+
end
|
280
|
+
|
281
|
+
private
|
282
|
+
def cache_attributes(obj, attrs)
|
283
|
+
key = hash_key obj
|
284
|
+
marshaled = {}
|
285
|
+
attrs.each do |v|
|
286
|
+
marshaled[v]=Marshal.dump obj.send(v)
|
287
|
+
end
|
288
|
+
redis.mapped_hmset key, marshaled
|
289
|
+
end
|
290
|
+
end
|
291
|
+
|
292
|
+
class SearchIndex < Index
|
293
|
+
#basic set index
|
294
|
+
def initialize(arg={})
|
295
|
+
super arg
|
296
|
+
@type ||= "string"
|
297
|
+
raise ClientError, "Model not passed to index." unless @model
|
298
|
+
end
|
299
|
+
|
300
|
+
def set_key(value, prefix=nil, raw_val=false)
|
301
|
+
if Enumerable === value
|
302
|
+
value.map { |val| set_key val, prefix, raw_val }
|
303
|
+
else
|
304
|
+
(@keyf) %[prefix || @redis_prefix || @model.prefix, raw_val ? value : digest(val value)]
|
305
|
+
end
|
306
|
+
end
|
307
|
+
alias :key :set_key
|
308
|
+
alias :key_for_query :key
|
309
|
+
def add(obj, value = nil)
|
310
|
+
value = index_val( value || obj.send(@attribute), obj)
|
311
|
+
#obj_id = obj.send(@key)
|
312
|
+
#raise "val too short" if !obj_id || (obj.respond_to?(:empty?) && obj.empty?)
|
313
|
+
if value.kind_of?(Enumerable)
|
314
|
+
value.each{|val| redis(obj).sadd set_key(val), obj.send(@key)}
|
315
|
+
else
|
316
|
+
redis(obj).sadd set_key(value), obj.send(@key)
|
317
|
+
end
|
318
|
+
update_live_delta obj
|
319
|
+
#redis(obj).eval "redis.log(redis.LOG_WARNING, 'added #{obj.id} to #{name} at #{value}, key #{key(value)}')"
|
320
|
+
end
|
321
|
+
def remove(obj, value = nil)
|
322
|
+
value = index_val( value || obj.send(@attribute), obj)
|
323
|
+
(value.kind_of?(Enumerable) ? value : [ value ]).each do |val|
|
324
|
+
redis(obj).srem set_key(val.nil? ? obj.send(@attribute) : val), obj.send(@key)
|
325
|
+
#redis(obj).eval "redis.log(redis.LOG_WARNING, 'removed #{obj.id} from #{name} at #{value}, key #{set_key(val.nil? ? obj.send(@attribute) : val)}')"
|
326
|
+
end
|
327
|
+
update_live_delta obj
|
328
|
+
end
|
329
|
+
def key_size(redis_key, r=nil)
|
330
|
+
(r || redis).scard redis_key
|
331
|
+
end
|
332
|
+
end
|
333
|
+
|
334
|
+
class ForeignIndex < SearchIndex
|
335
|
+
#this foreign index wrapper has caused some problems in the past.
|
336
|
+
#it's annoying to work with and should be retired in favor of a transparent
|
337
|
+
#foreign index proxy object
|
338
|
+
attr_accessor :real_index
|
339
|
+
def initialize(arg)
|
340
|
+
raise ArgumentError, "Missing required initialization attribute real_index for ForeignIndex." unless arg[:real_index]
|
341
|
+
super arg
|
342
|
+
end
|
343
|
+
def create(*a) end
|
344
|
+
alias :delete :create
|
345
|
+
alias :update :create
|
346
|
+
alias :eliminate :create
|
347
|
+
%w(set_key key key_for_query live_delta_key skip_create? exists? keys update_live_delta key_size erase!).each do |methname|
|
348
|
+
define_method methname do |*arg|
|
349
|
+
@real_index.send methname, *arg
|
350
|
+
end
|
351
|
+
end
|
352
|
+
def foreign_id(obj)
|
353
|
+
obj.send(@key)
|
354
|
+
end
|
355
|
+
end
|
356
|
+
|
357
|
+
class PresenceIndex < SearchIndex
|
358
|
+
def initialize(arg)
|
359
|
+
super arg
|
360
|
+
@counter_keyf = "#{@model.prefix}#{self.class.name.sub(/^.*::/, "")}:#{@name}:#{@attribute}=%s:counter"
|
361
|
+
@attribute = @key
|
362
|
+
@threshold ||= 1
|
363
|
+
end
|
364
|
+
def digest(*arg)
|
365
|
+
"present"
|
366
|
+
end
|
367
|
+
def counter_key(obj, val=nil)
|
368
|
+
@counter_keyf % (val || value_is(obj))
|
369
|
+
end
|
370
|
+
def add(obj, value=nil)
|
371
|
+
k = redis(obj).incr counter_key(obj)
|
372
|
+
if k == @threshold
|
373
|
+
super obj
|
374
|
+
end
|
375
|
+
end
|
376
|
+
def remove(obj, value=nil)
|
377
|
+
ckey = counter_key obj
|
378
|
+
r = redis(obj)
|
379
|
+
r.decr ckey
|
380
|
+
if r.get(ckey).to_i. < @threshold
|
381
|
+
r.del ckey
|
382
|
+
super obj
|
383
|
+
end
|
384
|
+
end
|
385
|
+
end
|
386
|
+
|
387
|
+
# The power, and occasional awkwardness, of sorted sets
|
388
|
+
class RangeIndex < SearchIndex
|
389
|
+
def initialize(arg)
|
390
|
+
@value ||= proc { |x| x.to_f }
|
391
|
+
super arg
|
392
|
+
end
|
393
|
+
def val(val=nil, obj=nil)
|
394
|
+
@value.call val, obj
|
395
|
+
end
|
396
|
+
def sorted_set_key(val=nil, prefix=nil, raw_val=false)
|
397
|
+
@keyf %[prefix || @model.prefix, "(...)"]
|
398
|
+
end
|
399
|
+
alias :key :sorted_set_key
|
400
|
+
def key_for_query(val=nil)
|
401
|
+
if val.nil?
|
402
|
+
key
|
403
|
+
else
|
404
|
+
"#{key}:rangehack:#{val.to_s}"
|
405
|
+
end
|
406
|
+
end
|
407
|
+
|
408
|
+
def rangehack_set_key
|
409
|
+
"#{sorted_set_key}:rangehacks"
|
410
|
+
end
|
411
|
+
|
412
|
+
def update(obj)
|
413
|
+
if !(diff = value_diff(obj)).nil?
|
414
|
+
increment(obj, diff) unless diff == 0
|
415
|
+
else
|
416
|
+
val_is, val_was = value_is(obj), value_was(obj)
|
417
|
+
add(obj, val_is) unless val_is == val_was
|
418
|
+
#removal is implicit with the way we're using sorted sets
|
419
|
+
end
|
420
|
+
#redis(obj).eval "redis.log(redis.LOG_WARNING, 'updated #{obj.id} for #{name}')"
|
421
|
+
end
|
422
|
+
|
423
|
+
def incremental?; true; end
|
424
|
+
def handle_range?; true; end
|
425
|
+
def usable_as_results?(val)
|
426
|
+
val.nil?
|
427
|
+
end
|
428
|
+
|
429
|
+
def zcommand(cmd, obj, value)
|
430
|
+
id= obj.send(@key)
|
431
|
+
my_val= val(value || value_is(obj), obj)
|
432
|
+
if cmd==:zrem
|
433
|
+
redis(obj).send cmd, sorted_set_key, id
|
434
|
+
else
|
435
|
+
redis(obj).send cmd, sorted_set_key, my_val, id
|
436
|
+
end
|
437
|
+
update_rangehacks cmd, id, my_val
|
438
|
+
update_live_delta obj
|
439
|
+
end
|
440
|
+
|
441
|
+
def add(obj, value=nil)
|
442
|
+
zcommand :zadd, obj, value
|
443
|
+
end
|
444
|
+
|
445
|
+
def increment(obj, value=nil)
|
446
|
+
zcommand :zincrby, obj, value
|
447
|
+
end
|
448
|
+
|
449
|
+
def remove(obj, value=nil)
|
450
|
+
zcommand :zrem, obj, value
|
451
|
+
end
|
452
|
+
|
453
|
+
#double hack
|
454
|
+
def update_rangehacks(action, id, val=nil)
|
455
|
+
case action
|
456
|
+
when :zrem
|
457
|
+
action=:del
|
458
|
+
when :zincrby
|
459
|
+
action=:incr
|
460
|
+
when :zadd
|
461
|
+
action=:add
|
462
|
+
end
|
463
|
+
Queris.run_script :update_rangehacks, redis, [rangehack_set_key, sorted_set_key], [action, id, val]
|
464
|
+
end
|
465
|
+
|
466
|
+
#there's no ZRANGESTORE, so we need to extract the desired range
|
467
|
+
#to a temporary zset first
|
468
|
+
attr_accessor :rangehack_keys
|
469
|
+
def ensure_rangehack_exists(redis, val, query)
|
470
|
+
#copy to temp key if needed
|
471
|
+
rangehack_keys||={}
|
472
|
+
unless val.nil?
|
473
|
+
rangehack_key = key_for_query val
|
474
|
+
return if rangehack_keys[rangehack_key]
|
475
|
+
val = (val...val) unless Enumerable === val
|
476
|
+
Queris.run_script :make_rangehack_if_needed, redis, [rangehack_key, key, rangehack_set_key, query.runstate_key(:ready)], [self.val(val.begin), self.val(val.end), val.exclude_end?]
|
477
|
+
#can be spiky-shit slow if whole zset must be copied
|
478
|
+
query.add_temp_key(rangehack_key)
|
479
|
+
end
|
480
|
+
end
|
481
|
+
def rangehack?(val)
|
482
|
+
not val.nil?
|
483
|
+
end
|
484
|
+
def clear_rangehack_keys
|
485
|
+
rangehack_keys={}
|
486
|
+
end
|
487
|
+
|
488
|
+
def temp_keys(val=nil)
|
489
|
+
val.nil? ? [] : [ key_for_query(val) ]
|
490
|
+
end
|
491
|
+
def distribution_summary
|
492
|
+
keycounts = distribution.values
|
493
|
+
"#{name}: #{keycounts.reduce(0){|a,b| a+b if Numeric === a && Numeric === b}} ids in #{keycounts.count} redis key."
|
494
|
+
end
|
495
|
+
def key_size(redis_key, r=nil)
|
496
|
+
(r || redis).zcard redis_key
|
497
|
+
end
|
498
|
+
private
|
499
|
+
def remove_inverse_range(redis, key, val)
|
500
|
+
first, last = val.begin.to_f, val.end.to_f
|
501
|
+
range_end = "#{!val.exclude_end? ? '(' : nil}#{last}" #)
|
502
|
+
if (first <= last)
|
503
|
+
redis.zremrangebyscore key, '-inf', "(#{first}" unless first == -Float::INFINITY #)
|
504
|
+
redis.zremrangebyscore key, range_end, 'inf' unless last == Float::INFINITY
|
505
|
+
else
|
506
|
+
redis.zremrangebyscore key, range_end, "(#{first}" #)
|
507
|
+
end
|
508
|
+
end
|
509
|
+
end
|
510
|
+
|
511
|
+
class ScoredSearchIndex < RangeIndex
|
512
|
+
def initialize(arg)
|
513
|
+
@score_attr=arg[:score_attr] || arg[:score_attribute] || arg[:score]
|
514
|
+
@score_val=arg[:score_val] || arg[:score_value]
|
515
|
+
@lazy_score_update=arg[:lazy_score_update] || arg[:lazy_score] || arg[:lazy_update] || arg[:lazy]
|
516
|
+
@value ||= proc{|x| x}
|
517
|
+
raise Index::Error, "ScoredSearchIndex needs :score, :score_attr or :score_val parameter" if @score_attr.nil? && @score_val.nil?
|
518
|
+
@score_val ||= proc {|x, obj| x.to_f}
|
519
|
+
super
|
520
|
+
end
|
521
|
+
|
522
|
+
def update_rangehacks(*arg); end
|
523
|
+
def handle_range?; false; end
|
524
|
+
def ensure_rangehack_exists(*arg); end
|
525
|
+
|
526
|
+
def usable_as_results?(val)
|
527
|
+
true
|
528
|
+
end
|
529
|
+
|
530
|
+
def score_is(obj)
|
531
|
+
score_attr_val=@score_attr.nil? ? nil : obj.send(@score_attr)
|
532
|
+
@score_val.call(score_attr_val, obj)
|
533
|
+
end
|
534
|
+
|
535
|
+
def score_was(obj)
|
536
|
+
if @score_attr.nil?
|
537
|
+
raise Index::Error, "score_was impossile without a score_attr"
|
538
|
+
else
|
539
|
+
score_attr_val=@score_attr.nil? ? nil : obj.send("#{@score_attr}_was")
|
540
|
+
@score_val.call(score_attr_val, obj)
|
541
|
+
end
|
542
|
+
end
|
543
|
+
|
544
|
+
|
545
|
+
def zcommand(cmd, obj, value)
|
546
|
+
id= obj.send(@key)
|
547
|
+
if id.nil?
|
548
|
+
if obj.respond_to? @key
|
549
|
+
raise Index::Error, "nil key attribute (#{@key}) for #{self.class.name} of model #{obj.class}"
|
550
|
+
else
|
551
|
+
raise Index::Error, "missing key attribute (#{@key}) for #{self.class.name} of model #{obj.class}"
|
552
|
+
end
|
553
|
+
end
|
554
|
+
my_val= val(value || value_is(obj), obj)
|
555
|
+
if cmd==:zrem
|
556
|
+
redis(obj).send cmd, sorted_set_key(my_val), id
|
557
|
+
else
|
558
|
+
redis(obj).send cmd, sorted_set_key(my_val), score_is(obj), id
|
559
|
+
end
|
560
|
+
end
|
561
|
+
|
562
|
+
def sorted_set_key(val=nil, prefix=nil, raw_val=false)
|
563
|
+
@keyf %[prefix || @model.prefix, val] #alternately, digest(val)
|
564
|
+
end
|
565
|
+
alias :key :sorted_set_key
|
566
|
+
|
567
|
+
def key_for_query(val=nil)
|
568
|
+
key val
|
569
|
+
end
|
570
|
+
|
571
|
+
def update_score(obj, val)
|
572
|
+
zcommand :zadd, obj, val
|
573
|
+
end
|
574
|
+
|
575
|
+
def update(obj)
|
576
|
+
val_is, val_was = val(value_is(obj)), val(value_was(obj))
|
577
|
+
if val_is == val_was
|
578
|
+
#should we update the score anyway? it's a O(log(N)) operation...
|
579
|
+
if @score_attr
|
580
|
+
sc_is, sc_was = score_is(obj), score_was(obj)
|
581
|
+
if sc_is != sc_was
|
582
|
+
update_score(obj, val_is)
|
583
|
+
end
|
584
|
+
elsif @lazy_score_update.nil?
|
585
|
+
add(obj, val_is)
|
586
|
+
end
|
587
|
+
else
|
588
|
+
remove(obj, val_was) unless val_was.nil?
|
589
|
+
add(obj, val_is)
|
590
|
+
end
|
591
|
+
end
|
592
|
+
|
593
|
+
end
|
594
|
+
|
595
|
+
class ExpiringPresenceIndex < RangeIndex
|
596
|
+
def initialize(arg={})
|
597
|
+
raise ArgumentError, "Expiring Presence index must have its time-to-live (:ttl) set." unless arg[:ttl]
|
598
|
+
novalue = !arg.key?(:attribute)
|
599
|
+
arg[:value] = proc{|v,o| Time.now.utc.to_f} if novalue
|
600
|
+
super arg
|
601
|
+
@attribute = @key if novalue #don't care what attribute we use.
|
602
|
+
end
|
603
|
+
def json_redis_dump(hash={})
|
604
|
+
hash[:index]=self.class.name.split('::').last
|
605
|
+
hash[:nocompare]=true
|
606
|
+
hash[:ttl]=@ttl
|
607
|
+
end
|
608
|
+
def incremental?; false; end
|
609
|
+
def update(obj)
|
610
|
+
add(obj)
|
611
|
+
end
|
612
|
+
def update_live_delta(*arg)
|
613
|
+
self
|
614
|
+
end
|
615
|
+
def add(*arg)
|
616
|
+
poke(nil, true) if live?
|
617
|
+
super(*arg)
|
618
|
+
end
|
619
|
+
def key_for_query(val=nil)
|
620
|
+
key
|
621
|
+
end
|
622
|
+
def ensure_rangehack_exists(*arg); end #nothing
|
623
|
+
def usable_as_results?(val)
|
624
|
+
false #because we always need to run stuff before query
|
625
|
+
end
|
626
|
+
|
627
|
+
def poke(rds=nil, schedule=false)
|
628
|
+
rds ||= Queris.redis :master #this index costs a roundtrip to master
|
629
|
+
if live?
|
630
|
+
Queris.run_script :update_live_expiring_presence_index, rds, [key, live_delta_key], [Time.now.utc.to_f, @ttl, schedule]
|
631
|
+
Queris.run_script :periodic_zremrangebyscore, rds, [live_delta_key], [(@delta_ttl/2), '-inf', (Time.now.utc.to_f - @delta_ttl)]
|
632
|
+
else
|
633
|
+
r.zremrangebyscore key, '-inf', Time.now.utc.to_f - @ttl
|
634
|
+
end
|
635
|
+
self
|
636
|
+
end
|
637
|
+
def count
|
638
|
+
redis.zrangebyscore(key, Time.now.utc.to_f - @ttl, 'inf').count
|
639
|
+
end
|
640
|
+
def wait_time
|
641
|
+
Queris.redis.ttl live_queries_key + ":wait"
|
642
|
+
end
|
643
|
+
|
644
|
+
def before_query(redis, query=nil)
|
645
|
+
poke(redis) #this is gonna cost me a roundtrip to master
|
646
|
+
end
|
647
|
+
end
|
648
|
+
|
649
|
+
#a stateful index that cannot be rebuilt without losing data.
|
650
|
+
class AccumulatorIndex < RangeIndex
|
651
|
+
def stateless?
|
652
|
+
false
|
653
|
+
end
|
654
|
+
def ensure_rangehack_exists(*arg); end #nothing
|
655
|
+
def update_rangehacks(*arg); end #also nothing
|
656
|
+
def add(obj, value=nil)
|
657
|
+
increment(obj, value)
|
658
|
+
end
|
659
|
+
end
|
660
|
+
|
661
|
+
class AccumulatorSearchIndex < ScoredSearchIndex
|
662
|
+
#attr_accessor :score_val
|
663
|
+
def stateless?
|
664
|
+
false
|
665
|
+
end
|
666
|
+
def ensure_rangehack_exists(*arg); end #nothing
|
667
|
+
def update_rangehacks(*arg); end #also nothing
|
668
|
+
def add(obj, value=nil)
|
669
|
+
increment(obj, value)
|
670
|
+
end
|
671
|
+
def update_score(obj, val)
|
672
|
+
zcommand :zincrby, obj, val
|
673
|
+
end
|
674
|
+
end
|
675
|
+
|
676
|
+
|
677
|
+
class DecayingAccumulatorIndex < AccumulatorIndex
|
678
|
+
TIME_OFFSET=Time.new(2015,1,1).to_f #change this every few years to current date to maintain decent index resolution
|
679
|
+
attr_reader :half_life
|
680
|
+
def initialize(arg)
|
681
|
+
@half_life = (arg[:half_life] || arg[:hl]).to_f
|
682
|
+
@value = Proc.new do |val|
|
683
|
+
val = Float(val)
|
684
|
+
val * 2.0 **(t(Time.now.to_f)/@half_life)
|
685
|
+
end
|
686
|
+
super arg
|
687
|
+
end
|
688
|
+
def t(seconds)
|
689
|
+
seconds - TIME_OFFSET
|
690
|
+
end
|
691
|
+
end
|
692
|
+
|
693
|
+
class DecayingAccumulatorSearchIndex < AccumulatorSearchIndex
|
694
|
+
TIME_OFFSET=Time.new(2015,9,4).to_f #change this every few years to current date to maintain decent index resolution
|
695
|
+
attr_reader :half_life
|
696
|
+
def initialize(arg)
|
697
|
+
@half_life = (arg[:half_life] || arg[:hl]).to_f
|
698
|
+
super arg
|
699
|
+
@real_score_val=@score_val
|
700
|
+
binding.pry if @real_score_val.nil?
|
701
|
+
1+1.1
|
702
|
+
@score_val= Proc.new do |val, obj|
|
703
|
+
val = Float(val)
|
704
|
+
score_attr_val=@score_attr.nil? ? nil : obj.send(@score_attr)
|
705
|
+
val * 2.0 **(t(@real_score_val.call(score_attr_val, obj))/@half_life)
|
706
|
+
end
|
707
|
+
|
708
|
+
end
|
709
|
+
def t(seconds)
|
710
|
+
seconds - TIME_OFFSET
|
711
|
+
end
|
712
|
+
end
|
713
|
+
|
714
|
+
class CountIndex < RangeIndex
|
715
|
+
def initialize(*arg)
|
716
|
+
raise "CountIndex is currently broken. Fix it or use something else"
|
717
|
+
end
|
718
|
+
def incrby(obj, val)
|
719
|
+
redis(obj).zincrby sorted_set_key, val, obj.send(@key)
|
720
|
+
if val<0
|
721
|
+
redis(obj).zremrangebyscore sorted_set_key, 0, '-inf'
|
722
|
+
#WHOA THERE. We just went O(log(N)) on this simple and presumably O(1) index update. That's bad.
|
723
|
+
#TODO: probabilistically run every 1/log(N) times or less. Average linear complexity for a modicum of win.
|
724
|
+
end
|
725
|
+
end
|
726
|
+
def add(obj)
|
727
|
+
incrby obj, 1
|
728
|
+
end
|
729
|
+
def remove(obj)
|
730
|
+
incrby obj, -1
|
731
|
+
end
|
732
|
+
def update(*arg)
|
733
|
+
end
|
734
|
+
end
|
735
|
+
end
|