acts_as_recommendable 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,27 @@
1
+ # Because Rails' FileStore doesn't Marshal data
2
+ rails_ver = Rails.respond_to?(:version) ? Rails.version : Rails::VERSION::STRING
3
+ if rails_ver == '2.1.0'
4
+
5
+ module ActiveSupport
6
+ module Cache
7
+ class FileStore < Store
8
+ attr_reader :cache_path
9
+
10
+ def read(name, options = nil)
11
+ super
12
+ File.open(real_file_path(name), 'rb') { |f| Marshal.load(f) } rescue nil
13
+ end
14
+
15
+ def write(name, value, options = nil)
16
+ super
17
+ ensure_cache_path(File.dirname(real_file_path(name)))
18
+ File.atomic_write(real_file_path(name), cache_path) { |f| Marshal.dump(value, f) }
19
+ rescue => e
20
+ logger.error "Couldn't create cache directory: #{name} (#{e.message})" if logger
21
+ end
22
+
23
+ end
24
+ end
25
+ end
26
+
27
+ end
@@ -0,0 +1,73 @@
1
+ require 'inline'
2
+ module MadeByMany
3
+ module ActsAsRecommendable
4
+ class Optimizations
5
+ module InlineC
6
+ inline do |builder|
7
+ builder.include "<math.h>"
8
+ builder.include "<ruby.h>"
9
+ builder.c '
10
+ double c_sim_pearson(VALUE items, int n, VALUE prefs1, VALUE prefs2) {
11
+ double sum1 = 0.0;
12
+ double sum2 = 0.0;
13
+ double sum1Sq = 0.0;
14
+ double sum2Sq = 0.0;
15
+ double pSum = 0.0;
16
+ double num;
17
+ double den;
18
+ int i;
19
+
20
+ VALUE *items_a = RARRAY_PTR(items);
21
+
22
+ for(i=0; i<n; i++) {
23
+ double prefs1_item;
24
+ double prefs2_item;
25
+
26
+ VALUE prefs1_item_ob;
27
+ VALUE prefs2_item_ob;
28
+
29
+ if (!st_lookup(RHASH(prefs1)->ntbl, items_a[i], &prefs1_item_ob)) {
30
+ prefs1_item = 0.0;
31
+ } else {
32
+ prefs1_item = NUM2DBL(prefs1_item_ob);
33
+ }
34
+
35
+ if (!st_lookup(RHASH(prefs2)->ntbl, items_a[i], &prefs2_item_ob)) {
36
+ prefs2_item = 0.0;
37
+ } else {
38
+ prefs2_item = NUM2DBL(prefs2_item_ob);
39
+ }
40
+
41
+ sum1 += prefs1_item;
42
+ sum2 += prefs2_item;
43
+ sum1Sq += pow(prefs1_item, 2);
44
+ sum2Sq += pow(prefs2_item, 2);
45
+ pSum += prefs2_item * prefs1_item;
46
+ }
47
+
48
+ num = pSum - ( ( sum1 * sum2 ) / n );
49
+ den = sqrt( ( sum1Sq - ( pow(sum1, 2) ) / n ) * ( sum2Sq - ( pow(sum2, 2) ) / n ) );
50
+ if(den == 0){
51
+ return 0.0;
52
+ } else {
53
+ return num / den;
54
+ }
55
+ }'
56
+ end
57
+ end
58
+ class << self
59
+ include InlineC
60
+ end
61
+ end
62
+
63
+ module Logic
64
+ # Pearson score
65
+ def self.sim_pearson(prefs, items, person1, person2)
66
+ n = items.length
67
+ return 0 if n == 0
68
+ Optimizations.c_sim_pearson(items, n, prefs[person1], prefs[person2])
69
+ end
70
+
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,12 @@
1
+ require 'rails'
2
+
3
+ ActiveRecord::Base.send(:include, MadeByMany::ActsAsRecommendable)
4
+
5
+
6
+ class MadeByMany::Railtie < ::Rails::Railtie
7
+
8
+ rake_tasks do
9
+ load File.join(File.dirname(__FILE__), 'tasks', 'acts_as_recommendable_tasks.rake')
10
+ # require File.join(File.dirname(__FILE__), 'tasks', 'acts_as_recommendable_tasks.rake')
11
+ end
12
+ end
@@ -0,0 +1,56 @@
1
+ namespace :recommendations do
2
+
3
+ desc 'builds the recommendations dataset'
4
+ task :build => [:environment] do
5
+ MadeByMany::ActsAsRecommendable::Logic.module_eval do
6
+ # This will need to change to your specific model:
7
+ options = User.aar_options
8
+
9
+ puts 'Finding items...'
10
+
11
+ # You may want to optimize this SQL, like this:
12
+ items = options[:on_class].pluck(:id)
13
+
14
+ prefs = {}
15
+
16
+ puts 'Finding users...'
17
+
18
+ # You may want to optimize this SQL
19
+ users = options[:class].includes(options[:on])
20
+
21
+ pbar = ProgressBar.create(:title => 'Gen matrix', :total => items.length)
22
+ items.each do |item_id|
23
+ prefs[item_id] ||= {}
24
+ users.each do |user|
25
+ if user.aar_items_with_scores[item_id]
26
+ score = user.aar_items_with_scores[item_id].aar_score
27
+ prefs[item_id][user.id] = score
28
+ end
29
+ end
30
+ pbar.increment
31
+ end
32
+ pbar.finish
33
+ matrix = [users.collect(&:id), prefs]
34
+
35
+ pbar = ProgressBar.create(:title => 'Gen dataset', :total => prefs.keys.length)
36
+
37
+ if options[:split_dataset]
38
+ generate_dataset(options, matrix) {|item, scores|
39
+ Rails.cache.write("aar_#{options[:on]}_#{item}", scores)
40
+ pbar.inc
41
+ }
42
+ else
43
+ result = {}
44
+ generate_dataset(options, matrix) {|item, scores|
45
+ result[item] = scores
46
+ pbar.increment
47
+ }
48
+ Rails.cache.write("aar_#{options[:on]}_dataset", result)
49
+ end
50
+
51
+ pbar.finish
52
+
53
+ puts 'Rebuild successful'
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,3 @@
1
+ *.sqlite3.db
2
+ *.sqlite3
3
+ *.log
@@ -0,0 +1,124 @@
1
+ require File.dirname(__FILE__) + '/test_helper'
2
+
3
+ class Book < ActiveRecord::Base
4
+ has_many :user_books
5
+ has_many :users, :through => :user_books
6
+ end
7
+
8
+ class UserBook < ActiveRecord::Base
9
+ belongs_to :book
10
+ belongs_to :user
11
+ end
12
+
13
+ class User < ActiveRecord::Base
14
+ has_many :user_books
15
+ has_many :books, :through => :user_books
16
+ acts_as_recommendable :books, :through => :user_books
17
+ end
18
+
19
+
20
+ class ActsAsRecommendableTest < ActiveSupport::TestCase
21
+
22
+ def setup
23
+ @users = (1..10).collect { |n| User.create(:name => "Patient #{n}") }
24
+ @books = (1..20).collect { |n| Book.create(:name => "Book #{n}") }
25
+
26
+ (1..8).collect { |n| UserBook.create(:user_id => 1, :book_id => n) }
27
+ [2, 4, 5, 7, 8].collect { |n| UserBook.create(:user_id => 2, :book_id => n) }
28
+ [3, 4, 5, 6].collect { |n| UserBook.create(:user_id => 3, :book_id => n) }
29
+ [9, 10, 11].collect { |n| UserBook.create(:user_id => 4, :book_id => n) }
30
+ [9, 10].collect { |n| UserBook.create(:user_id => 5, :book_id => n) }
31
+ [1, 2, 19, 20].collect { |n| UserBook.create(:user_id => 6, :book_id => n) }
32
+ [1, 2, 20].collect { |n| UserBook.create(:user_id => 7, :book_id => n) }
33
+ [12, 13, 14, 15, 1, 10, 20].collect { |n| UserBook.create(:user_id => 8, :book_id => n) }
34
+ [12, 13, 1, 10, 20].collect { |n| UserBook.create(:user_id => 9, :book_id => n) }
35
+ [14, 1, 10, 20].collect { |n| UserBook.create(:user_id => 10, :book_id => n) }
36
+ end
37
+
38
+ def test_available_methods
39
+ user = User.find(@users[0].id)
40
+ assert_not_nil user
41
+ assert_respond_to user, :similar_users
42
+ assert_respond_to user, :recommended_books
43
+ end
44
+
45
+ def test_similar_users
46
+ sim_users = get_sim_users
47
+ assert_not_nil sim_users
48
+ end
49
+
50
+ def test_similar_users_format
51
+ sim_users = get_sim_users
52
+ assert_kind_of Array, sim_users
53
+ assert_kind_of User, sim_users.first
54
+ assert_kind_of Numeric, sim_users.first.similar_score
55
+ end
56
+
57
+ def test_similar_users_results
58
+ sim_users = get_sim_users
59
+ assert sim_users.include?(User.find(2))
60
+ assert_respond_to sim_users[0], :similar_score
61
+ assert !sim_users.include?(User.find(5))
62
+ end
63
+
64
+ def test_similar_users_scores
65
+ sim_users = get_sim_users
66
+ assert_respond_to sim_users[0], :similar_score
67
+ assert sim_users[0].similar_score > 0
68
+ end
69
+
70
+ def test_recommended_books
71
+ recommended_books = get_recommend_books
72
+ assert_not_nil recommended_books
73
+ end
74
+
75
+ def test_recommended_books_format
76
+ recommended_books = get_recommend_books
77
+ assert_kind_of Array, recommended_books
78
+ assert_kind_of Book, recommended_books.first
79
+ assert_kind_of Numeric, recommended_books.first.recommendation_score
80
+ end
81
+
82
+ def test_recommended_books_results
83
+ recommended_books = get_recommend_books
84
+ assert_equal true, recommended_books.include?(Book.find(3))
85
+ assert recommended_books.find {|b| b == Book.find(3) }.recommendation_score > 0
86
+ end
87
+
88
+ def test_recommended_books_scores
89
+ recommended_books = get_recommend_books
90
+ assert_respond_to recommended_books[0], :recommendation_score
91
+ assert recommended_books[0].recommendation_score > 0
92
+ end
93
+
94
+ def test_dataset
95
+ use_dataset {
96
+ MadeByMany::ActsAsRecommendable::Logic.module_eval do
97
+ generate_dataset(User.aar_options) {|item, scores|
98
+ Rails.cache.write("aar_#{User.aar_options[:on]}_#{item}", scores)
99
+ }
100
+ end
101
+ recommended_books = get_recommend_books
102
+ assert_equal true, recommended_books.include?(Book.find(3))
103
+ assert recommended_books.find {|b| b == Book.find(3) }.recommendation_score > 0
104
+ }
105
+ end
106
+
107
+ private
108
+ def use_dataset
109
+ User.aar_options[:use_dataset] = true
110
+ yield
111
+ User.aar_options[:use_dataset] = false
112
+ end
113
+
114
+ def get_sim_users
115
+ user = User.find(1)
116
+ user.similar_users
117
+ end
118
+
119
+ def get_recommend_books
120
+ user = User.find(2)
121
+ user.recommended_books
122
+ end
123
+
124
+ end
@@ -0,0 +1,18 @@
1
+ sqlite:
2
+ :adapter: sqlite
3
+ :database: acts_as_recommendable.sqlite.db
4
+ sqlite3:
5
+ :adapter: sqlite3
6
+ :database: acts_as_recommendable.sqlite3.db
7
+ postgresql:
8
+ :adapter: postgresql
9
+ :username: postgres
10
+ :password: postgres
11
+ :database: acts_as_recommendable_test
12
+ :min_messages: ERROR
13
+ mysql:
14
+ :adapter: mysql
15
+ :host: localhost
16
+ :username: rails
17
+ :password:
18
+ :database: acts_as_recommendable_test
@@ -0,0 +1,81 @@
1
+ # Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html
2
+
3
+ no_time_for_goodbye:
4
+ id: 1
5
+ name: No Time For Goodbye
6
+
7
+ the_outcast:
8
+ id: 2
9
+ name: The Outcast
10
+
11
+ the_forgotten_garden:
12
+ id: 3
13
+ name: The Forgotten Garden
14
+
15
+ the_road_home:
16
+ id: 4
17
+ name: The Road Home
18
+
19
+ east_of_the_sun:
20
+ id: 5
21
+ name: East of the Sun
22
+
23
+ the_kite_runner:
24
+ id: 6
25
+ name: The Kite Runner
26
+
27
+ the_book_thief:
28
+ id: 7
29
+ name: The Book Thief
30
+
31
+ watchmen:
32
+ id: 8
33
+ name: The Watchmen
34
+
35
+ down_river:
36
+ id: 9
37
+ name: Down River
38
+
39
+ the_ghost:
40
+ id: 10
41
+ name: The Ghost
42
+
43
+ the_secret:
44
+ id: 11
45
+ name: The Secret
46
+
47
+ eclipse:
48
+ id: 12
49
+ name: Eclipse
50
+
51
+ twilight:
52
+ id: 13
53
+ name: Twilight
54
+
55
+ the_memory_garden:
56
+ id: 14
57
+ name: The Memory Garden
58
+
59
+ the_discovery_of_france:
60
+ id: 15
61
+ name: The Discovery of France
62
+
63
+ devil_may_care:
64
+ id: 16
65
+ name: Devil May Care
66
+
67
+ new_moon:
68
+ id: 17
69
+ name: New Moon
70
+
71
+ the_return:
72
+ id: 18
73
+ name: The Return
74
+
75
+ wife_in_the_north:
76
+ id: 19
77
+ name: Wife in the North
78
+
79
+ fractured:
80
+ id: 20
81
+ name: Fractured
@@ -0,0 +1,189 @@
1
+ # Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html
2
+
3
+ one_one:
4
+ user_id: 1
5
+ book_id: 1
6
+
7
+ one_two:
8
+ user_id: 1
9
+ book_id: 2
10
+
11
+ one_three:
12
+ user_id: 1
13
+ book_id: 3
14
+
15
+ one_four:
16
+ user_id: 1
17
+ book_id: 4
18
+
19
+ one_five:
20
+ user_id: 1
21
+ book_id: 5
22
+
23
+ one_six:
24
+ user_id: 1
25
+ book_id: 6
26
+
27
+ one_seven:
28
+ user_id: 1
29
+ book_id: 7
30
+
31
+ one_eight:
32
+ user_id: 1
33
+ book_id: 8
34
+
35
+ two_one:
36
+ user_id: 2
37
+ book_id: 1
38
+
39
+ two_two:
40
+ user_id: 2
41
+ book_id: 2
42
+
43
+ two_four:
44
+ user_id: 2
45
+ book_id: 4
46
+
47
+ two_five:
48
+ user_id: 2
49
+ book_id: 5
50
+
51
+
52
+ two_seven:
53
+ user_id: 2
54
+ book_id: 7
55
+
56
+ two_eight:
57
+ user_id: 2
58
+ book_id: 8
59
+
60
+ three_three:
61
+ user_id: 3
62
+ book_id: 3
63
+
64
+ three_four:
65
+ user_id: 3
66
+ book_id: 4
67
+
68
+ three_five:
69
+ user_id: 3
70
+ book_id: 5
71
+
72
+ three_six:
73
+ user_id: 3
74
+ book_id: 6
75
+
76
+ four_one:
77
+ user_id: 4
78
+ book_id: 9
79
+
80
+ four_two:
81
+ user_id: 4
82
+ book_id: 10
83
+
84
+ four_three:
85
+ user_id: 4
86
+ book_id: 11
87
+
88
+ five_one:
89
+ user_id: 5
90
+ book_id: 9
91
+
92
+ five_two:
93
+ user_id: 5
94
+ book_id: 10
95
+
96
+ six_one:
97
+ user_id: 6
98
+ book_id: 1
99
+
100
+ six_two:
101
+ user_id: 6
102
+ book_id: 2
103
+
104
+ six_three:
105
+ user_id: 6
106
+ book_id: 19
107
+
108
+ six_four:
109
+ user_id: 6
110
+ book_id: 20
111
+
112
+ seven_one:
113
+ user_id: 7
114
+ book_id: 1
115
+
116
+ seven_two:
117
+ user_id: 7
118
+ book_id: 2
119
+
120
+ seven_four:
121
+ user_id: 7
122
+ book_id: 20
123
+
124
+ eight_one:
125
+ user_id: 8
126
+ book_id: 12
127
+
128
+ eight_two:
129
+ user_id: 8
130
+ book_id: 13
131
+
132
+ eight_three:
133
+ user_id: 8
134
+ book_id: 14
135
+
136
+ eight_four:
137
+ user_id: 8
138
+ book_id: 15
139
+
140
+ eight_five:
141
+ user_id: 8
142
+ book_id: 1
143
+
144
+ eight_six:
145
+ user_id: 8
146
+ book_id: 10
147
+
148
+ eight_seven:
149
+ user_id: 8
150
+ book_id: 20
151
+
152
+ nine_one:
153
+ user_id: 9
154
+ book_id: 12
155
+
156
+ nine_two:
157
+ user_id: 9
158
+ book_id: 13
159
+
160
+
161
+ nine_five:
162
+ user_id: 9
163
+ book_id: 1
164
+
165
+ nine_six:
166
+ user_id: 9
167
+ book_id: 10
168
+
169
+ nine_seven:
170
+ user_id: 9
171
+ book_id: 20
172
+
173
+ ten_three:
174
+ user_id: 10
175
+ book_id: 14
176
+
177
+ ten_five:
178
+ user_id: 10
179
+ book_id: 1
180
+
181
+ ten_six:
182
+ user_id: 10
183
+ book_id: 10
184
+
185
+ ten_seven:
186
+ user_id: 10
187
+ book_id: 20
188
+
189
+