Module: Predictor::Base

Defined in:
lib/predictor/base.rb

Defined Under Namespace

Modules: ClassMethods

Class Method Summary collapse

Instance Method Summary collapse

Dynamic Method Handling

This class handles dynamic methods through the method_missing method

#method_missing(method, *args) ⇒ Object


82
83
84
85
86
87
88
# File 'lib/predictor/base.rb', line 82

def method_missing(method, *args)
  if input_matrices.has_key?(method)
    input_matrices[method]
  else
    raise NoMethodError.new(method.to_s)
  end
end

Class Method Details

.included(base) ⇒ Object


2
3
4
# File 'lib/predictor/base.rb', line 2

def self.included(base)
  base.extend(ClassMethods)
end

Instance Method Details

#add_item(item) ⇒ Object


277
278
279
# File 'lib/predictor/base.rb', line 277

def add_item(item)
  Predictor.redis.sadd(redis_key(:all_items), item)
end

#add_to_matrix(matrix, set, *items) ⇒ Object


98
99
100
101
# File 'lib/predictor/base.rb', line 98

def add_to_matrix(matrix, set, *items)
  items = items.flatten if items.count == 1 && items[0].is_a?(Array)  # Old syntax
  input_matrices[matrix].add_to_set(set, *items)
end

#add_to_matrix!(matrix, set, *items) ⇒ Object


103
104
105
106
107
# File 'lib/predictor/base.rb', line 103

def add_to_matrix!(matrix, set, *items)
  items = items.flatten if items.count == 1 && items[0].is_a?(Array)  # Old syntax
  add_to_matrix(matrix, set, *items)
  process_items!(*items)
end

#all_itemsObject


94
95
96
# File 'lib/predictor/base.rb', line 94

def all_items
  Predictor.redis.smembers(redis_key(:all_items))
end

#clean!Object


299
300
301
302
303
304
# File 'lib/predictor/base.rb', line 299

def clean!
  keys = Predictor.redis.keys(redis_key('*'))
  unless keys.empty?
    Predictor.redis.del(keys)
  end
end

#delete_from_matrix!(matrix, item) ⇒ Object


262
263
264
265
266
267
268
# File 'lib/predictor/base.rb', line 262

def delete_from_matrix!(matrix, item)
  # Deleting from a specific matrix, so get related_items, delete, then update the similarity of those related_items
  items = related_items(item)
  input_matrices[matrix].delete_item(item)
  items.each { |related_item| cache_similarity(item, related_item) }
  return self
end

#delete_item!(item) ⇒ Object


281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
# File 'lib/predictor/base.rb', line 281

def delete_item!(item)
  Predictor.redis.srem(redis_key(:all_items), item)
  Predictor.redis.watch(redis_key(:similarities, item)) do
    items = related_items(item)
    Predictor.redis.multi do |multi|
      items.each do |related_item|
        multi.zrem(redis_key(:similarities, related_item), item)
      end
      multi.del redis_key(:similarities, item)
    end
  end

  input_matrices.each do |k,m|
    m.delete_item(item)
  end
  return self
end

#delete_pair_from_matrix!(matrix, set, item) ⇒ Object


270
271
272
273
274
275
# File 'lib/predictor/base.rb', line 270

def delete_pair_from_matrix!(matrix, set, item)
  items = related_items(item)
  input_matrices[matrix].remove_from_set(set, item)
  items.each { |related_item| cache_similarity(item, related_item) }
  return self
end

#ensure_similarity_limit_is_obeyed!Object


306
307
308
309
310
311
312
313
314
315
316
317
# File 'lib/predictor/base.rb', line 306

def ensure_similarity_limit_is_obeyed!
  if similarity_limit
    items = all_items
    Predictor.redis.multi do |multi|
      items.each do |item|
        key = redis_key(:similarities, item)
        multi.zremrangebyrank(key, 0, -(similarity_limit + 1))
        multi.zunionstore key, [key] # Rewrite zset to take advantage of ziplist implementation.
      end
    end
  end
end

#get_redis_prefixObject


66
67
68
# File 'lib/predictor/base.rb', line 66

def get_redis_prefix
  nil # Override in subclass.
end

#input_matricesObject


59
60
61
62
63
64
# File 'lib/predictor/base.rb', line 59

def input_matrices
  @input_matrices ||= Hash[self.class.input_matrices.map{ |key, opts|
    opts.merge!(:key => key, :base => self)
    [ key, Predictor::InputMatrix.new(opts) ]
  }]
end

#predictions_for(set = nil, item_set: nil, matrix_label: nil, with_scores: false, on: nil, offset: 0, limit: -1,, exclusion_set: [], boost: {}) ⇒ Object


119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
# File 'lib/predictor/base.rb', line 119

