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



78
79
80
81
82
83
84
# File 'lib/predictor/base.rb', line 78

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_to_matrix(matrix, set, *items) ⇒ Object



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

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



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

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



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

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

#clean!Object



284
285
286
287
288
289
# File 'lib/predictor/base.rb', line 284

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

#delete_from_matrix!(matrix, item) ⇒ Object



258
259
260
261
262
263
264
# File 'lib/predictor/base.rb', line 258

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



266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
# File 'lib/predictor/base.rb', line 266

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

#ensure_similarity_limit_is_obeyed!Object



291
292
293
294
295
296
297
298
299
300
301
302
# File 'lib/predictor/base.rb', line 291

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

#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



115
116
117
118
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
# File 'lib/predictor/base.rb', line 115

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



253
254
255
256
# File 'lib/predictor/base.rb', line 253

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

#process_item!(item) ⇒ Object



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

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

#process_items!(*items) ⇒ Object



197
198
199
200
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
# File 'lib/predictor/base.rb', line 197

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



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

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

#redis_prefixObject



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

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


105
106
107
108
109
110
111
112
113
# File 'lib/predictor/base.rb', line 105

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)


86
87
88
# File 'lib/predictor/base.rb', line 86

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

#sets_for(item) ⇒ Object



188
189
190
191
# File 'lib/predictor/base.rb', line 188

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



177
178
179
180
181
182
183
184
185
186
# File 'lib/predictor/base.rb', line 177

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



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

def similarity_limit
  self.class.similarity_limit
end