acts_as_recommendable 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +15 -0
- data/Gemfile +3 -0
- data/MIT-LICENSE +20 -0
- data/README +123 -0
- data/Rakefile +17 -0
- data/acts_as_recommendable.gemspec +29 -0
- data/init.rb +14 -0
- data/lib/acts_as_recommendable.rb +365 -0
- data/lib/cache_fix.rb +27 -0
- data/lib/optimizations.rb +73 -0
- data/lib/railtie.rb +12 -0
- data/lib/tasks/acts_as_recommendable_tasks.rake +56 -0
- data/test/.gitignore +3 -0
- data/test/acts_as_recommendable_test.rb +124 -0
- data/test/database.yml +18 -0
- data/test/fixtures/books.yml +81 -0
- data/test/fixtures/user_books.yml +189 -0
- data/test/fixtures/users.yml +41 -0
- data/test/schema.rb +19 -0
- data/test/test_helper.rb +43 -0
- metadata +186 -0
data/lib/cache_fix.rb
ADDED
@@ -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
|
data/lib/railtie.rb
ADDED
@@ -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
|
data/test/.gitignore
ADDED
@@ -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
|
data/test/database.yml
ADDED
@@ -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
|
+
|