def predictions_for(set=nil, item_set: nil, matrix_label: nil, with_scores: false, on: nil, offset: 0, limit: -1, exclusion_set: [], boost: {})
  fail "item_set or matrix_label and set is required" unless item_set || (matrix_label && set)

  on = Array(on)

  if matrix_label
    matrix = input_matrices[matrix_label]
    item_set = Predictor.redis.smembers(matrix.redis_key(:items, set))
  end

  item_keys = []
  weights   = []

  item_set.each do |item|
    item_keys << redis_key(:similarities, item)
    weights   << 1.0
  end

  boost.each do |matrix_label, values|
    m = input_matrices[matrix_label]

    # Passing plain sets to zunionstore is undocumented, but tested and supported:
    # https://github.com/antirez/redis/blob/2.8.11/tests/unit/type/zset.tcl#L481-L489

    case values
    when Hash
      values[:values].each do |value|
        item_keys << m.redis_key(:items, value)
        weights   << values[:weight]
      end
    when Array
      values.each do |value|
        item_keys << m.redis_key(:items, value)
        weights   << 1.0
      end
    else
      raise "Bad value for boost: #{boost.inspect}"
    end
  end

  return [] if item_keys.empty?

  predictions = nil

  Predictor.redis.multi do |multi|
    multi.zunionstore 'temp', item_keys, weights: weights
    multi.zrem 'temp', item_set if item_set.any?
    multi.zrem 'temp', exclusion_set if exclusion_set.length > 0

    if on.any?
      multi.zadd 'temp2', on.map{ |val| [0.0, val] }
      multi.zinterstore 'temp', ['temp', 'temp2']
      multi.del 'temp2'
    end

    predictions = multi.zrevrange 'temp', offset, limit == -1 ? limit : offset + (limit - 1), with_scores: with_scores
    multi.del 'temp'
  end

  predictions.value
end

#process!Object


257
258
259
260
# File 'lib/predictor/base.rb', line 257

def process!
  process_items!(*all_items)
  return self
end

#process_item!(item) ⇒ Object


197
198
199
# File 'lib/predictor/base.rb', line 197

def process_item!(item)
  process_items!(item)  # Old method
end

#process_items!(*items) ⇒ Object


201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
# File 'lib/predictor/base.rb', line 201

def process_items!(*items)
  items = items.flatten if items.count == 1 && items[0].is_a?(Array) # Old syntax

  case self.class.get_processing_technique
  when :lua
    matrix_data = {}
    input_matrices.each do |name, matrix|
      matrix_data[name] = {weight: matrix.weight, measure: matrix.measure_name}
    end
    matrix_json = JSON.dump(matrix_data)

    items.each do |item|
      Predictor.process_lua_script(redis_key, matrix_json, similarity_limit, item)
    end
  when :union
    items.each do |item|
      keys    = []
      weights = []

      input_matrices.each do |key, matrix|
        k = matrix.redis_key(:sets, item)
        item_keys = Predictor.redis.smembers(k).map { |set| matrix.redis_key(:items, set) }

        counts = Predictor.redis.multi do |multi|
          item_keys.each { |key| Predictor.redis.scard(key) }
        end

        item_keys.zip(counts).each do |key, count|
          unless count.zero?
            keys << key
            weights << matrix.weight / count
          end
        end
      end

      Predictor.redis.multi do |multi|
        key = redis_key(:similarities, item)
        multi.del(key)

        if keys.any?
          multi.zunionstore(key, keys, weights: weights)
          multi.zrem(key, item)
          multi.zremrangebyrank(key, 0, -(similarity_limit + 1))
          multi.zunionstore key, [key] # Rewrite zset for optimized storage.
        end
      end
    end
  else # Default to old behavior, processing things in Ruby.
    items.each do |item|
      related_items(item).each { |related_item| cache_similarity(item, related_item) }
    end
  end

  return self
end

#redis_key(*append) ⇒ Object


78
79
80
# File 'lib/predictor/base.rb', line 78

def redis_key(*append)
  ([redis_prefix] + append).flatten.compact.join(":")
end

#redis_prefixObject


70
71
72
# File 'lib/predictor/base.rb', line 70

def redis_prefix
  [Predictor.get_redis_prefix, self.class.get_redis_prefix, self.get_redis_prefix].compact
end

109
110
111
112
113
114
115
116
117
# File 'lib/predictor/base.rb', line 109

def related_items(item)
  keys = []
  input_matrices.each do |key, matrix|
    sets = Predictor.redis.smembers(matrix.redis_key(:sets, item))
    keys.concat(sets.map { |set| matrix.redis_key(:items, set) })
  end

  keys.empty? ? [] : (Predictor.redis.sunion(keys) - [item.to_s])
end

#respond_to?(method, include_all = false) ⇒ Boolean

Returns:

  • (Boolean)

90
91
92
# File 'lib/predictor/base.rb', line 90

def respond_to?(method, include_all = false)
  input_matrices.has_key?(method) ? true : super
end

#sets_for(item) ⇒ Object


192
193
194
195
# File 'lib/predictor/base.rb', line 192

def sets_for(item)
  keys = input_matrices.map{ |k,m| m.redis_key(:sets, item) }
  Predictor.redis.sunion keys
end

#similarities_for(item, with_scores: false, offset: 0, limit: -1,, exclusion_set: []) ⇒ Object


181
182
183
184
185
186
187
188
189
190
# File 'lib/predictor/base.rb', line 181

def similarities_for(item, with_scores: false, offset: 0, limit: -1, exclusion_set: [])
  neighbors = nil
  Predictor.redis.multi do |multi|
    multi.zunionstore 'temp', [1, redis_key(:similarities, item)]
    multi.zrem 'temp', exclusion_set if exclusion_set.length > 0
    neighbors = multi.zrevrange('temp', offset, limit == -1 ? limit : offset + (limit - 1), with_scores: with_scores)
    multi.del 'temp'
  end
  return neighbors.value
end

#similarity_limitObject


74
75
76
# File 'lib/predictor/base.rb', line 74

def similarity_limit
  self.class.similarity_limit
end