UNPKG

11.2 kBtext/coffeescriptView Raw
1bb = require 'bluebird'
2_ = require 'lodash'
3
4moment = require "moment"
5
6#The only stateful things in GER are the ESM and the options
7class GER
8
9 constructor: (@esm) ->
10
11 ####################### Weighted people #################################
12
13 calculate_similarities_from_thing: (namespace, thing, things, actions, configuration) ->
14 @esm.calculate_similarities_from_thing(namespace, thing, things, actions, _.clone(configuration))
15
16 calculate_similarities_from_person: (namespace, person, people, actions, configuration) ->
17 @esm.calculate_similarities_from_person(namespace, person, people, actions, _.clone(configuration))
18 .then( (similarities) =>
19 similarities[person] = 1 #manually add person to weights
20 similarities
21 )
22
23 filter_recommendations: (namespace, person, recommendations, filter_previous_actions) ->
24 recommended_things = _.uniq( (x.thing for x in recommendations) )
25 @esm.filter_things_by_previous_actions(namespace, person, recommended_things, filter_previous_actions)
26 .then( (filtered_recommendations) ->
27 filtered_recs = []
28 for rec in recommendations
29 if rec.thing in filtered_recommendations
30 filtered_recs.push rec
31
32 filtered_recs
33 )
34
35 filter_similarities: (similarities) ->
36 ns = {}
37 for pt, weight of similarities
38 if weight != 0
39 ns[pt] = weight
40 ns
41
42 neighbourhood_confidence: (n_values ) ->
43 #The more similar people found, the more we trust the recommendations
44 #15 is a magic number chosen to make 10 around 50% and 50 around 95%
45 pc = 1.0 - Math.pow(Math.E,( (- n_values) / 15 ))
46 #The person confidence multiplied by the mean distance
47 pc
48
49 history_confidence: (n_history) ->
50 # The more hisotry (input) the more we trust the recommendations
51 # 35 is a magic number to make 100 about 100%
52 hc = 1.0 - Math.pow(Math.E,( (- n_history) / 35 ))
53 hc
54
55 recommendations_confidence: (recommendations) ->
56 return 0 if recommendations.length == 0
57 # The greater the mean recommendation the more we trust the recommendations
58 # 2 is a magic number to make 10 about 100%
59 total_weight = 0
60 for r in recommendations
61 total_weight += r.weight
62
63 mean_weight = total_weight/recommendations.length
64 tc = 1.0 - Math.pow(Math.E,( (- mean_weight) / 2 ))
65
66 tc
67
68 person_neighbourhood: (namespace, person, actions, configuration) ->
69 @esm.person_neighbourhood(namespace, person, Object.keys(actions), _.clone(configuration))
70
71 thing_neighbourhood: (namespace, thing, actions, configuration) ->
72 @esm.thing_neighbourhood(namespace, thing, Object.keys(actions), _.clone(configuration))
73
74 recent_recommendations_by_people: (namespace, actions, people, configuration) ->
75 @esm.recent_recommendations_by_people(namespace, Object.keys(actions), people, _.clone(configuration))
76
77 calculate_people_recommendations: (similarities, recommendations, configuration) ->
78 thing_group = {}
79
80
81 for rec in recommendations
82 if thing_group[rec.thing] == undefined
83 thing_group[rec.thing] = {
84 thing: rec.thing
85 weight: 0
86 last_actioned_at: rec.last_actioned_at
87 last_expires_at: rec.last_expires_at
88 people: []
89 }
90
91 thing_group[rec.thing].last_actioned_at = moment.max(moment(thing_group[rec.thing].last_actioned_at), moment(rec.last_actioned_at)).format()
92 thing_group[rec.thing].last_expires_at = moment.max(moment(thing_group[rec.thing].last_expires_at), moment(rec.last_expires_at)).format()
93
94 thing_group[rec.thing].weight += similarities[rec.person]
95
96 thing_group[rec.thing].people.push rec.person
97
98 recommendations = []
99 for thing, rec of thing_group
100 recommendations.push rec
101
102 recommendations = recommendations.sort((x, y) -> y.weight - x.weight)
103 recommendations
104
105
106 calculate_thing_recommendations: (thing, similarities, neighbourhood, configuration) ->
107 recommendations = []
108
109 for rec in neighbourhood
110 recommendations.push {
111 thing: rec.thing
112 weight: rec.people.length * similarities[rec.thing] # could be more subtle than n_people * similarity
113 last_actioned_at: rec.last_actioned_at
114 last_expires_at: rec.last_expires_at
115 people: rec.people
116 }
117
118 recommendations = recommendations.sort((x, y) -> y.weight - x.weight)
119 recommendations
120
121 generate_recommendations_for_person: (namespace, person, actions, person_history_count, configuration) ->
122 #"Recommendations for a Person"
123
124 @person_neighbourhood(namespace, person, actions, configuration)
125 .then( (people) =>
126 bb.all([
127 people,
128 @calculate_similarities_from_person(namespace, person, people, actions, _.clone(configuration))
129 @recent_recommendations_by_people(namespace, actions, people.concat(person), _.clone(configuration))
130 ])
131 )
132 .spread( ( neighbourhood, similarities, recommendations ) =>
133 bb.all([
134 neighbourhood,
135 similarities,
136 @filter_recommendations(namespace, person, recommendations, configuration.filter_previous_actions)
137 ])
138 )
139 .spread( (neighbourhood, similarities, recommendations) =>
140 recommendations_object = {}
141 recommendations_object.recommendations = @calculate_people_recommendations(similarities, recommendations, configuration)
142 recommendations_object.neighbourhood = @filter_similarities(similarities)
143
144 neighbourhood_confidence = @neighbourhood_confidence(neighbourhood.length)
145 history_confidence = @history_confidence(person_history_count)
146 recommendations_confidence = @recommendations_confidence(recommendations_object.recommendations)
147
148 recommendations_object.confidence = neighbourhood_confidence * history_confidence * recommendations_confidence
149
150 recommendations_object
151 )
152
153 generate_recommendations_for_thing: (namespace, thing, actions, thing_history_count, configuration) ->
154 #"People who Actioned this Thing also Actioned"
155
156 @thing_neighbourhood(namespace, thing, actions, configuration)
157 .then( (thing_neighbours) =>
158 things = (nei.thing for nei in thing_neighbours)
159 bb.all([
160 thing_neighbours,
161 @calculate_similarities_from_thing(namespace, thing , things, actions, _.clone(configuration))
162 ])
163 )
164 .spread( (neighbourhood, similarities) =>
165 recommendations_object = {}
166 recommendations_object.recommendations = @calculate_thing_recommendations(thing, similarities, neighbourhood, configuration)
167 recommendations_object.neighbourhood = @filter_similarities(similarities)
168
169 neighbourhood_confidence = @neighbourhood_confidence(neighbourhood.length)
170 history_confidence = @history_confidence(thing_history_count)
171 recommendations_confidence = @recommendations_confidence(recommendations_object.recommendations)
172
173 recommendations_object.confidence = neighbourhood_confidence * history_confidence * recommendations_confidence
174
175 #console.log JSON.stringify(recommendations_object,null,2)
176
177 recommendations_object
178 )
179 # weight people by the action weight
180 # find things that those
181 # @recent_recommendations_by_people(namespace, action, people.concat(person), configuration.recommendations_per_neighbour)
182
183 default_configuration: (configuration) ->
184 _.defaults(configuration,
185 minimum_history_required: 1,
186 neighbourhood_search_size: 100
187 similarity_search_size: 100
188 event_decay_rate: 1
189 neighbourhood_size: 25,
190 recommendations_per_neighbour: 5
191 filter_previous_actions: [],
192 time_until_expiry: 0
193 actions: {},
194 current_datetime: new Date() #set the current datetime, useful for testing and ML,
195 )
196
197 normalize_actions: (in_actions) ->
198 total_action_weight = 0
199 for action, weight of in_actions
200 continue if weight <= 0
201 total_action_weight += weight
202
203 #filter and normalize actions with 0 weight from actions
204 actions = {}
205 for action, weight of in_actions
206 continue if weight <= 0
207 actions[action] = weight/total_action_weight
208 actions
209
210 recommendations_for_thing: (namespace, thing, configuration = {}) ->
211 configuration = @default_configuration(configuration)
212 actions = configuration.actions
213
214 @find_events(namespace, actions: Object.keys(actions), thing: thing, current_datetime: configuration.current_datetime, size: 100)
215 .then( (events) =>
216 return {recommendations: [], confidence: 0} if events.length < configuration.minimum_history_required
217
218 return @generate_recommendations_for_thing(namespace, thing, actions, events.length, configuration)
219 )
220
221
222 recommendations_for_person: (namespace, person, configuration = {}) ->
223 configuration = @default_configuration(configuration)
224 actions = configuration.actions
225
226 #first a check or two
227 @find_events(namespace, actions: Object.keys(actions), person: person, current_datetime: configuration.current_datetime, size: 100)
228 .then( (events) =>
229
230 return {recommendations: [], confidence: 0} if events.length < configuration.minimum_history_required
231
232 return @generate_recommendations_for_person(namespace, person, actions, events.length, configuration)
233 )
234
235 ##Wrappers of the ESM
236
237 count_events: (namespace) ->
238 @esm.count_events(namespace)
239
240 estimate_event_count: (namespace) ->
241 @esm.estimate_event_count(namespace)
242
243 events: (events) ->
244 @esm.add_events(events)
245 .then( -> events)
246
247 event: (namespace, person, action, thing, dates = {}) ->
248 @esm.add_event(namespace, person,action, thing, dates)
249 .then( -> {person: person, action: action, thing: thing})
250
251 find_events: (namespace, options = {}) ->
252 @esm.find_events(namespace, options)
253
254 delete_events: (namespace, person, action, thing) ->
255 @esm.delete_events(namespace, person, action, thing)
256
257 namespace_exists: (namespace) ->
258 @esm.exists(namespace)
259
260 list_namespaces: () ->
261 @esm.list_namespaces()
262
263 initialize_namespace: (namespace) ->
264 @esm.initialize(namespace)
265
266 destroy_namespace: (namespace) ->
267 @esm.destroy(namespace)
268
269 # DATABASE CLEANING #
270 compact_database: ( namespace, options = {}) ->
271
272 options = _.defaults(options,
273 compact_database_person_action_limit: 1500
274 compact_database_thing_action_limit: 1500
275 actions: []
276 )
277
278 @esm.pre_compact(namespace)
279 .then( =>
280 @esm.compact_people(namespace, options.compact_database_person_action_limit, options.actions)
281 )
282 .then( =>
283 @esm.compact_things(namespace, options.compact_database_thing_action_limit, options.actions)
284 )
285 .then( =>
286 @esm.post_compact(namespace)
287 )
288
289 compact_database_to_size: (namespace, number_of_events) ->
290 # Smartly Cut (lossy) the tail of the database (based on created_at) to a defined size
291 #STEP 1
292 @esm.remove_events_till_size(namespace, number_of_events)
293
294
295RET = {}
296
297RET.GER = GER
298
299knex = require 'knex'
300RET.knex = knex
301
302RET.PsqlESM = require('./lib/psql_esm')
303RET.MemESM = require('./lib/basic_in_memory_esm')
304
305Errors = require './lib/errors'
306
307GER.NamespaceDoestNotExist = Errors.NamespaceDoestNotExist
308
309module.exports = RET;
310
311