recommender 2.0.0 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6024955f742097c38d66b229a1a39c2eac7665f9a6ccaa870d93d6b5327a8b83
4
- data.tar.gz: df0f2fe577d47dcc775bc1fe884da3be87f5dbcdb8c23e2e69be470937a9f91f
3
+ metadata.gz: f23eac1f93035fe6f01ceb18a5f62ac4062578eed8ea87f7e7ddb0781fb889af
4
+ data.tar.gz: 66b4a78cc848d0587f1451bff6694eba94653e91ee6a916b1a910c099636ed24
5
5
  SHA512:
6
- metadata.gz: 133276891d8a885b903e8cf2512ff263b56980c7752649234281a7d38fa90dc46758da1a5b7d160919457c009854915bb0452fc7a2cd8660685c6d5ca1cec6b2
7
- data.tar.gz: d035dc95292eec167e9acad8df02bca40f93084da8e5abad1aafd6d95f89e81366270cb09df843142cf8b0494ce57b5166326e62aff166880237513aaff06533
6
+ metadata.gz: 1291601deb871fb74aeee99f1936788daaf293757819f7440babe4881a470e500a57b366a7bd8a42d403845113d6ca707e824bd3fac0e97d13d22721635dee21
7
+ data.tar.gz: 5c2d3e693ef42fa4df000276213dd2e71edd72bce77c44606a6e7fa2f3be435d52f2758d3305ed648d238d7ff163f5f94eb6e13e425b525d304558caebfb5783
data/CHANGELOG.md CHANGED
@@ -1,17 +1,18 @@
1
- \# Changelog
1
+ # Changelog
2
2
 
3
- \## \[Unreleased\]
3
+ ## \[2.0.1\] - 2024-07-16
4
4
 
5
- \## \[2.0.0\] - 2024-06-22
6
- \### Added
7
- \- Feature: Similarity based on multiple associations combined with weights.
8
- \- Feature: User-item recommendations based on all their items.
9
- \- Feature: Recommendations based on numerical ratings.
10
- \- Feature: Recommendations based on a weighted mix of various associations.
5
+ ### Added
11
6
 
12
- \### Changed
13
- \- Internal refactoring to support the new features.
7
+ - Feature: Similarity based on multiple associations combined with weights.
8
+ - Feature: User-item recommendations based on all their items.
14
9
 
15
- \## \[0.1.0\] - 2024-06-18
16
- \### Added
17
- \- Initial release
10
+ ### Changed
11
+
12
+ - Internal refactoring to support the new features.
13
+
14
+ ## \[0.1.0\] - 2024-06-18
15
+
16
+ ### Added
17
+
18
+ - Initial release
data/README.md CHANGED
@@ -8,47 +8,48 @@ The `Recommender` gem is a versatile recommendation engine built for Ruby on Rai
8
8
  - **Multiple Association Support:** Compatible with `has_and_belongs_to_many`, `has_many :through`, and `has_many` associations, allowing seamless integration with different data models.
9
9
  - **Customizable Recommendations:** Easily extendable and configurable to fit the specific needs of your application.
10
10
  - **Lightweight and Efficient:** Designed to be efficient and minimalistic, ensuring fast recommendation calculations without heavy overhead.
11
+ - Feature: Similarity based on multiple associations combined with weights.
12
+ - Feature: User-item recommendations based on all their items.
11
13
 
12
- ### Installation
14
+ ### Coming soon:
13
15
 
14
- Add this line to your application's Gemfile:
16
+ - Feature: Recommendations based on a weighted mix of various associations.
15
17
 
16
- ruby
18
+ ### Installation
17
19
 
18
- Copy code
20
+ Add this line to your application's Gemfile:
19
21
 
20
22
  `gem 'recommender'`
21
23
 
22
24
  And then execute:
23
25
 
24
- sh
25
-
26
- Copy code
27
-
28
26
  `bundle install`
29
27
 
30
28
  ### Usage
31
29
 
32
30
  Include the `Recommender::Recommendation` module in your model and set the association:
33
31
 
34
- ```
35
- class Album < ApplicationRecord
36
- include Recommender::Recommendation
37
- has_and_belongs_to_many :users
38
- validates :name, presence: true, uniqueness: { case_sensitive: false }
32
+ ```ruby
33
+ class User < ApplicationRecord
34
+ include Recommender::Recommendation
35
+
36
+ has_many :movie_likes, dependent: :destroy
37
+ has_many :movies, through: :movie_likes
39
38
 
40
- set_association :users
41
- end
39
+ validates :name, presence: true, uniqueness: { case_sensitive: false }
40
+
41
+ set_association :movies
42
+ end
42
43
  ```
43
44
 
44
45
  Now you can get recommendations for an instance:
45
46
 
46
- ```
47
- user = User.find(1)
48
- recommendations = user.recommendations(results: 5)
49
- recommendations.each do |recommended_album, score|
50
- puts "#{recommended_album.name} - Score: #{score}"
51
- end
47
+ ```ruby
48
+ user = User.find(1)
49
+ recommendations = user.recommendations(results: 5)
50
+ recommendations.each do |recommended_movie, score|
51
+ puts "#{recommended_movie.name} - Score: #{score}"
52
+ end
52
53
  ```
