diff --git a/.travis.yml b/.travis.yml index 18d5ba7..eb11c99 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,6 @@ language: node_js node_js: - - '0.8' - '0.10' +services: + - redis-server script: grunt travis \ No newline at end of file diff --git a/Gruntfile.js b/Gruntfile.js index 31cc344..e276cdf 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -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) diff --git a/lib/algorithms.js b/lib/algorithms.js index 181c572..4abf58b 100644 --- a/lib/algorithms.js +++ b/lib/algorithms.js @@ -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 @@ -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]; }); @@ -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; @@ -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 @@ -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){ @@ -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 @@ -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(); + }); +}; diff --git a/lib/input.js b/lib/input.js index ccaddb5..633abc7 100644 --- a/lib/input.js +++ b/lib/input.js @@ -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; + } + 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); }); } @@ -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); }); }); }); diff --git a/lib/raccoon.js b/lib/raccoon.js index 0246df3..2dee04e 100644 --- a/lib/raccoon.js +++ b/lib/raccoon.js @@ -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; diff --git a/lib/stat.js b/lib/stat.js index 23f99c2..0afd1bc 100644 --- a/lib/stat.js +++ b/lib/stat.js @@ -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); }); }, @@ -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); }); } diff --git a/package.json b/package.json index fe4e223..5bc2367 100644 --- a/package.json +++ b/package.json @@ -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": { @@ -40,6 +40,13 @@ "engines": { "node": "*" }, + "scripts": { + "blanket": { + "data-cover-flags": { + "engineOnly":true + } + } + }, "licenses": [ { "type": "MIT", diff --git a/test/testRaccoon.js b/test/testRaccoon.js index 691d1ce..0b82e56 100644 --- a/test/testRaccoon.js +++ b/test/testRaccoon.js @@ -30,7 +30,7 @@ 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(); }); @@ -38,7 +38,7 @@ describe('basic likes and dislikes', function(){ }); 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(); });