disco 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 8f0494a12f2efe7d077d989960b4f40bae39e9d433b8c38d45be367f79ad1a3e
4
+ data.tar.gz: d5ef0e761f5cc3a409e0b7c3d8cdc14a33d5c03ca4eca7b476159c82954ad163
5
+ SHA512:
6
+ metadata.gz: 0a4995986ac209da39ff9f7c449225f1fc560364936f6df76647e33500f8f07749dc857f2a334b7bb59dadd941403feeee4f43f51647a7df799051a095f0cb3b
7
+ data.tar.gz: e1bef5d6f9d3272cbca8a6fc56d7d14bcc2bc2b269ddda7931a256f6a4c019419b2e590e06841e5a09914b9523a5b4c09c9ff8bfd817b3d3bd2e99ecf3368752
data/CHANGELOG.md ADDED
@@ -0,0 +1,3 @@
1
+ ## 0.1.0
2
+
3
+ - First release
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2019 Andrew Kane
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,282 @@
1
+ # Disco
2
+
3
+ :fire: Collaborative filtering for Ruby
4
+
5
+ - Supports user-based and item-based recommendations
6
+ - Works with explicit and implicit feedback
7
+ - Uses matrix factorization
8
+
9
+ [![Build Status](https://travis-ci.org/ankane/disco.svg?branch=master)](https://travis-ci.org/ankane/disco)
10
+
11
+ ## Installation
12
+
13
+ Add this line to your application’s Gemfile:
14
+
15
+ ```ruby
16
+ gem 'disco'
17
+ ```
18
+
19
+ ## Getting Started
20
+
21
+ Create a recommender
22
+
23
+ ```ruby
24
+ recommender = Disco::Recommender.new
25
+ ```
26
+
27
+ If users rate items directly, this is known as explicit feedback. Fit the recommender with:
28
+
29
+ ```ruby
30
+ recommender.fit([
31
+ {user_id: 1, item_id: 1, rating: 5},
32
+ {user_id: 2, item_id: 1, rating: 3}
33
+ ])
34
+ ```
35
+
36
+ > IDs can be integers, strings, or any other data type
37
+
38
+ If users don’t rate items directly (for instance, they’re purchasing items or reading posts), this is known as implicit feedback. Leave out the rating, or use a value like number of purchases, number of page views, or time spent on page:
39
+
40
+ ```ruby
41
+ recommender.fit([
42
+ {user_id: 1, item_id: 1, value: 1},
43
+ {user_id: 2, item_id: 1, value: 1}
44
+ ])
45
+ ```
46
+
47
+ > Use `value` instead of rating for implicit feedback
48
+
49
+ Get user-based (user-item) recommendations - “users like you also liked”
50
+
51
+ ```ruby
52
+ recommender.user_recs(user_id)
53
+ ```
54
+
55
+ Get item-based (item-item) recommendations - “users who liked this item also liked”
56
+
57
+ ```ruby
58
+ recommender.item_recs(item_id)
59
+ ```
60
+
61
+ Use the `count` option to specify the number of recommendations (default is 5)
62
+
63
+ ```ruby
64
+ recommender.user_recs(user_id, count: 3)
65
+ ```
66
+
67
+ Get predicted ratings for specific items
68
+
69
+ ```ruby
70
+ recommender.user_recs(user_id, item_ids: [1, 2, 3])
71
+ ```
72
+
73
+ Get similar users
74
+
75
+ ```ruby
76
+ recommender.similar_users(user_id)
77
+ ```
78
+
79
+ ## Examples
80
+
81
+ ### MovieLens
82
+
83
+ Load the data
84
+
85
+ ```ruby
86
+ data = Disco.load_movielens
87
+ ```
88
+
89
+ Create a recommender and get similar movies
90
+
91
+ ```ruby
92
+ recommender = Disco::Recommender.new(factors: 20)
93
+ recommender.fit(data)
94
+ recommender.item_recs("Star Wars (1977)")
95
+ ```
96
+
97
+ ### Ahoy
98
+
99
+ [Ahoy](https://github.com/ankane/ahoy) is a great source for implicit feedback
100
+
101
+ ```ruby
102
+ views = Ahoy::Event.
103
+ where(name: "Viewed post").
104
+ group(:user_id, "properties->>'post_id'") # postgres syntax
105
+ count
106
+
107
+ data =
108
+ views.map do |(user_id, post_id), count|
109
+ {
110
+ user_id: user_id,
111
+ post_id: post_id,
112
+ value: count
113
+ }
114
+ end
115
+ ```
116
+
117
+ Create a recommender and get recommended posts for a user
118
+
119
+ ```ruby
120
+ recommender = Disco::Recommender.new
121
+ recommender.fit(data)
122
+ recommender.user_recs(current_user.id)
123
+ ```
124
+
125
+ ## Storing Recommendations
126
+
127
+ Disco makes it easy to store recommendations in Rails.
128
+
129
+ ```sh
130
+ rails generate disco:recommendation
131
+ rails db:migrate
132
+ ```
133
+
134
+ For user-based recommendations, use:
135
+
136
+ ```ruby
137
+ class User < ApplicationRecord
138
+ has_recommended :products
139
+ end
140
+ ```
141
+
142
+ > Change `:products` to match the model you’re recommending
143
+
144
+ Save recommendations
145
+
146
+ ```ruby
147
+ User.find_each do |user|
148
+ recs = recommender.user_recs(user.id)
149
+ user.update_recommended_products(recs)
150
+ end
151
+ ```
152
+
153
+ Get recommendations
154
+
155
+ ```ruby
156
+ user.recommended_products
157
+ ```
158
+
159
+ For item-based recommendations, use:
160
+
161
+ ```ruby
162
+ class Product < ApplicationRecord
163
+ has_recommended :products
164
+ end
165
+ ```
166
+
167
+ Specify multiple types of recommendations for a model with:
168
+
169
+ ```ruby
170
+ class User < ApplicationRecord
171
+ has_recommended :products
172
+ has_recommended :products_v2, class_name: "Product"
173
+ end
174
+ ```
175
+
176
+ And use the appropriate methods:
177
+
178
+ ```ruby
179
+ user.update_recommended_products_v2(recs)
180
+ user.recommended_products_v2
181
+ ```
182
+
183
+ For Rails < 6, speed up inserts by adding [activerecord-import](https://github.com/zdennis/activerecord-import) to your app.
184
+
185
+ ## Storing Recommenders
186
+
187
+ If you’d prefer to perform recommendations on-the-fly, store the recommender
188
+
189
+ ```ruby
190
+ bin = Marshal.dump(recommender)
191
+ File.binwrite("recommender.bin", bin)
192
+ ```
193
+
194
+ > You can save it to a file, database, or any other storage system
195
+
196
+ Load a recommender
197
+
198
+ ```ruby
199
+ bin = File.binread("recommender.bin")
200
+ recommender = Marshal.load(bin)
201
+ ```
202
+
203
+ ## Algorithms
204
+
205
+ Disco uses matrix factorization.
206
+
207
+ - For explicit feedback, it uses [stochastic gradient descent](https://www.csie.ntu.edu.tw/~cjlin/papers/libmf/libmf_journal.pdf)
208
+ - For implicit feedback, it uses [coordinate descent](https://www.csie.ntu.edu.tw/~cjlin/papers/one-class-mf/biased-mf-sdm-with-supp.pdf)
209
+
210
+ Specify the number of factors and epochs
211
+
212
+ ```ruby
213
+ Disco::Recommender.new(factors: 8, epochs: 20)
214
+ ```
215
+
216
+ If recommendations look off, trying changing `factors`. The default is 8, but 3 could be good for some applications and 300 good for others.
217
+
218
+ ## Validation
219
+
220
+ Pass a validation set with:
221
+
222
+ ```ruby
223
+ recommender.fit(data, validation_set: validation_set)
224
+ ```
225
+
226
+ ## Cold Start
227
+
228
+ Collaborative filtering suffers from the [cold start problem](https://www.yuspify.com/blog/cold-start-problem-recommender-systems/). It’s unable to make good recommendations without data on a user or item, which is problematic for new users and items.
229
+
230
+ ```ruby
231
+ recommender.user_recs(new_user_id) # returns empty array
232
+ ```
233
+
234
+ There are a number of ways to deal with this, but here are some common ones:
235
+
236
+ - For user-based recommendations, show new users the most popular items.
237
+ - For item-based recommendations, make content-based recommendations with a gem like [tf-idf-similarity](https://github.com/jpmckinney/tf-idf-similarity).
238
+
239
+ ## Daru
240
+
241
+ Disco works with Daru data frames
242
+
243
+ ```ruby
244
+ data = Daru::DataFrame.from_csv("ratings.csv")
245
+ recommender.fit(data)
246
+ ```
247
+
248
+ ## Reference
249
+
250
+ Get the global mean
251
+
252
+ ```ruby
253
+ recommender.global_mean
254
+ ```
255
+
256
+ Get the factors
257
+
258
+ ```ruby
259
+ recommender.user_factors
260
+ recommender.item_factors
261
+ ```
262
+
263
+ ## Credits
264
+
265
+ Thanks to:
266
+
267
+ - [LIBMF](https://github.com/cjlin1/libmf) for providing high performance matrix factorization
268
+ - [Implicit](https://github.com/benfred/implicit/) for serving as an initial reference for user and item similarity
269
+ - [@dasch](https://github.com/dasch) for the gem name
270
+
271
+ ## History
272
+
273
+ View the [changelog](https://github.com/ankane/disco/blob/master/CHANGELOG.md)
274
+
275
+ ## Contributing
276
+
277
+ Everyone is encouraged to help improve this project. Here are a few ways you can help:
278
+
279
+ - [Report bugs](https://github.com/ankane/disco/issues)
280
+ - Fix bugs and [submit pull requests](https://github.com/ankane/disco/pulls)
281
+ - Write, clarify, or fix documentation
282
+ - Suggest or add new features
data/lib/disco.rb ADDED
@@ -0,0 +1,29 @@
1
+ # dependencies
2
+ require "libmf"
3
+ require "numo/narray"
4
+
5
+ # stdlib
6
+ require "csv"
7
+ require "fileutils"
8
+ require "net/http"
9
+
10
+ # modules
11
+ require "disco/data"
12
+ require "disco/recommender"
13
+ require "disco/version"
14
+
15
+ # integrations
16
+ require "disco/engine" if defined?(Rails)
17
+
18
+ module Disco
19
+ class Error < StandardError; end
20
+
21
+ extend Data
22
+ end
23
+
24
+ if defined?(ActiveSupport.on_load)
25
+ ActiveSupport.on_load(:active_record) do
26
+ require "disco/model"
27
+ extend Disco::Model
28
+ end
29
+ end
data/lib/disco/data.rb ADDED
@@ -0,0 +1,75 @@
1
+ module Disco
2
+ module Data
3
+ def load_movielens
4
+ item_path = download_file("ml-100k/u.item", "http://files.grouplens.org/datasets/movielens/ml-100k/u.item",
5
+ file_hash: "553841ebc7de3a0fd0d6b62a204ea30c1e651aacfb2814c7a6584ac52f2c5701")
6
+ data_path = download_file("ml-100k/u.data", "http://files.grouplens.org/datasets/movielens/ml-100k/u.data",
7
+ file_hash: "06416e597f82b7342361e41163890c81036900f418ad91315590814211dca490")
8
+
9
+ # convert u.item to utf-8
10
+ movies_str = File.read(item_path).encode("UTF-8", "binary", invalid: :replace, undef: :replace, replace: "")
11
+
12
+ movies = {}
13
+ CSV.parse(movies_str, col_sep: "|") do |row|
14
+ movies[row[0]] = row[1]
15
+ end
16
+
17
+ data = []
18
+ CSV.foreach(data_path, col_sep: "\t") do |row|
19
+ data << {
20
+ user_id: row[0].to_i,
21
+ item_id: movies[row[1]],
22
+ rating: row[2].to_i
23
+ }
24
+ end
25
+
26
+ data
27
+ end
28
+
29
+ private
30
+
31
+ def download_file(fname, origin, file_hash:)
32
+ # TODO handle this better
33
+ raise "No HOME" unless ENV["HOME"]
34
+ dest = "#{ENV["HOME"]}/.disco/#{fname}"
35
+ FileUtils.mkdir_p(File.dirname(dest))
36
+
37
+ return dest if File.exist?(dest)
38
+
39
+ temp_dir ||= File.dirname(Tempfile.new("disco"))
40
+ temp_path = "#{temp_dir}/#{Time.now.to_f}" # TODO better name
41
+
42
+ digest = Digest::SHA2.new
43
+
44
+ uri = URI(origin)
45
+
46
+ # Net::HTTP automatically adds Accept-Encoding for compression
47
+ # of response bodies and automatically decompresses gzip
48
+ # and deflateresponses unless a Range header was sent.
49
+ # https://ruby-doc.org/stdlib-2.6.4/libdoc/net/http/rdoc/Net/HTTP.html
50
+ Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https") do |http|
51
+ request = Net::HTTP::Get.new(uri)
52
+
53
+ puts "Downloading data from #{origin}"
54
+ File.open(temp_path, "wb") do |f|
55
+ http.request(request) do |response|
56
+ response.read_body do |chunk|
57
+ f.write(chunk)
58
+ digest.update(chunk)
59
+ end
60
+ end
61
+ end
62
+ end
63
+
64
+ if digest.hexdigest != file_hash
65
+ raise Error, "Bad hash: #{digest.hexdigest}"
66
+ end
67
+
68
+ puts "Hash verified: #{file_hash}"
69
+
70
+ FileUtils.mv(temp_path, dest)
71
+
72
+ dest
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,4 @@
1
+ module Disco
2
+ class Engine < Rails::Engine
3
+ end
4
+ end
@@ -0,0 +1,36 @@
1
+ module Disco
2
+ module Model
3
+ def has_recommended(name, class_name: nil)
4
+ class_name ||= name.to_s.singularize.camelize
5
+
6
+ class_eval do
7
+ unless reflect_on_association(:recommendations)
8
+ has_many :recommendations, class_name: "Disco::Recommendation", as: :subject, dependent: :destroy
9
+ end
10
+
11
+ has_many :"recommended_#{name}", -> { where("disco_recommendations.context = ?", name).order("disco_recommendations.score DESC") }, through: :recommendations, source: :item, source_type: class_name
12
+
13
+ define_method("update_recommended_#{name}") do |items|
14
+ now = Time.now
15
+ items = items.map { |item| {subject_type: model_name.name, subject_id: id, item_type: class_name, item_id: item[:item_id], context: name, score: item[:score], created_at: now, updated_at: now} }
16
+
17
+ self.class.transaction do
18
+ recommendations.where(context: name).delete_all
19
+
20
+ if items.any?
21
+ if recommendations.respond_to?(:insert_all!)
22
+ # Rails 6
23
+ recommendations.insert_all!(items)
24
+ elsif recommendations.respond_to?(:bulk_import!)
25
+ # activerecord-import
26
+ recommendations.bulk_import!(items, validate: false)
27
+ else
28
+ recommendations.create!([items])
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,208 @@
1
+ module Disco
2
+ class Recommender
3
+ attr_reader :global_mean, :item_factors, :user_factors
4
+
5
+ def initialize(factors: 8, epochs: 20, verbose: nil)
6
+ @factors = factors
7
+ @epochs = epochs
8
+ @verbose = verbose
9
+ end
10
+
11
+ def fit(train_set, validation_set: nil)
12
+ if defined?(Daru)
13
+ if train_set.is_a?(Daru::DataFrame)
14
+ train_set = train_set.to_a[0]
15
+ end
16
+ if validation_set.is_a?(Daru::DataFrame)
17
+ validation_set = validation_set.to_a[0]
18
+ end
19
+ end
20
+
21
+ @implicit = !train_set.any? { |v| v[:rating] }
22
+
23
+ unless @implicit
24
+ ratings = train_set.map { |o| o[:rating] }
25
+ check_ratings(ratings)
26
+ @min_rating = ratings.min
27
+ @max_rating = ratings.max
28
+
29
+ if validation_set
30
+ check_ratings(validation_set.map { |o| o[:rating] })
31
+ end
32
+ end
33
+
34
+ check_training_set(train_set)
35
+ create_maps(train_set)
36
+
37
+ @rated = Hash.new { |hash, key| hash[key] = {} }
38
+ input = []
39
+ value_key = @implicit ? :value : :rating
40
+ train_set.each do |v|
41
+ u = @user_map[v[:user_id]]
42
+ i = @item_map[v[:item_id]]
43
+ @rated[u][i] = true
44
+
45
+ # explicit will always have a value due to check_ratings
46
+ input << [u, i, v[value_key] || 1]
47
+ end
48
+ @rated.default = nil
49
+
50
+ eval_set = nil
51
+ if validation_set
52
+ eval_set = []
53
+ validation_set.each do |v|
54
+ u = @user_map[v[:user_id]]
55
+ i = @item_map[v[:item_id]]
56
+
57
+ # set to non-existent item
58
+ u ||= -1
59
+ i ||= -1
60
+
61
+ eval_set << [u, i, v[value_key] || 1]
62
+ end
63
+ end
64
+
65
+ loss = @implicit ? 12 : 0
66
+ verbose = @verbose
67
+ verbose = true if verbose.nil? && eval_set
68
+ model = Libmf::Model.new(loss: loss, factors: @factors, iterations: @epochs, quiet: !verbose)
69
+ model.fit(input, eval_set: eval_set)
70
+
71
+ @global_mean = model.bias
72
+
73
+ # TODO read from LIBMF directly to Numo for performance
74
+ @user_factors = Numo::DFloat.cast(model.p_factors)
75
+ @item_factors = Numo::DFloat.cast(model.q_factors)
76
+ end
77
+
78
+ def user_recs(user_id, count: 5, item_ids: nil)
79
+ u = @user_map[user_id]
80
+
81
+ if u
82
+ predictions = @global_mean + @item_factors.dot(@user_factors[u, true])
83
+ predictions.inplace.clip(@min_rating, @max_rating) if @min_rating
84
+
85
+ predictions =
86
+ @item_map.keys.zip(predictions).map do |item_id, pred|
87
+ {item_id: item_id, score: pred}
88
+ end
89
+
90
+ if item_ids
91
+ idx = item_ids.map { |i| @item_map[i] }.compact
92
+ predictions.values_at(*idx)
93
+ else
94
+ @rated[u].keys.each do |i|
95
+ predictions.delete_at(i)
96
+ end
97
+ end
98
+
99
+ predictions.sort_by! { |pred| -pred[:score] } # already sorted by id
100
+ predictions = predictions.first(count) if count && !item_ids
101
+ predictions
102
+ else
103
+ # no items if user is unknown
104
+ # TODO maybe most popular items
105
+ []
106
+ end
107
+ end
108
+
109
+ def similar_items(item_id, count: 5)
110
+ similar(item_id, @item_map, @item_factors, item_norms, count)
111
+ end
112
+ alias_method :item_recs, :similar_items
113
+
114
+ def similar_users(user_id, count: 5)
115
+ similar(user_id, @user_map, @user_factors, user_norms, count)
116
+ end
117
+
118
+ private
119
+
120
+ def user_norms
121
+ @user_norms ||= norms(@user_factors)
122
+ end
123
+
124
+ def item_norms
125
+ @item_norms ||= norms(@item_factors)
126
+ end
127
+
128
+ def norms(factors)
129
+ norms = Numo::DFloat::Math.sqrt((factors * factors).sum(axis: 1))
130
+ norms[norms.eq(0)] = 1e-10 # no zeros
131
+ norms
132
+ end
133
+
134
+ def similar(id, map, factors, norms, count)
135
+ i = map[id]
136
+ if i
137
+ predictions = factors.dot(factors[i, true]) / norms
138
+
139
+ predictions =
140
+ map.keys.zip(predictions).map do |item_id, pred|
141
+ {item_id: item_id, score: pred}
142
+ end
143
+
144
+ predictions.delete_at(i)
145
+ predictions.sort_by! { |pred| -pred[:score] } # already sorted by id
146
+ predictions = predictions.first(count) if count
147
+ predictions
148
+ else
149
+ []
150
+ end
151
+ end
152
+
153
+ def create_maps(train_set)
154
+ user_ids = train_set.map { |v| v[:user_id] }.uniq.sort
155
+ item_ids = train_set.map { |v| v[:item_id] }.uniq.sort
156
+
157
+ @user_map = user_ids.zip(user_ids.size.times).to_h
158
+ @item_map = item_ids.zip(item_ids.size.times).to_h
159
+ end
160
+
161
+ def check_ratings(ratings)
162
+ unless ratings.all? { |r| !r.nil? }
163
+ raise ArgumentError, "Missing ratings"
164
+ end
165
+ unless ratings.all? { |r| r.is_a?(Numeric) }
166
+ raise ArgumentError, "Ratings must be numeric"
167
+ end
168
+ end
169
+
170
+ def check_training_set(train_set)
171
+ raise ArgumentError, "No training data" if train_set.empty?
172
+ end
173
+
174
+ def marshal_dump
175
+ obj = {
176
+ implicit: @implicit,
177
+ user_map: @user_map,
178
+ item_map: @item_map,
179
+ rated: @rated,
180
+ global_mean: @global_mean,
181
+ user_factors: @user_factors,
182
+ item_factors: @item_factors
183
+ }
184
+
185
+ unless @implicit
186
+ obj[:min_rating] = @min_rating
187
+ obj[:max_rating] = @max_rating
188
+ end
189
+
190
+ obj
191
+ end
192
+
193
+ def marshal_load(obj)
194
+ @implicit = obj[:implicit]
195
+ @user_map = obj[:user_map]
196
+ @item_map = obj[:item_map]
197
+ @rated = obj[:rated]
198
+ @global_mean = obj[:global_mean]
199
+ @user_factors = obj[:user_factors]
200
+ @item_factors = obj[:item_factors]
201
+
202
+ unless @implicit
203
+ @min_rating = obj[:min_rating]
204
+ @max_rating = obj[:max_rating]
205
+ end
206
+ end
207
+ end
208
+ end
@@ -0,0 +1,3 @@
1
+ module Disco
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,18 @@
1
+ require "rails/generators/active_record"
2
+
3
+ module Disco
4
+ module Generators
5
+ class RecommendationGenerator < Rails::Generators::Base
6
+ include ActiveRecord::Generators::Migration
7
+ source_root File.join(__dir__, "templates")
8
+
9
+ def copy_migration
10
+ migration_template "migration.rb", "db/migrate/create_disco_recommendations.rb", migration_version: migration_version
11
+ end
12
+
13
+ def migration_version
14
+ "[#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}]"
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,11 @@
1
+ class <%= migration_class_name %> < ActiveRecord::Migration<%= migration_version %>
2
+ def change
3
+ create_table :disco_recommendations do |t|
4
+ t.references :subject, polymorphic: true
5
+ t.references :item, polymorphic: true
6
+ t.string :context
7
+ t.float :score
8
+ t.timestamps
9
+ end
10
+ end
11
+ end
metadata ADDED
@@ -0,0 +1,165 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: disco
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Andrew Kane
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2019-11-14 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: libmf
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: 0.1.3
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: 0.1.3
27
+ - !ruby/object:Gem::Dependency
28
+ name: numo-narray
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: bundler
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rake
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: minitest
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '5'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '5'
83
+ - !ruby/object:Gem::Dependency
84
+ name: activerecord
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: sqlite3
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: daru
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ description:
126
+ email: andrew@chartkick.com
127
+ executables: []
128
+ extensions: []
129
+ extra_rdoc_files: []
130
+ files:
131
+ - CHANGELOG.md
132
+ - LICENSE.txt
133
+ - README.md
134
+ - lib/disco.rb
135
+ - lib/disco/data.rb
136
+ - lib/disco/engine.rb
137
+ - lib/disco/model.rb
138
+ - lib/disco/recommender.rb
139
+ - lib/disco/version.rb
140
+ - lib/generators/disco/recommendation_generator.rb
141
+ - lib/generators/disco/templates/migration.rb.tt
142
+ homepage: https://github.com/ankane/disco
143
+ licenses:
144
+ - MIT
145
+ metadata: {}
146
+ post_install_message:
147
+ rdoc_options: []
148
+ require_paths:
149
+ - lib
150
+ required_ruby_version: !ruby/object:Gem::Requirement
151
+ requirements:
152
+ - - ">="
153
+ - !ruby/object:Gem::Version
154
+ version: '2.4'
155
+ required_rubygems_version: !ruby/object:Gem::Requirement
156
+ requirements:
157
+ - - ">="
158
+ - !ruby/object:Gem::Version
159
+ version: '0'
160
+ requirements: []
161
+ rubygems_version: 3.0.3
162
+ signing_key:
163
+ specification_version: 4
164
+ summary: Collaborative filtering for Ruby
165
+ test_files: []