53
54
 
54
55
  ### How It Works
@@ -10,47 +10,42 @@ module Recommender
10
10
  extend ClassMethods
11
11
  end
12
12
 
13
- AssociationMetaData = Struct.new("AssociationMetaData", :join_table, :foreign_key, :association_foreign_key, :reflection_name, :weight)
13
+ AssociationMetadata = Struct.new(:join_table, :foreign_key, :association_foreign_key, :reflection_name)
14
14
 
15
15
  module ClassMethods
16
- attr_accessor :association_meta_data
16
+ attr_accessor :association_metadata
17
17
 
18
- def set_associations(associations)
19
- @association_meta_data = associations.map do |association_name, weight|
20
- reflection = reflect_on_association(association_name.to_sym)
21
- raise ArgumentError, "Association '#{association_name}' not found" unless reflection
18
+ def set_association(association_name)
19
+ reflection = reflect_on_association(association_name.to_sym)
20
+ raise ArgumentError, "Association '#{association_name}' not found" unless reflection
22
21
 
23
- build_association_meta_data(reflection, weight)
24
- end
22
+ @association_metadata ||= build_association_metadata(reflection)
25
23
  end
26
24
 
27
25
  private
28
26
 
29
- def build_association_meta_data(reflection, weight)
27
+ def build_association_metadata(reflection)
30
28
  case reflection
31
29
  when ActiveRecord::Reflection::HasAndBelongsToManyReflection
