similar_models 0.3.0 → 0.4.0

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: 1c7c43faef16009cad0a971ec3afc6efc716bd77723d45395a70365e2a96fa9b
4
- data.tar.gz: ac4deb962291c450f53cbf4731ada109ad3a505e009225dcae1f87495c817802
3
+ metadata.gz: 2bf02e51c067023b861f64a73394be810f74ed007a4299a4cc4de18fb7e8ff71
4
+ data.tar.gz: ae0fdceeb1388fa7a247c89d892d812c2554ffb0c7f12c1389da6b313b3b3350
5
5
  SHA512:
6
- metadata.gz: 228a426d2b7a8f10e0db47034e66a8f166ca957ef05e242fc787eeb2c84add141c4b4ba4b95a38f4eaa909cb13d69b8209db968d06b8f4d90ab46445828791c4
7
- data.tar.gz: 3dfee9418bb74aaddc686f96be94a69073b395f7a9edeac5a7a6900044c70cb0386a2da7ac7b84aa69f8766bcb8eb8b12c6c303bbb4f55615846f610c7fa0b1c
6
+ metadata.gz: 3ac48f199b64dc571a9f05caf15128429b809988d8d904750870fa30db3b74a3a5bb5a650862b3b97ce331bd5b9c250efe4efb090c96552c2610da8dbed46760
7
+ data.tar.gz: d0bbb042025768241fcc2b487b9767e19fbd8244f79653d213908added72df10a9368daba02f192390954ebf5f96618b035b49ae4dabbeae0d8c156a629d3dca
data/README.md CHANGED
@@ -1,8 +1,14 @@
1
1
  # Similar Models
2
2
 
3
- Adds a `similar_{model name plural}` method to an active record model, but can be set to any name using `as: {method name}`. It returns the most similar models of the same class based on associated models in common.
3
+ Adds a `similar_#{model_name.plural}` instance and class method to an active record model, but can be set to any name using `as: {method name}`.
4
4
 
