/
sample.service.js
108 lines (86 loc) · 3.77 KB
/
sample.service.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
'use strict';
const _ = require('lodash'),
weightedRandom = require('weighted-random-object'),
ModelService = require('./model.service');
/**
* Returns a number of samples from the input collection
* randomly selected based on their 'weight' value
*/
const getSamples = (collection = [], sampleSize = 1) => {
// clone the original input array since we'll be removing elements from it
const elements = _.clone(collection);
const actualSamples = _.min([_.size(collection), sampleSize]);
return _.times(actualSamples, () => {
var selected = weightedRandom(elements);
var index = _.findIndex(elements, selected);
elements.splice(index, 1);
return selected;
});
};
/**
* Based on the input UserItemWeights object, return a map of the
* item IDs to their weights for this user
*/
const getItemIdToWeightMap = (userItemWeights) => {
const itemIdToWeightMap = {};
_.forEach(_.get(userItemWeights, 'itemWeights', []), (itemWeight) => {
itemIdToWeightMap[itemWeight.item] = itemWeight.weight;
});
return itemIdToWeightMap;
};
/**
* Filter out recommendations where:
* 1. the user requested to not see the recommendation again (the "DNR" list)
* 2. the user has a sufficiently high weight ("they've seen it enough")
* 3. or a sufficiently low recommendation score
* @param {string[]} dnr the "Do Not Recommend" list to check for the recommended item
* @param {object} itemToWeightMap the map of items to their weights
* @param {object} recommendation the recommendation that may be filtered out
*/
const filterRecommendations = (dnr, itemToWeightMap, recommendation) => {
// If the item is in the DNR list, ignore it
if(!_.isEmpty(dnr)) {
var dnrEntry = _.find(dnr.doNotRecommend, { item: recommendation.item });
if( !_.isEmpty(dnrEntry) ) { // if it has a DNR entry, return false to filter it out
// log.debug('Filtering out %s based on DNR entry', rec.item);
return false;
}
}
// TODO Determine what these numbers / thresholds should be. Also, make them configurable
return recommendation.weight > 0.5 || itemToWeightMap[recommendation.item] <= 2;
};
const addScoreToItems = (scoreMap, items) => {
return _.map(items, (item) => {
var recommendationScore = scoreMap[item._id];
// log.debug('found recommendation score of %s', recommendationScore);
return {
score: recommendationScore,
item: item
};
});
};
/**
* For the passed-in user, retrieve up to a number of samples based on their
* recommendations from the collaborative filtering output. Default to 20 samples
*/
const sampleRecommendationsForUser = (userId, numberOfSamples = 20) => {
var recommendationPromise = ModelService.getAllRecommendationsForUser(userId);
var itemWeightsPromise = ModelService.getItemWeightsForUser(userId);
var doNotRecommendPromise = ModelService.getDoNotRecommendByUser(userId);
return Promise.all([recommendationPromise, itemWeightsPromise, doNotRecommendPromise])
.then(([userRecommendationsObj, userItemWeights, dnr]) => {
var itemIdToWeightMap = getItemIdToWeightMap(userItemWeights);
var allRecommendations = _.get(userRecommendationsObj, 'recommendations', []);
var itemToScoreMap = {};
_.forEach(allRecommendations, (rec) => {
itemToScoreMap[rec.item] = rec.weight;
});
var filteredRecommendations = _.filter(allRecommendations, filterRecommendations.bind(this, dnr, itemIdToWeightMap));
var sampleRecommendations = getSamples(filteredRecommendations, numberOfSamples);
return addScoreToItems(itemToScoreMap, _.map(sampleRecommendations, 'item'));
});
};
module.exports = {
getSamples,
sampleRecommendationsForUser
};