32
- AssociationMetaData.new(
30
+ AssociationMetadata.new(
33
31
  reflection.join_table,
34
32
  reflection.foreign_key,
35
33
  reflection.association_foreign_key,
36
- reflection.name,
37
- weight
34
+ reflection.name
38
35
  )
39
36
  when ActiveRecord::Reflection::ThroughReflection
40
- AssociationMetaData.new(
37
+ AssociationMetadata.new(
41
38
  reflection.through_reflection.table_name,
42
39
  reflection.through_reflection.foreign_key,
43
40
  reflection.association_foreign_key,
44
- reflection.name,
45
- weight
41
+ reflection.name
46
42
  )
47
43
  when ActiveRecord::Reflection::HasManyReflection
48
- AssociationMetaData.new(
44
+ AssociationMetadata.new(
49
45
  reflection.klass.table_name,
50
46
  reflection.foreign_key,
51
47
  reflection.foreign_key,
52
48
  reflection.name,
53
- weight
54
49
  )
55
50
  else
56
51
  raise ArgumentError, "Association '#{reflection.name}' is not a supported type"
@@ -58,33 +53,57 @@ module Recommender
58
53
  end
59
54
  end
60
55
 
61
- def recommendations(results: 5)
56
+ def recommendations(results: 10)
62
57
  other_instances = self.class.where.not(id: id)
63
- item_recommendations = other_instances.reduce(Hash.new(0)) do |acc, instance|
64
- self.class.association_meta_data.each do |meta_data|
65
- self_items = send(meta_data.reflection_name).pluck(:id).to_set
66
- instance_items = instance.send(meta_data.reflection_name).pluck(:id).to_set
67
- common_items = self_items & instance_items
68
-
69
- jaccard_index = common_items.size.to_f / (self_items | instance_items).size
70
- dice_sorensen_coefficient = (2.0 * common_items.size) / (self_items.size + instance_items.size)
71
- collaborative_weight = common_items.size.to_f / Math.sqrt(self_items.size * instance_items.size)
72
-
73
- # Calculate weighted score for this association
74
- association_weighted_score = meta_data.weight * (jaccard_index + dice_sorensen_coefficient + collaborative_weight) / 3.0
75
-
76
- # Recommend items based on this association
77
- (instance_items - common_items).each do |item_id|
78
- acc[item_id] += association_weighted_score unless self_items.include?(item_id)
79
- end
80
- end
81
58
 
82
- acc
59
+ self_items = associated_items.to_set
60
+
61
+ item_recommendations = calculate_recommendations(other_instances, self_items)
62
+
63
+ sorted_recommendation_ids = sort_recommendations(item_recommendations).take(results)
64
+
65
+ fetch_recommendation_objects(sorted_recommendation_ids, item_recommendations)
66
+ end
67
+
68
+ private
69
+
70
+ def associated_items
71
+ send(self.class.association_metadata.reflection_name).pluck(self.class.association_metadata.association_foreign_key)
72
+ end
73
+
74
+ def calculate_recommendations(other_instances, self_items)
75
+ other_instances.each_with_object(Hash.new(0)) do |instance, acc|
76
+ instance_items = instance.send(self.class.association_metadata.reflection_name).pluck(self.class.association_metadata.association_foreign_key).to_set
77
+ common_items = instance_items & self_items
78
+
79
+ weight = calculate_weight(common_items, instance_items, self_items)
80
+
81
+ (instance_items - common_items).each do |item_id|
82
+ acc[item_id] += weight unless self_items.include?(item_id)
83
+ end
83
84
  end
85
+ end
86
+
87
+ def calculate_weight(common_items, instance_items, self_items)
88
+ jaccard_index = common_items.size.to_f / (instance_items | self_items).size
89
+ dice_sorensen_coefficient = (2.0 * common_items.size) / (instance_items.size + self_items.size)
90
+ collaborative_weight = common_items.size.to_f / Math.sqrt(instance_items.size * self_items.size)
84
91
 
85
- sorted_recommendation_ids = item_recommendations.keys.sort_by { |id| item_recommendations[id] }.reverse.take(results)
86
- association_table = self.class.reflect_on_association(self.class.association_meta_data.first.reflection_name).klass
87
- sorted_recommendation_ids.map { |id| [association_table.find(id), item_recommendations[id]] }
92
+ (jaccard_index + dice_sorensen_coefficient + collaborative_weight) / 3.0
93
+ end
94
+
95
+ def sort_recommendations(item_recommendations)
96
+ item_recommendations.keys.sort_by { |id| item_recommendations[id] }.reverse
97
+ end
98
+
99
+ def fetch_recommendation_objects(sorted_recommendation_ids, item_recommendations)
100
+ association = self.class.reflect_on_association(self.class.association_metadata.reflection_name)
101
+ association_table = association.klass
102
+
103
+ sorted_recommendation_ids.map do |id|
104
+ value = item_recommendations[id]
105
+ [association_table.find(id), value.is_a?(Float) && value.nan? ? 0 : value]
106
+ end
88
107
  end
89
108
  end
90
109
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Recommender
4
- VERSION = '2.0.0'
4
+ VERSION = '2.0.1'
5
5
  end
data/recommender.gemspec CHANGED
@@ -31,7 +31,6 @@ Gem::Specification.new do |spec|
31
31
  spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
32
32
  spec.require_paths = ["lib"]
33
33
 
34
- # spec.add_runtime_dependency 'activesupport', '~> 3.0', '>= 3.0.0'
35
34
  spec.add_development_dependency "rspec", "~> 3.9.0"
36
35
  spec.add_development_dependency "pg", "~> 1.1"
37
36
  spec.add_development_dependency "factory_bot_rails", "~> 6.2"
@@ -1,11 +1,9 @@
1
1
  PATH
2
2
  remote: ../..
3
3
  specs:
4
- recommender (0.1.0)
5
- activesupport (>= 3.0.0)
6
- database_cleaner-active_record
4
+ recommender (2.0.0)
5
+ database_cleaner-active_record (~> 2.0)
7
6
  faker (~> 2.21)
8
- pry-rails
9
7
  rails (~> 7.1.3)
10
8
 
11
9
  GEM
@@ -89,7 +87,6 @@ GEM
89
87
  bigdecimal (3.1.8)
90
88
  bindex (0.8.1)
91
89
  builder (3.3.0)
92
- coderay (1.1.3)
93
90
  concurrent-ruby (1.3.3)
94
91
  connection_pool (2.4.1)
95
92
  crass (1.0.6)
@@ -122,7 +119,6 @@ GEM
122
119
  net-pop
123
120
  net-smtp
124
121
  marcel (1.0.4)
125
- method_source (1.1.0)
126
122
  mini_mime (1.1.5)
127
123
  minitest (5.24.0)
128
124
  mutex_m (0.2.0)
@@ -143,11 +139,6 @@ GEM
143
139
  nokogiri (1.16.6-x86_64-linux)
144
140
  racc (~> 1.4)
145
141
  pg (1.5.6)
146
- pry (0.14.2)
147
- coderay (~> 1.1)
148
- method_source (~> 1.0)
149
- pry-rails (0.3.9)
150
- pry (>= 0.10.4)
151
142
  psych (5.1.2)
152
143
  stringio
153
144
  puma (6.4.2)
@@ -4,5 +4,5 @@ class Album < ApplicationRecord
4
4
  has_and_belongs_to_many :users
5
5
  validates :name, presence: true, uniqueness: { case_sensitive: false }
6
6
 
7
- set_associations users: 2.0
7
+ set_association :users
8
8
  end
@@ -6,5 +6,6 @@ class User < ApplicationRecord
6
6
  has_and_belongs_to_many :albums
7
7
 
8
8
  validates :name, presence: true, uniqueness: { case_sensitive: false }
9
- set_associations movies: 3.0
9
+
10
+ set_association :movies
10
11
  end