similar_models 0.3.0 → 0.4.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 +4 -4
- data/README.md +49 -25
- data/lib/similar_models/version.rb +1 -1
- data/lib/similar_models.rb +67 -8
- data/similar_models.gemspec +2 -2
- data/spec/post_spec.rb +49 -21
- metadata +7 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 1512f7d1b0d895cc90aa6bf3357e8379aac8f57848de01f2a0d05816107e8936
|
4
|
+
data.tar.gz: 5001875c74fce6ba9bfbce5af20424742c87564d07a7b16393f953ada0b59cfa
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 76f8b14e266fc0cf9063c19d5a39c6ce346b05539a450ab100e13e0440b7ea6293b4c02c1250f1e9a68a37fb9a7d952b3b6380bf3ea64bdd5c01984848d001a2
|
7
|
+
data.tar.gz: 51e379dc7d45bd905d0a52d791b8cd9a869ee5fe285848ced7ff3a579e4758ccb677095faadd5b55d812055acc87b4c0dbdc260e95aaa1ef8b51fa983d50b32b
|
data/README.md
CHANGED
@@ -1,8 +1,16 @@
|
|
1
1
|
# Similar Models
|
2
2
|
|
3
|
-
Adds a `similar_{
|
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
|
5
|
+
The instance method returns models that have associated models in common ordered by most in common first.
|
6
|
+
|
7
|
+
A practical example for this could be linking to related blog posts when rendering an individual blog post. Related blog posts could be related by author, tag or author and tag combined.
|
8
|
+
|
9
|
+
The class method returns models ordered by most associated models in common.
|
10
|
+
|
11
|
+
If the commonality count is the same then a second order clause of `created_at` takes precedence if present.
|
12
|
+
|
13
|
+
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
14
|
|
7
15
|
## Installation
|
8
16
|
|
@@ -23,64 +31,80 @@ $ bundle
|
|
23
31
|
Post example
|
24
32
|
|
25
33
|
```ruby
|
26
|
-
class Post <
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
+
class Post < ApplicationRecord
|
35
|
+
has_many :author_posts
|
36
|
+
has_many :authors, through: :author_posts
|
37
|
+
has_and_belongs_to_many :tags
|
38
|
+
|
39
|
+
has_similar_models :authors
|
40
|
+
has_similar_models :tags, as: :similar_posts_by_tag
|
41
|
+
has_similar_models :authors, :tags, as: :similar_posts_by_author_and_tag
|
34
42
|
end
|
35
43
|
|
36
|
-
class Tag <
|
44
|
+
class Tag < ApplicationRecord
|
37
45
|
end
|
38
46
|
|
39
|
-
class Author <
|
40
|
-
|
47
|
+
class Author < ApplicationRecord
|
48
|
+
has_many :author_posts
|
41
49
|
end
|
42
50
|
|
43
|
-
class AuthorPosts <
|
44
|
-
|
45
|
-
|
51
|
+
class AuthorPosts < ApplicationRecord
|
52
|
+
belongs_to :author
|
53
|
+
belongs_to :post
|
46
54
|
end
|
47
55
|
```
|
48
56
|
|
49
|
-
To return
|
57
|
+
To return posts with authors in common with the `post` model by most in common first:
|
50
58
|
|
51
59
|
```ruby
|
52
60
|
post.similar_posts
|
53
61
|
```
|
54
62
|
|
63
|
+
To return posts ordered by most authors in common:
|
64
|
+
```ruby
|
65
|
+
Post.similar_posts
|
66
|
+
```
|
67
|
+
|
55
68
|
The returned object is an ActiveRecord::Relation and so chaining of other query methods is possible:
|
56
69
|
|
57
70
|
```ruby
|
58
|
-
post.similar_posts.where(
|
71
|
+
post.similar_posts.where(created_at: 10.days.ago..).limit(5)
|
59
72
|
```
|
60
73
|
|
61
|
-
To return
|
74
|
+
To return posts with tags in common with the `post` model by most in common first:
|
62
75
|
|
63
76
|
```ruby
|
64
77
|
post.similar_posts_by_tag
|
65
78
|
```
|
66
79
|
|
67
|
-
To return
|
80
|
+
To return posts ordered by most tags in common:
|
81
|
+
```ruby
|
82
|
+
Post.similar_posts_by_tag
|
83
|
+
```
|
84
|
+
|
85
|
+
To return posts with the authors and tags in common with the `post` model by most in common first:
|
68
86
|
|
69
87
|
```ruby
|
70
88
|
post.similar_posts_by_author_and_tag
|
71
89
|
```
|
72
90
|
|
91
|
+
To return posts ordered by most authors and tags in common:
|
92
|
+
|
93
|
+
```ruby
|
94
|
+
Post.similar_posts_by_author_and_tag
|
95
|
+
```
|
96
|
+
|
73
97
|
The count of the associated models in common is accessible on each returned model:
|
74
98
|
|
75
99
|
```ruby
|
76
|
-
post.
|
77
|
-
post.
|
78
|
-
post.
|
100
|
+
post.similar_posts_commonality_count
|
101
|
+
post.similar_posts_by_tag_commonality_count
|
102
|
+
post.similar_posts_by_author_and_tag_commonality_count
|
79
103
|
```
|
80
104
|
|
81
|
-
**Note multiple associations do not work with sqlite.**
|
105
|
+
**Note multiple associations for the instance method do not work with sqlite.**
|
82
106
|
|
83
|
-
|
107
|
+
**Pagination is not supported on the instance method due to the use of `group by`.**
|
84
108
|
|
85
109
|
## In conjunction with acts-as-taggable-on
|
86
110
|
|
data/lib/similar_models.rb
CHANGED
@@ -3,9 +3,66 @@ require 'similar_models/version'
|
|
3
3
|
module SimilarModels
|
4
4
|
|
5
5
|
def has_similar_models(*many_to_many_associations, as: nil)
|
6
|
-
as
|
6
|
+
as ||= "similar_#{model_name.plural}"
|
7
7
|
|
8
|
-
#
|
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
|
43
|
+
# ORDER BY similar_posts_commonality_count DESC, created_at DESC
|
44
|
+
#
|
45
|
+
# example sql query for two many to many associations:
|
46
|
+
#
|
47
|
+
# SELECT posts.*, count(posts.alt_id) AS similar_posts_by_author_and_tag_commonality_count
|
48
|
+
# FROM (
|
49
|
+
# (SELECT "posts".* FROM "posts"
|
50
|
+
# INNER JOIN author_posts ON author_posts.post_id = posts.alt_id
|
51
|
+
# WHERE (author_posts.author_id IN
|
52
|
+
# (select author_posts.author_id from author_posts where author_posts.post_id = ?)
|
53
|
+
# )
|
54
|
+
# )
|
55
|
+
# UNION ALL
|
56
|
+
# (SELECT "posts".* FROM "posts"
|
57
|
+
# INNER JOIN posts_tags ON posts_tags.post_id = posts.alt_id
|
58
|
+
# WHERE (posts_tags.tag_id IN
|
59
|
+
# (select posts_tags.tag_id from posts_tags where posts_tags.post_id = ?)
|
60
|
+
# )
|
61
|
+
# )
|
62
|
+
# )
|
63
|
+
# AS posts WHERE "posts"."alt_id" != ? GROUP BY posts.alt_id, posts.created_at, posts.updated_at
|
64
|
+
# ORDER BY similar_posts_by_author_and_tag_commonality_count DESC, created_at DESC
|
65
|
+
#
|
9
66
|
define_method as do
|
10
67
|
table_name = self.class.table_name
|
11
68
|
primary_key = self.class.primary_key
|
@@ -13,8 +70,8 @@ module SimilarModels
|
|
13
70
|
association_scopes = []
|
14
71
|
|
15
72
|
many_to_many_associations.each do |many_to_many_association|
|
16
|
-
|
17
|
-
join_table, foreign_key, association_foreign_key = self.class.join_table_values(
|
73
|
+
association = self.class.reflect_on_association(many_to_many_association)
|
74
|
+
join_table, foreign_key, association_foreign_key = self.class.join_table_values(association)
|
18
75
|
|
19
76
|
association_scopes << self.class.where(
|
20
77
|
"#{join_table}.#{association_foreign_key} IN \
|
@@ -23,14 +80,16 @@ module SimilarModels
|
|
23
80
|
).joins("INNER JOIN #{join_table} ON #{join_table}.#{foreign_key} = #{primary_key_ref}")
|
24
81
|
end
|
25
82
|
|
26
|
-
|
27
|
-
|
28
|
-
|
83
|
+
order_clause = "#{as}_commonality_count DESC"
|
84
|
+
order_clause += ", created_at DESC" if self.class.column_names.include?('created_at')
|
85
|
+
scope = self.class.select("#{table_name}.*, count(#{primary_key_ref}) AS #{as}_commonality_count").
|
86
|
+
where.not(primary_key => self.id).order(order_clause)
|
29
87
|
|
30
88
|
# if there is only one many-to-many association no need to use UNION sql syntax
|
31
89
|
if association_scopes.one?
|
32
|
-
scope.merge(association_scopes.first).group(
|
90
|
+
scope.merge(association_scopes.first).group(primary_key_ref)
|
33
91
|
else
|
92
|
+
group_by_clause = self.class.column_names.map { |column| "#{table_name}.#{column}"}.join(', ')
|
34
93
|
scope.from("((#{association_scopes.map(&:to_sql).join(') UNION ALL (')})) AS #{table_name}").group(group_by_clause)
|
35
94
|
end
|
36
95
|
end
|
data/similar_models.gemspec
CHANGED
@@ -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
|
12
|
-
spec.summary = %q{Returns models that have
|
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
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
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
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
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
|
-
|
47
|
-
|
48
|
-
|
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.
|
4
|
+
version: 0.4.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jolyon Pawlyn
|
8
8
|
bindir: bin
|
9
9
|
cert_chain: []
|
10
|
-
date: 2025-02-
|
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
|
111
|
-
|
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
|
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
|