Skip to content

Instantly share code, notes, and snippets.

@kidGodzilla
Last active November 13, 2016 00:18
Show Gist options
  • Select an option

  • Save kidGodzilla/a19127afdbc95d37f2cf91bf8ba748b5 to your computer and use it in GitHub Desktop.

Select an option

Save kidGodzilla/a19127afdbc95d37f2cf91bf8ba748b5 to your computer and use it in GitHub Desktop.
Client-side Javascript Recommendation Engine
/****************************************************
* Core.js
***************************************************/
function needsNew() {
throw new TypeError("Failed to construct: Please use the 'new' operator, this object constructor cannot be called as a function.");
}
/**
* a Generic core object
*/
var Core = (function () {
var constructor = this;
/**
* Prevent direct execution
*/
//if (!(this instanceof Core))
// needsNew();
/**
* datastore getter
*/
function get (key) {
return this.data[key];
}
/**
* datastore setter
*/
function set (key, value) {
return this.data[key] = value;
}
/**
* Executes an array of functions, Sequentially
*/
function executeFunctionArray (functionArray, args) {
if (typeof(functionArray) !== "object" || !functionArray.length) return false;
for (var i = 0; i < functionArray.length; i++) {
args = functionArray[i](args);
}
return args;
}
/**
* Registers a new global on the current object
*/
function registerGlobal (key, value) {
if (typeof(this[key]) === "undefined") {
if (typeof(value) === "function") {
this[key] = function () {
/**
* Prepare Arguments
*
* TODO: (Source: MDN)
* You should not slice on arguments because it prevents optimizations in JavaScript
* engines (V8 for example). Instead, try constructing a new array by iterating
* through the arguments object.
*/
// var args = Array.prototype.slice.call(arguments);
var args = arguments;
if (args.length === 0) args = null;
/**
* Execute Before hooks on the arguments
*/
if (this.hooks[key] && this.hooks[key].before && this.hooks[key].before.length > 0)
args = executeFunctionArray(this.hooks[key].before, args);
/**
* Execute the intended function
*/
var result = value.apply(this, args);
/**
* Execute After hooks on the result
*/
if (this.hooks[key] && this.hooks[key].after && this.hooks[key].after.length > 0)
result = executeFunctionArray(this.hooks[key].after, result);
return result;
};
} else {
// If the global is being set to any other type of object or value, just do it.
this[key] = value;
}
} else {
console.log("ERROR: A module attempted to write to the `" + key + "` namespace, but it is already being used.");
}
}
/**
* Registers a new before hook on a method
*
* Example:
* We could add a before hook to generateUID which always set the separator to `+`
*
* ```javascript
* this.before('generateUID', function(args) {
* if (args) args[0] = '+';
* return args;
* });
* ```
*
* Then, when we called generateUID('-'), we would get a GUID separated by `+` instead.
*
* TODO: Consider moving this.before & this.after to a private namespace to they cannot
* be easily accessed by 3rd party code.
*
*/
function before (key, func) {
if (!this.hooks[key]) this.hooks[key] = {};
if (!this.hooks[key].before) this.hooks[key].before = [];
this.hooks[key].before.push(func);
}
/**
* Registers a new after hook on a this method
*/
function after (key, func) {
if (!this.hooks[key]) this.hooks[key] = {};
if (!this.hooks[key].after) this.hooks[key].after = [];
this.hooks[key].after.push(func);
}
/**
* Return public objects & methods
*/
var obj = {
data: {},
hooks: {},
executeFunctionArray: executeFunctionArray,
registerGlobal: registerGlobal,
before: before,
after: after,
get: get,
set: set
};
return function () {
return obj;
};
})();
var Recommender = window.Recommender = new Core();
var _Data = {};
var _Affinity = {};
var _Users = [];
var isDirty = false;
Recommender.registerGlobal('dislikes', []);
// Array Intersection Utility
Recommender.registerGlobal('intersection', function (array1, array2) {
return array1.filter(function(n) {
return array2.indexOf(n) != -1;
});
});
// Array Difference Utility
Recommender.registerGlobal('difference', function(b, a) {
return b.filter(function(i) { return a.indexOf(i) < 0; });
});
/**
* Force a stringified object into an object. Also prevents the occurrence of fatal
* errors when attempting JSON.parse on an object.
*
* @method parseUntilObject
* @return {Object} Returns an object from a possibly over-stringified object
*/
Recommender.registerGlobal('parseUntilObject', function (str, i) {
var response;
i = i || 0;
if (typeof(str) !== "object" && i < 32) {
try {
response = Tracker.parseUntilObject(JSON.parse(str), ++i);
} catch (e) {
return;
}
return response;
}
return str;
});
// Adds like data to the graph
Recommender.registerGlobal('like', function (user, like) {
if (_Users.indexOf(user) === -1) _Users.push(user);
var userLikes = _Data[user] || [];
if (userLikes.indexOf(like) === -1) {
userLikes.push(like);
_Data[user] = userLikes;
isDirty = true;
}
});
// Compute the affinity of all users in the graph
Recommender.registerGlobal('computeAffinity', function () {
if (!_Users.length) return;
_Users.forEach(function (currentUser) {
var affinity = {};
var currentUserLikes = _Data[currentUser];
_Users.forEach(function (user) {
if (currentUser === user) return;
// Count number of intersecting items
var userLikes = _Data[user];
var intersection = Recommender.intersection(userLikes, currentUserLikes);
affinity[user] = intersection.length;
});
_Affinity[currentUser] = affinity;
// console.log(currentUser, affinity);
});
});
// Returns a list of users, sorted by those most similar to myself
Recommender.registerGlobal('similarUsersTo', function (user) {
if (_Affinity[user]) {
var results = [];
for (var prop in _Affinity[user]) {
results.push({
user: prop,
affinity: _Affinity[user][prop]
});
}
results.sort(function(a, b){
return a.affinity < b.affinity;
});
return results;
}
return false;
});
// Return recommendations for a specific user
// (Anything the most-similar other-user likes that I don't)
Recommender.registerGlobal('recommendationsForUser', function (user) {
var similarUsers = Recommender.similarUsersTo(user);
var maxCount = ~(similarUsers.length / 2) * -1;
var results = [];
for (var i = 0; i < maxCount; i++) {
var similarUser = Recommender.similarUsersTo(user)[i].user;
var result = Recommender.difference(_Data[similarUser], _Data[user]);
if (result.length) result.forEach(function (item) {
// Rm item if it exists, then add to the front, so more popular items end up at the front
if (results.indexOf(item) !== -1) {
results.splice(results.indexOf(item), 1);
results.unshift(item);
} else {
results.push(item);
}
});
}
results = results.filter(function(el) {
return !Recommender.dislikes.includes(el);
});
return results;
});
// Compute graph affinity every 10 seconds, if the graph has been modified
var computeAffinityInterval = setInterval(function () {
if (!isDirty) return;
Recommender.computeAffinity();
isDirty = false;
}, 10000);
// Persistence Layer
Recommender.registerGlobal('persist', function () {
var data = JSON.stringify({
data: _Data,
affinity: _Affinity,
users: _Users
});
// Example Persistence
// $.post(url, data);
localStorage.setItem('recommendations', data);
});
// Load data
Recommender.registerGlobal('loadData', function () {
// Example Loader
// $.get(url)
var data = localStorage.getItem('recommendations');
if (!data) return;
data = Recommender.parseUntilObject(data);
var _Data = data.data || {};
var _Affinity = data.affinity || {};
var _Users = data.users || [];
});
// Recommender.loadData();
Recommender.like('James', 'Soda');
Recommender.like('James', 'Coke');
Recommender.like('James', 'Hamburgers');
Recommender.like('James', 'Cheese');
Recommender.like('James', 'Shrimp');
Recommender.like('James', 'Beef');
Recommender.like('James', 'Pho');
Recommender.like('James', 'Phad Thai');
Recommender.like('Jason', 'Soda');
Recommender.like('Jason', 'Sprite');
Recommender.like('Jason', 'Hamburgers');
Recommender.like('Jason', 'Onions');
Recommender.like('Jason', 'Fries');
Recommender.like('Jason', 'Beef');
Recommender.like('Mary', 'Soda');
Recommender.like('Mary', 'Sprite');
Recommender.like('Mary', 'Tacos');
Recommender.like('Mary', 'Onions');
Recommender.like('Mary', 'Fries');
Recommender.like('Mary', 'Beef');
Recommender.like('Fred', 'Soda');
Recommender.like('Fred', 'Sprite');
Recommender.like('Fred', 'Hamburgers');
Recommender.like('Fred', 'Cheese');
Recommender.like('Fred', 'Fries');
Recommender.like('Fred', 'Beef');
Recommender.like('Fred', 'Juice');
Recommender.like('John', 'Soda');
Recommender.like('John', 'Orange Soda');
Recommender.like('John', 'Pepsi');
Recommender.like('John', 'Cheeseburgers');
Recommender.like('John', 'Onions');
Recommender.like('John', 'Fries');
Recommender.like('John', 'Beef');
Recommender.computeAffinity();
console.log("Recommendations for James:", Recommender.recommendationsForUser('James'));
console.log("Recommendations for Mary:", Recommender.recommendationsForUser('Mary'));
console.log("Recommendations for John:", Recommender.recommendationsForUser('John'));
console.log("Recommendations for Jason:", Recommender.recommendationsForUser('Jason'));
console.log("Recommendations for Fred:", Recommender.recommendationsForUser('Fred'));
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment