├── .versions ├── README.md ├── package.js ├── samcorcos:recengine-tests.js └── samcorcos:recengine.js /.versions: -------------------------------------------------------------------------------- 1 | application-configuration@1.0.4 2 | base64@1.0.2 3 | binary-heap@1.0.2 4 | callback-hook@1.0.2 5 | check@1.0.4 6 | ddp@1.0.14 7 | ejson@1.0.5 8 | follower-livedata@1.0.3 9 | geojson-utils@1.0.2 10 | id-map@1.0.2 11 | json@1.0.2 12 | local-test:samcorcos:recengine@1.0.6 13 | logging@1.0.6 14 | meteor@1.1.4 15 | minimongo@1.0.6 16 | mongo@1.0.11 17 | ordered-dict@1.0.2 18 | random@1.0.2 19 | retry@1.0.2 20 | samcorcos:recengine@1.0.6 21 | tinytest@1.0.4 22 | tracker@1.0.5 23 | underscore@1.0.2 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [**DEPRECATED:** This approach does not scale. I highly recommend looking into a graph database such as Neo4j to solve this problem.] 2 | 3 | # RecEngine 4 | 5 | Lightweight, easily implemented recommendation engine. This package is for users who are "liking" things, "purchasing" things, or "voting" on things. 6 | 7 | ``` 8 | $ meteor add samcorcos:recengine 9 | ``` 10 | 11 | Associate items based on user "upvotes", "likes", "purchases", etc using the syntax: 12 | 13 | ``` 14 | recEngine.link('', '') 15 | ``` 16 | 17 | Then, to get a recommendation, use the syntax: 18 | 19 | ``` 20 | recEngine.suggest('', , function(error, result) { 21 | // "result" is an array of objects 22 | // Within the "result" object is "suggestion" and "weight" 23 | // "suggestion" is the item being suggested 24 | // "weight" is the strength of the suggestion -- higher number means stronger suggestion 25 | }) 26 | ``` 27 | 28 | ## Algorithm 29 | 30 | Suggestions are produced using the [Ford-Fulkerson algorithm](http://en.wikipedia.org/wiki/Ford%E2%80%93Fulkerson_algorithm), which computes the maximum flow in a flow network. 31 | 32 | To provide an analogy, think of the algorithm as finding the most efficient way to drive from point A to point B, taking into consideration the number of lanes each street has—streets with more lanes are better. 33 | 34 | Much of the logic comes from: https://gist.github.com/methodin/1561824 35 | 36 | Runs in `O(E*Fm)` time, where `E` is the number of edges, and `Fm` is the maximum flow. 37 | 38 | 39 | ## Basic Example 40 | 41 | This works not only with `USERID`s, but with colloquial names and just about anything else. For example, you could install the package and write the following: 42 | 43 | ``` 44 | if (Meteor.isServer) { 45 | Meteor.startup(function () { 46 | recEngine.link("Mike", "cake"); 47 | recEngine.link("Mike", "coffee"); 48 | recEngine.link("Mike", "pie"); 49 | recEngine.link("Sarah", "coffee"); 50 | recEngine.link("Sarah", "cake"); 51 | recEngine.link("Alex", "yogurt"); 52 | recEngine.link("Alex", "cake"); 53 | recEngine.link("John", "cake"); 54 | recEngine.link("John", "coffee"); 55 | recEngine.link("John", "pie"); 56 | recEngine.link("Nick", "coffee"); 57 | recEngine.link("Nick", "cake"); 58 | recEngine.link("Sally", "yogurt"); 59 | recEngine.link("Sally", "cake"); 60 | recEngine.link("Zeke", "cake"); 61 | 62 | recEngine.suggest("Zeke", 2, function(err,res) { 63 | if (err) console.log(err); 64 | console.log(res); 65 | }) 66 | }); 67 | } 68 | ``` 69 | 70 | ... and your server console should read: 71 | 72 | ``` 73 | [ { suggestion: 'coffee', weight: 3 }, 74 | { suggestion: 'pie', weight: 2 } ] 75 | ``` 76 | 77 | This means that Zeke would most probably like coffee given the preferences of his peers, and would probably also like pie, though, to a lesser degree. 78 | 79 | ## More Practical Example 80 | 81 | Say you have an app that keeps track of "likes" or "upvotes". In the method you wrote to keep track of upvotes, you can add a call to `recEngine.link('', '')` in your function. For example: 82 | 83 | ``` 84 | Meteor.methods({ 85 | upvote: function(movie) { 86 | var userId = Meteor.user()._id; 87 | Movies.update({ _id: movie._id}, {$addToSet: { upvotes: userId} }, function(err, res) { 88 | if (err) console.log(err); 89 | recEngine.link(userId, movie._id); 90 | }); 91 | } 92 | }) 93 | ``` 94 | 95 | Then, when you have enough upvotes in your database (it doesn't take very many), you can make suggestions in the following format: 96 | 97 | ``` 98 | Meteor.methods({ 99 | suggest: function(numSuggestions) { 100 | var userId = Meteor.user()._id; 101 | recEngine.suggest(userId, numSuggestions, function(err, res) { 102 | if (err) console.log(err); 103 | return res; 104 | }) 105 | } 106 | }) 107 | ``` 108 | 109 | This will give you how ever many suggestions you declared as the variable `numSuggestions` (could be any number) for the currently logged in user. 110 | 111 | ## Additional information 112 | 113 | Keep in mind that this package only allows users to vote on each item once, which works for most voting systems, but not all. 114 | 115 | ## To Do 116 | 117 | 1. Optimize performance by storing suggestions 118 | 2. Configurable number of records stored, to allow developer to change performance `recEngine.config.numRecords` 119 | 3. Add a method for finding similar users `recEngine.similarUsers` 120 | 4. Add a method for finding similar items `recEngine.similarItems` 121 | 5. Divide inputs into contexts so retrieval can be categorized 122 | -------------------------------------------------------------------------------- /package.js: -------------------------------------------------------------------------------- 1 | Package.describe({ 2 | name: 'samcorcos:recengine', 3 | version: '1.0.6', 4 | summary: 'Lightweight recommendation engine for Meteor', 5 | git: 'https://github.com/samcorcos/recEngine.git', 6 | documentation: 'README.md' 7 | }); 8 | 9 | Package.onUse(function(api) { 10 | api.versionsFrom('1.0.3.1'); 11 | api.use("underscore") 12 | api.export("recEngine", "server") 13 | api.export("RecEngine", "server") 14 | api.export("RecEngineLinks", "server") 15 | api.addFiles('samcorcos:recengine.js') 16 | }); 17 | 18 | Package.onTest(function(api) { 19 | api.use('tinytest'); 20 | api.use('samcorcos:recengine'); 21 | api.addFiles('samcorcos:recengine-tests.js'); 22 | }); 23 | -------------------------------------------------------------------------------- /samcorcos:recengine-tests.js: -------------------------------------------------------------------------------- 1 | // Write your tests here! 2 | // Here is an example. 3 | Tinytest.add('example', function (test) { 4 | test.equal(true, true); 5 | }); 6 | -------------------------------------------------------------------------------- /samcorcos:recengine.js: -------------------------------------------------------------------------------- 1 | RecEngine = new Meteor.Collection('recEngine') 2 | 3 | if (Meteor.isServer) { 4 | Meteor.publish('recEngine', function() { 5 | RecEngine.find({}) 6 | }); 7 | } 8 | 9 | RecEngineLinks = new Meteor.Collection('recEngineLinks') 10 | 11 | if (Meteor.isServer) { 12 | Meteor.publish('recEngineLinks', function() { 13 | RecEngineLinks.find({}) 14 | }); 15 | } 16 | 17 | // Represents an edge from source to sink with capacity 18 | var Edge = function(source, sink, capacity) { 19 | this.source = source; 20 | this.sink = sink; 21 | this.capacity = capacity; 22 | this.reverseEdge = null; 23 | this.flow = 0; 24 | }; 25 | 26 | // Main class to manage the network 27 | var FlowNetwork = function() { 28 | this.edges = {}; 29 | 30 | // Is this edge/residual capacity combination in the path already? 31 | this.findEdgeInPath = function(path, edge, residual) { 32 | for(var p=0;p 0 && !this.findEdgeInPath(path, edge, residual)) { 66 | var tpath = path.slice(0); 67 | tpath.push([edge, residual]); 68 | var result = this.findPath(edge.sink, sink, tpath); 69 | if(result != null) return result; 70 | } 71 | } 72 | return null; 73 | }; 74 | 75 | // Find the max flow in this network 76 | this.maxFlow = function(source, sink) { 77 | var path = this.findPath(source, sink, []); 78 | while(path != null) { // ERROR IS HERE - INFINITE LOOP 79 | console.log("while"); 80 | var flow = 9999999999; 81 | // Find the minimum flow 82 | for(var i=0;i b.weight) { return -1; } 205 | return 0; 206 | }) 207 | 208 | // Error handle for insufficient data 209 | if (numSuggestions > result.length) { 210 | error = "Insufficient data! Only data for " + result.length + " suggestions." 211 | console.error(error); 212 | } 213 | 214 | 215 | // Slice the results to handle 216 | result = result.slice(0, numSuggestions); 217 | 218 | // Return with any errors and the result 219 | return cb(error, result) 220 | } 221 | --------------------------------------------------------------------------------