Last active
November 13, 2016 00:18
-
-
Save kidGodzilla/a19127afdbc95d37f2cf91bf8ba748b5 to your computer and use it in GitHub Desktop.
Client-side Javascript Recommendation Engine
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /**************************************************** | |
| * 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