Skip to content
3 changes: 2 additions & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
language: node_js
node_js:
- '0.8'
- '0.10'
services:
- redis-server
script: grunt travis
2 changes: 1 addition & 1 deletion Gruntfile.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ module.exports = function(grunt) {
grunt.loadNpmTasks('grunt-mocha-cov');

// Tasks
grunt.registerTask('travis', [ 'jshint', 'mochacov:test', 'mochacov:coverage' ]);
grunt.registerTask('travis', [ 'jshint', 'mochacov:test']);
grunt.registerTask('test', ['jshint:all', 'mochacov:test']);

// Default task (runs when running `grunt` without arguments)
Expand Down
57 changes: 35 additions & 22 deletions lib/algorithms.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,13 @@ var jaccardCoefficient = function(userId1, userId2, callback){
finalJaccard = 0,
ratedInCommon = 0;
// retrieving a set of all the users likes incommon
client.sinter([config.className,userId1,'liked'].join(":"),[config.className,userId2,'liked'].join(":"), function(err, results1){
client.sinter([config.className,'user',userId1,'liked'].join(":"),[config.className,'user',userId2,'liked'].join(":"), function(err, results1){
// retrieving a set of the users dislike incommon
client.sinter([config.className,userId1,'disliked'].join(":"),[config.className,userId2,'disliked'].join(":"), function(err, results2){
client.sinter([config.className,'user',userId1,'disliked'].join(":"),[config.className,'user',userId2,'disliked'].join(":"), function(err, results2){
// retrieving a set of the users like and dislikes that they disagree on
client.sinter([config.className,userId1,'liked'].join(":"),[config.className,userId2,'disliked'].join(":"), function(err, results3){
client.sinter([config.className,'user',userId1,'liked'].join(":"),[config.className,'user',userId2,'disliked'].join(":"), function(err, results3){
// retrieving a set of the users like and dislikes that they disagree on
client.sinter([config.className,userId1,'disliked'].join(":"),[config.className,userId2,'liked'].join(":"), function(err, results4){
client.sinter([config.className,'user',userId1,'disliked'].join(":"),[config.className,'user',userId2,'liked'].join(":"), function(err, results4){
// calculating the sum of the similarities minus the sum of the disagreements
similarity = (results1.length+results2.length-results3.length-results4.length);
// calculating the number of movies rated incommon
Expand All @@ -43,17 +43,17 @@ exports.updateSimilarityFor = function(userId, cb){
// initializing variables
var similaritySet, userRatedItemIds, itemLiked, itemDisliked, itemLikeDislikeKeys;
// setting the redis key for the user's similarity set
similaritySet = [config.className,userId,'similaritySet'].join(":");
similaritySet = [config.className,'user',userId,'similaritySet'].join(":");
// creating a combined set with the all of a users likes and dislikes
client.sunion([config.className,userId,'liked'].join(":"),[config.className,userId,'disliked'].join(":"), function(err, userRatedItemIds){
client.sunion([config.className,'user',userId,'liked'].join(":"),[config.className,'user',userId,'disliked'].join(":"), function(err, userRatedItemIds){
// if they have rated anything
if (userRatedItemIds.length > 0){
// creating a list of redis keys to look up all of the likes and dislikes for a given set of items
itemLikeDislikeKeys = _.map(userRatedItemIds, function(itemId, key){
// key for that item being liked
itemLiked = [config.className, itemId, 'liked'].join(":");
itemLiked = [config.className, 'item', itemId, 'liked'].join(":");
// key for the item being disliked
itemDisliked = [config.className, itemId, 'disliked'].join(":");
itemDisliked = [config.className, 'item', itemId, 'disliked'].join(":");
// returning an array of those keys
return [itemLiked, itemDisliked];
});
Expand Down Expand Up @@ -98,9 +98,9 @@ exports.predictFor = function(userId, itemId, callback){
itemId = String(itemId);
var finalSimilaritySum = 0.0;
var prediction = 0.0;
var similaritySet = [config.className, userId, 'similaritySet'].join(':');
var likedBySet = [config.className, itemId, 'liked'].join(':');
var dislikedBySet = [config.className, itemId, 'disliked'].join(':');
var similaritySet = [config.className, 'user', userId, 'similaritySet'].join(':');
var likedBySet = [config.className, 'item', itemId, 'liked'].join(':');
var dislikedBySet = [config.className, 'item', itemId, 'disliked'].join(':');
exports.similaritySum(similaritySet, likedBySet, function(result1){
exports.similaritySum(similaritySet, dislikedBySet, function(result2){
finalSimilaritySum = result1 - result2;
Expand Down Expand Up @@ -147,23 +147,23 @@ exports.updateRecommendationsFor = function(userId, cb){
var setsToUnion = [];
var scoreMap = [];
// initializing the redis keys for temp sets, the similarity set and the recommended set
var tempSet = [config.className, userId, 'tempSet'].join(":");
var tempDiffSet = [config.className, userId, 'tempDiffSet'].join(":");
var similaritySet = [config.className, userId, 'similaritySet'].join(":");
var recommendedSet = [config.className, userId, 'recommendedSet'].join(":");
var tempSet = [config.className, 'user', userId, 'tempSet'].join(":");
var tempDiffSet = [config.className, 'user', userId, 'tempDiffSet'].join(":");
var similaritySet = [config.className, 'user', userId, 'similaritySet'].join(":");
var recommendedSet = [config.className, 'user', userId, 'recommendedSet'].join(":");
// returns an array of the users that are most similar within k nearest neighbors
client.zrevrange(similaritySet, 0, config.nearestNeighbors-1, function(err, mostSimilarUserIds){
// returns an array of the users that are least simimilar within k nearest neighbors
client.zrange(similaritySet, 0, config.nearestNeighbors-1, function(err, leastSimilarUserIds){
// iterate through the user ids to create the redis keys for all those users likes
_.each(mostSimilarUserIds, function(id, key){
setsToUnion.push([config.className,id,'liked'].join(":"));
_.each(mostSimilarUserIds, function(userId, key){
setsToUnion.push([config.className, 'user', userId,'liked'].join(":"));
});
// if you want to factor in the least similar least likes, you change this in config
// left it off because it was recommending items that every disliked universally
if (config.factorLeastSimilarLeastLiked){
_.each(leastSimilarUserIds, function(id, key){
setsToUnion.push([config.className,id,'disliked'].join(":"));
_.each(leastSimilarUserIds, function(userId, key){
setsToUnion.push([config.className, 'user', userId,'disliked'].join(":"));
});
}
// if there is at least one set in the array, continue
Expand All @@ -180,7 +180,7 @@ exports.updateRecommendationsFor = function(userId, cb){
function(err){
// using the new array of all the items that were liked by people similar and disliked by people opposite, create a new set with all the
// items that the current user hasn't already rated
client.sdiff(tempSet, [config.className,userId,'liked'].join(":"), [config.className,userId,'disliked'].join(":"), function(err, notYetRatedItems){
client.sdiff(tempSet, [config.className,'user',userId,'liked'].join(":"), [config.className,'user',userId,'disliked'].join(":"), function(err, notYetRatedItems){
// with the array of items that user has not yet rated, iterate through all of them and predict what the current user would rate it
async.each(notYetRatedItems,
function(itemId, callback){
Expand Down Expand Up @@ -229,8 +229,8 @@ exports.updateRecommendationsFor = function(userId, cb){
exports.updateWilsonScore = function(itemId, callback){
// creating the redis keys for scoreboard and to get the items liked and disliked sets
var scoreBoard = [config.className, 'scoreBoard'].join(":");
var likedBySet = [config.className, itemId, 'liked'].join(':');
var dislikedBySet = [config.className, itemId, 'disliked'].join(':');
var likedBySet = [config.className, 'item', itemId, 'liked'].join(':');
var dislikedBySet = [config.className, 'item', itemId, 'disliked'].join(':');
// used for a confidence interval of 95%
var z = 1.96;
// initializing variables to calculate wilson score
Expand Down Expand Up @@ -266,3 +266,16 @@ exports.updateWilsonScore = function(itemId, callback){
});
};

exports.updateUser = function(userId, cb) {
exports.updateSimilarityFor(userId, function(){
exports.updateRecommendationsFor(userId, function(){
cb();
});
});
};

exports.updateItem = function(itemId, cb) {
exports.updateWilsonScore(itemId, function(){
cb();
});
};
39 changes: 23 additions & 16 deletions lib/input.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,22 @@
var config = require('./config.js'),
algo = require('./algorithms.js');
async = require('async');

var updateSequence = function(userId, itemId, callback){
algo.updateSimilarityFor(userId, function(){

var updateSequence = function(userId, itemId, update, callback){
//make the update parameter optional while still allowing the callback to be last (for readability)
if (typeof update == 'function') {
callback = update;
update = true;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not make use of ES6's default parameters?

}
if (update) {
async.parallel([
function(cb){
algo.updateWilsonScore(itemId, function(){
algo.updateItem(itemId, function(){
cb(null);
});
},
function(cb){
algo.updateRecommendationsFor(userId, function(){
algo.updateUser(userId, function(){
cb(null);
});
}
Expand All @@ -21,30 +26,32 @@ var updateSequence = function(userId, itemId, callback){
if (err){console.log('error', err);}
callback();
});
});
} else {
callback();
}
};

var input = {
liked: function(userId, itemId, callback){
client.sismember([config.className, itemId, 'liked'].join(":"), userId, function(err, results){
liked: function(userId, itemId, update, callback){
client.sismember([config.className, 'item', itemId, 'liked'].join(":"), userId, function(err, results){
if (results === 0){
client.zincrby([config.className, 'mostLiked'].join(":"), 1, itemId);
}
client.sadd([config.className, userId,'liked'].join(':'), itemId, function(err){
client.sadd([config.className, itemId, 'liked'].join(':'), userId, function(err){
updateSequence(userId, itemId, callback);
client.sadd([config.className, 'user', userId,'liked'].join(':'), itemId, function(err){
client.sadd([config.className, 'item', itemId, 'liked'].join(':'), userId, function(err){
updateSequence(userId, itemId, update, callback);
});
});
});
},
disliked: function(userId, itemId, callback){
client.sismember([config.className, itemId, 'disliked'].join(":"), userId, function(err, results){
disliked: function(userId, itemId, update, callback){
client.sismember([config.className, 'item', itemId, 'disliked'].join(":"), userId, function(err, results){
if (results === 0){
client.zincrby([config.className, 'mostDisliked'].join(":"), 1, itemId);
}
client.sadd([config.className, userId, 'disliked'].join(':'), itemId, function(err){
client.sadd([config.className, itemId, 'disliked'].join(':'), userId, function(err){
updateSequence(userId, itemId, callback);
client.sadd([config.className, 'user', userId, 'disliked'].join(':'), itemId, function(err){
client.sadd([config.className, 'item', itemId, 'disliked'].join(':'), userId, function(err){
updateSequence(userId, itemId, update, callback);
});
});
});
Expand Down
2 changes: 2 additions & 0 deletions lib/raccoon.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ Raccoon.prototype.config = config;
Raccoon.prototype.stat = stat;
Raccoon.prototype.liked = input.liked;
Raccoon.prototype.disliked = input.disliked;
Raccoon.prototype.updateUser = algo.updateUser;
Raccoon.prototype.updateItem = algo.updateItem;
Raccoon.prototype.recommendFor = stat.recommendFor;
Raccoon.prototype.bestRated = stat.bestRated;
Raccoon.prototype.worstRated = stat.worstRated;
Expand Down
20 changes: 10 additions & 10 deletions lib/stat.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ var config = require('./config.js');

var stat = {
recommendFor: function(userId, numberOfRecs, callback){
client.zrevrange([config.className, userId, 'recommendedSet'].join(":"), 0, numberOfRecs, function(err, results){
client.zrevrange([config.className, 'user', userId, 'recommendedSet'].join(":"), 0, numberOfRecs, function(err, results){
callback(results);
});
},
Expand Down Expand Up @@ -36,47 +36,47 @@ var stat = {

},
mostSimilarUsers: function(userId, callback){
client.zrevrange([config.className, userId, 'similaritySet'].join(":"), 0, -1, function(err, results){
client.zrevrange([config.className, 'user', userId, 'similaritySet'].join(":"), 0, -1, function(err, results){
callback(results);
});
},
leastSimilarUsers: function(userId, callback){
client.zrange([config.className, userId, 'similaritySet'].join(":"), 0, -1, function(err, results){
client.zrange([config.className, 'user', userId, 'similaritySet'].join(":"), 0, -1, function(err, results){
callback(results);
});
},
likedBy: function(itemId, callback){
client.smembers([config.className,itemId,'liked'].join(':'), function(err, results){
client.smembers([config.className, 'item', itemId,'liked'].join(':'), function(err, results){
callback(results);
});
},
likedCount: function(itemId, callback){
client.scard([config.className,itemId, 'liked'].join(':'), function(err, results){
client.scard([config.className, 'item', itemId, 'liked'].join(':'), function(err, results){
callback(results);
});
},
dislikedBy: function(itemId, callback){
client.smembers([config.className,itemId,'disliked'].join(':'), function(err, results){
client.smembers([config.className, 'item', itemId, 'disliked'].join(':'), function(err, results){
callback(results);
});
},
dislikedCount: function(itemId, callback){
client.scard([config.className,itemId, 'disliked'].join(':'), function(err, results){
client.scard([config.className, 'item', itemId, 'disliked'].join(':'), function(err, results){
callback(results);
});
},
allLikedFor: function(userId, callback){
client.smembers([config.className, userId, 'liked'].join(":"), function(err, results){
client.smembers([config.className, 'user', userId, 'liked'].join(":"), function(err, results){
callback(results);
});
},
allDislikedFor: function(userId, callback){
client.smembers([config.className, userId, 'disliked'].join(":"), function(err, results){
client.smembers([config.className, 'user', userId, 'disliked'].join(":"), function(err, results){
callback(results);
});
},
allWatchedFor: function(userId, callback){
client.sunion([config.className, userId, 'liked'].join(":"), [config.className, userId, 'disliked'].join(":"), function(err, results){
client.sunion([config.className, 'user', userId, 'liked'].join(":"), [config.className, 'user', userId, 'disliked'].join(":"), function(err, results){
callback(results);
});
}
Expand Down
13 changes: 10 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,14 @@
"mocha-lcov-reporter": "0.0.1",
"grunt": "0.4.1",
"grunt-cli": "0.1.7",
"chai": "*",
"chai": ">=1.0.0 <2",
"mocha": "*",
"grunt-mocha-test": "0.6.2",
"grunt-contrib-jshint": "0.6.2",
"grunt-contrib-jshint": "0.6.3",
"grunt-contrib-watch": "0.5.1",
"grunt-mocha-cov": "0.0.4",
"sinon-chai": "2.4.0",
"sinon": "1.7.3",
"sinon": ">=1.4.0 <2",
"grunt-blanket-mocha": "0.2.0"
},
"config": {
Expand All @@ -40,6 +40,13 @@
"engines": {
"node": "*"
},
"scripts": {
"blanket": {
"data-cover-flags": {
"engineOnly":true
}
}
},
"licenses": [
{
"type": "MIT",
Expand Down
4 changes: 2 additions & 2 deletions test/testRaccoon.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,15 +30,15 @@ describe('basic likes and dislikes', function(){
});
describe('basic like', function(){
it('should validate a user has been added after a rating', function(done){
client.smembers('movie:chris:liked', function(err, results){
client.smembers('movie:user:chris:liked', function(err, results){
assert.equal(results[0],'batman');
done();
});
});
});
describe('basic dislike', function(){
it('should validate a user has been added after a rating', function(done){
client.smembers('movie:greg:disliked', function(err, results){
client.smembers('movie:user:greg:disliked', function(err, results){
assert.equal(results[0],'batman');
done();
});
Expand Down