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



70
71
72
73
74
75
76
# File 'lib/predictor/base.rb', line 70

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



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

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



91
92
93
94
95
# File 'lib/predictor/base.rb', line 91

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



82
83
84
# File 'lib/predictor/base.rb', line 82

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

#clean!Object



219
220
221
222
223
224
# File 'lib/predictor/base.rb', line 219

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

#delete_from_matrix!(matrix, item) ⇒ Object



193
194
195
196
197
198
199
# File 'lib/predictor/base.rb', line 193

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



201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
# File 'lib/predictor/base.rb', line 201

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



226
227
228
229
230
231
232
233
234
235
236
237
# File 'lib/predictor/base.rb', line 226

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



51
52
53
54
55
56
# File 'lib/predictor/base.rb', line 51

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, offset: 0, limit: -1,, exclusion_set: [], boost: {}) ⇒ Object



107
108
109
110
111
112
113
114
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
# File 'lib/predictor/base.rb', line 107

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

  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
    predictions = multi.zrevrange 'temp', offset, limit == -1 ? limit : offset + (limit - 1), with_scores: with_scores
    multi.del 'temp'
  end

  predictions.value
end

#process!Object



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

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

#process_item!(item) ⇒ Object



176
177
178
# File 'lib/predictor/base.rb', line 176

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

#process_items!(*items) ⇒ Object



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

def process_items!(*items)
  items = items.flatten if items.count == 1 && items[0].is_a?(Array)  # Old syntax
  items.each do |item|
    related_items(item).each{ |related_item| cache_similarity(item, related_item) }
  end
  return self
end

#redis_key(*append) ⇒ Object



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

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

#redis_prefixObject



58
59
60
# File 'lib/predictor/base.rb', line 58

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


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

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) ⇒ Boolean

Returns:

  • (Boolean)


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

def respond_to?(method)
  input_matrices.has_key?(method) ? true : super
end

#sets_for(item) ⇒ Object



171
172
173
174
# File 'lib/predictor/base.rb', line 171

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



160
161
162
163
164
165
166
167
168
169
# File 'lib/predictor/base.rb', line 160

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



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

def similarity_limit
  self.class.similarity_limit
end