5
- The association(s) have to be many to many, so either [habtm](http://guides.rubyonrails.org/association_basics.html#the-has-and-belongs-to-many-association) or [has_many :through](http://guides.rubyonrails.org/association_basics.html#the-has-many-through-association).
5
+ The instance method returns models that have associated models in common ordered by most in common first.
6
+
7
+ The class method returns models ordered by most associated models in common.
8
+
9
+ If the commonality count is the same then a second order clause of `created_at` if present takes precedence.
10
+
11
+ The association(s) have to be many to many, so either [habtm](https://guides.rubyonrails.org/association_basics.html#has-and-belongs-to-many) or [has_many :through](https://guides.rubyonrails.org/association_basics.html#has-many-through).
6
12
 
7
13
  ## Installation
8
14
 
@@ -23,64 +29,80 @@ $ bundle
23
29
  Post example
24
30
 
25
31
  ```ruby
26
- class Post < ActiveRecord::Base
27
- has_many :author_posts
28
- has_many :authors, through: :author_posts
29
- has_and_belongs_to_many :tags
30
-
31
- has_similar_models :authors
32
- has_similar_models :tags, as: :similar_posts_by_tag
33
- has_similar_models :authors, :tags, as: :similar_posts_by_author_and_tag
32
+ class Post < ApplicationRecord
33
+ has_many :author_posts
34
+ has_many :authors, through: :author_posts
35
+ has_and_belongs_to_many :tags
36
+
37
+ has_similar_models :authors
38
+ has_similar_models :tags, as: :similar_posts_by_tag
39
+ has_similar_models :authors, :tags, as: :similar_posts_by_author_and_tag
34
40
  end
35
41
 
36
- class Tag < ActiveRecord::Base
42
+ class Tag < ApplicationRecord
37
43
  end
38
44
 
39
- class Author < ActiveRecord::Base
40
- has_many :author_posts
45
+ class Author < ApplicationRecord
46
+ has_many :author_posts
41
47
  end
42
48
 
43
- class AuthorPosts < ActiveRecord::Base
44
- belongs_to :author
45
- belongs_to :post
49
+ class AuthorPosts < ApplicationRecord
50
+ belongs_to :author
51
+ belongs_to :post
46
52
  end
47
53
  ```
48
54
 
49
- To return the posts with the most authors in common with `post` in descending order:
55
+ To return posts with authors in common with the `post` model by most in common first:
50
56
 
51
57
  ```ruby
52
58
  post.similar_posts
53
59
  ```
54
60
 
61
+ To return posts ordered by most authors in common:
62
+ ```ruby
63
+ Post.similar_posts
64
+ ```
65
+
55
66
  The returned object is an ActiveRecord::Relation and so chaining of other query methods is possible:
56
67
 
57
68
  ```ruby
58
- post.similar_posts.where(posts.created_at: 10.days.ago..).limit(5)
69
+ post.similar_posts.where(created_at: 10.days.ago..).limit(5)
59
70
  ```
60
71
 
61
- To return the posts with the most tags in common with `post` in descending order:
72
+ To return posts with tags in common with the `post` model by most in common first:
62
73
 
63
74
  ```ruby
64
75
  post.similar_posts_by_tag
65
76
  ```
66
77
 
67
- To return the posts with the most authors and tags in common with `post` in descending order:
78
+ To return posts ordered by most tags in common:
79
+ ```ruby
80
+ Post.similar_posts_by_tag
81
+ ```
82
+
83
+ To return posts with the authors and tags in common with the `post` model by most in common first:
68
84
 
69
85
  ```ruby
70
86
  post.similar_posts_by_author_and_tag
71
87
  ```
72
88
 
89
+ To return posts ordered by most authors and tags in common:
90
+
91
+ ```ruby
92
+ Post.similar_posts_by_author_and_tag
93
+ ```
94
+
73
95
  The count of the associated models in common is accessible on each returned model:
74
96
 
75
97
  ```ruby
76
- post.similar_posts_model_count
77
- post.similar_posts_by_tag_model_count
78
- post.similar_posts_by_author_and_tag_model_count
98
+ post.similar_posts_commonality_count
99
+ post.similar_posts_by_tag_commonality_count
100
+ post.similar_posts_by_author_and_tag_commonality_count
79
101
  ```
80
102
 
81
- **Note multiple associations do not work with sqlite.**
103
+ **Note multiple associations for the instance method do not work with sqlite.**
82
104
 
83
- Because of the use of `group`, pagination is not supported.
105
+ **Pagination is not supported on the instance method due to the use of `group by`.**
84
106
 
85
107
  ## In conjunction with acts-as-taggable-on
86
108
 
@@ -1,3 +1,3 @@
1
1
  module SimilarModels
2
- VERSION = '0.3.0'
2
+ VERSION = '0.4.0'
3
3
  end
@@ -3,9 +3,45 @@ require 'similar_models/version'
3
3
  module SimilarModels
4
4
 
5
5
  def has_similar_models(*many_to_many_associations, as: nil)
6
- as = "similar_#{model_name.plural}" unless as
6
+ as ||= "similar_#{model_name.plural}"
7
7
 
8
- # defaults to 'def similar_{model name}'
8
+ # example sql query for one many to many association:
9
+ #
10
+ # SELECT posts.*,
11
+ # (select count(*) from author_posts where post_id != posts.alt_id and author_id in
12
+ # (select author_id from author_posts where post_id = posts.alt_id)) AS similar_posts_commonality_count
13
+ # FROM "posts"
14
+ # ORDER BY similar_posts_commonality_count DESC, created_at DESC
15
+ #
16
+ define_singleton_method as do
17
+ primary_key_ref = "#{table_name}.#{primary_key}"
18
+ similarity_counts = []
19
+
20
+ many_to_many_associations.each do |many_to_many_association|
21
+ association = reflect_on_association(many_to_many_association)
22
+ join_table, foreign_key, association_foreign_key = join_table_values(association)
23
+
24
+ similarity_counts <<
25
+ "(select count(*) from #{join_table} where #{foreign_key} != #{primary_key_ref} and " \
26
+ "#{association_foreign_key} in " \
27
+ "(select #{association_foreign_key} from #{join_table} where #{foreign_key} = #{primary_key_ref}))"
28
+ end
29
+
30
+ order_clause = "#{as}_commonality_count DESC"
31
+ order_clause += ", created_at DESC" if column_names.include?('created_at')
32
+ select("#{table_name}.*, #{similarity_counts.join(' + ')} AS #{as}_commonality_count").order(order_clause)
33
+ end
34
+
35
+ # example sql query for one many to many association:
36
+ #
37
+ # SELECT posts.*, count(posts.alt_id) AS similar_posts_commonality_count
38
+ # FROM "posts"
39
+ # INNER JOIN author_posts ON author_posts.post_id = posts.alt_id
40
+ # WHERE "posts"."alt_id" != ? AND
41
+ # author_posts.author_id IN (select author_posts.author_id from author_posts where author_posts.post_id = ?)
42
+ # GROUP BY posts.alt_id, posts.created_at, posts.updated_at
43
+ # ORDER BY similar_posts_commonality_count DESC, created_at DESC
44
+ #
9
45
  define_method as do
10
46
  table_name = self.class.table_name
11
47
  primary_key = self.class.primary_key
@@ -13,8 +49,8 @@ module SimilarModels
13
49
  association_scopes = []
14
50
 
15
51
  many_to_many_associations.each do |many_to_many_association|
16
- assocation = self.class.reflect_on_association(many_to_many_association)
17
- join_table, foreign_key, association_foreign_key = self.class.join_table_values(assocation)
52
+ association = self.class.reflect_on_association(many_to_many_association)
53
+ join_table, foreign_key, association_foreign_key = self.class.join_table_values(association)
18
54
 
19
55
  association_scopes << self.class.where(
20
56
  "#{join_table}.#{association_foreign_key} IN \
@@ -23,8 +59,10 @@ module SimilarModels
23
59
  ).joins("INNER JOIN #{join_table} ON #{join_table}.#{foreign_key} = #{primary_key_ref}")
24
60
  end
25
61
 
26
- scope = self.class.select("#{table_name}.*, count(#{primary_key_ref}) AS #{as}_model_count").
27
- where.not(primary_key => self.id).order("#{as}_model_count DESC")
62
+ order_clause = "#{as}_commonality_count DESC"
63
+ order_clause += ", created_at DESC" if self.class.column_names.include?('created_at')
64
+ scope = self.class.select("#{table_name}.*, count(#{primary_key_ref}) AS #{as}_commonality_count").
65
+ where.not(primary_key => self.id).order(order_clause)
28
66
  group_by_clause = self.class.column_names.map { |column| "#{table_name}.#{column}"}.join(', ')
29
67
 
30
68
  # if there is only one many-to-many association no need to use UNION sql syntax
@@ -8,8 +8,8 @@ Gem::Specification.new do |spec|
8
8
  spec.version = SimilarModels::VERSION
9
9
  spec.authors = ["Jolyon Pawlyn"]
10
10
  spec.email = ["jpawlyn@gmail.com"]
11
- spec.description = %q{Adds an instance method to an active record model that returns the most similar models based on associated models in common}
12
- spec.summary = %q{Returns models that have the most associated models in common}
11
+ spec.description = %q{Adds a `similar_#{model_name.plural}` instance and class method to an active record model and returns models based on associated models in common ordered by most in common first}
12
+ spec.summary = %q{Returns models that have associated models in common ordered by most in common first}
13
13
  spec.homepage = "https://github.com/jpawlyn/similar_models"
14
14
  spec.license = "MIT"
15
15
 
data/spec/post_spec.rb CHANGED
@@ -5,34 +5,52 @@ describe Post do
5
5
  let(:author2) { Author.create! }
6
6
  let(:author3) { Author.create! }
7
7
  let(:author4) { Author.create! }
8
+ let(:author5) { Author.create! }
8
9
  let(:tag1) { Tag.create! }
9
10
  let(:tag2) { Tag.create! }
10
11
  let(:tag3) { Tag.create! }
11
12
  let(:tag4) { Tag.create! }
12
13
 
13
14
  context 'has_many through:' do
14
- it 'return posts that have the most authors in common with post' do
15
- post = Post.create! authors: [author1, author2, author3]
16
- post1 = Post.create! authors: [author1, author4]
17
- post2 = Post.create! authors: [author1, author2]
18
- post3 = Post.create! authors: [author4]
19
- post4 = Post.create! authors: [author1, author2, author3]
20
-
21
- expect(post.similar_posts.map(&:similar_posts_model_count)).to eq([3, 2, 1])
22
- expect(post.similar_posts).to eq([post4, post2, post1])
15
+ let!(:post) { Post.create! authors: [author1, author2, author3] }
16
+ let!(:post1) { Post.create! authors: [author1] }
17
+ let!(:post2) { Post.create! authors: [author4] }
18
+ let!(:post3) { Post.create! authors: [author1, author2, author3, author4] }
19
+ let!(:post4) { Post.create! authors: [author5] }
20
+
21
+ describe '.similar_posts' do
22
+ it 'return posts ordered by most authors in common' do
23
+ expect(described_class.similar_posts.map(&:similar_posts_commonality_count)).to eq([5, 4, 2, 1, 0])
24
+ expect(described_class.similar_posts).to eq([post3, post, post1, post2, post4])
25
+ end
26
+ end
27
+
28
+ describe '#similar_posts' do
29
+ it 'return posts that have authors in common with `post` ordered by most in common first' do
30
+ expect(post.similar_posts.map(&:similar_posts_commonality_count)).to eq([3, 1])
31
+ expect(post.similar_posts).to eq([post3, post1])
32
+ end
23
33
  end
24
34
  end
25
35
 
26
36
  context 'has_and_belongs_to_many' do
27
- it 'return posts that have the most tags in common with post' do
28
- post = Post.create! tags: [tag1, tag2, tag3]
29
- post1 = Post.create! tags: [tag1, tag4]
30
- post2 = Post.create! tags: [tag1, tag2]
31
- post3 = Post.create! tags: [tag4]
32
- post4 = Post.create! tags: [tag1, tag2, tag3]
33
-
34
- expect(post.similar_posts_by_tag.map(&:similar_posts_by_tag_model_count)).to eq([3, 2, 1])
35
- expect(post.similar_posts_by_tag).to eq([post4, post2, post1])
37
+ let!(:post) { Post.create! tags: [tag1, tag2, tag3] }
38
+ let!(:post1) { Post.create! tags: [tag1, tag4] }
39
+ let!(:post2) { Post.create! tags: [tag2] }
40
+ let!(:post3) { Post.create! tags: [tag2, tag3] }
41
+
42
+ describe '.similar_posts_by_tag' do
43
+ it 'return posts ordered by most tags in common' do
44
+ expect(described_class.similar_posts_by_tag.map(&:similar_posts_by_tag_commonality_count)).to eq([4, 3, 2, 1])
45
+ expect(described_class.similar_posts_by_tag).to eq([post, post3, post2, post1])
46
+ end
47
+ end
48
+
49
+ describe '#similar_posts_by_tag' do
50
+ it 'return posts that have tags in common with `post` ordered by most in common first' do
51
+ expect(post.similar_posts_by_tag.map(&:similar_posts_by_tag_commonality_count)).to eq([2, 1, 1])
52
+ expect(post.similar_posts_by_tag).to eq([post3, post2, post1])
53
+ end
36
54
  end
37
55
  end
38
56
 
@@ -43,9 +61,19 @@ describe Post do
43
61
  let!(:post3) { Post.create! tags: [tag4] }
44
62
  let!(:post4) { Post.create! authors: [author1], tags: [tag1, tag2, tag3] }
45
63
 
46
- it 'return posts that have the most authors and tags in common with post' do
47
- expect(post.similar_posts_by_author_and_tag.map(&:similar_posts_by_author_and_tag_model_count)).to eq([5, 4, 1])
48
- expect(post.similar_posts_by_author_and_tag).to eq([post1, post4, post2])
64
+ describe '.similar_posts_by_author_and_tag' do
65
+ it 'return posts ordered by most authors and tags in common' do
66
+ expect(described_class.similar_posts_by_author_and_tag.map(&:similar_posts_by_author_and_tag_commonality_count))
67
+ .to eq([10, 10, 8, 3, 1])
68
+ expect(described_class.similar_posts_by_author_and_tag).to eq([post1, post, post4, post2, post3])
69
+ end
70
+ end
71
+
72
+ describe '#similar_posts_by_author_and_tag' do
73
+ it 'return posts that have authors and tags in common with `post` ordered by most in common first' do
74
+ expect(post.similar_posts_by_author_and_tag.map(&:similar_posts_by_author_and_tag_commonality_count)).to eq([5, 4, 1])
75
+ expect(post.similar_posts_by_author_and_tag).to eq([post1, post4, post2])
76
+ end
49
77
  end
50
78
  end
51
79
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: similar_models
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jolyon Pawlyn
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2025-02-24 00:00:00.000000000 Z
10
+ date: 2025-02-25 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: activerecord
@@ -107,8 +107,9 @@ dependencies:
107
107
  - - "~>"
108
108
  - !ruby/object:Gem::Version
109
109
  version: '1.0'
110
- description: Adds an instance method to an active record model that returns the most
111
- similar models based on associated models in common
110
+ description: Adds a `similar_#{model_name.plural}` instance and class method to an
111
+ active record model and returns models based on associated models in common ordered
112
+ by most in common first
112
113
  email:
113
114
  - jpawlyn@gmail.com
114
115
  executables: []
@@ -148,7 +149,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
148
149
  requirements: []
149
150
  rubygems_version: 3.6.5
150
151
  specification_version: 4
151
- summary: Returns models that have the most associated models in common
152
+ summary: Returns models that have associated models in common ordered by most in common
153
+ first
152
154
  test_files:
153
155
  - spec/post_spec.rb
154
156
  - spec/spec_helper.rb