PgTaggable
A simple tagging gem for Rails using PostgreSQL array.
Installation
Add this line to your application's Gemfile:
gem "pg_taggable"
And then execute:
$ bundle
Or install it yourself as:
$ gem install pg_taggable
Usage
Setup
Add array columns and index to your table. For example:
class CreatePosts < ActiveRecord::Migration[8.0]
def change
create_table :posts do |t|
t.string :tags, array: true, default: []
t.timestamps
t.index :tags, using: 'gin'
end
end
end
Indicate that attribute is "taggable" in a Rails model, like this:
class Post < ActiveRecord::Base
taggable :tags
end
Modify
You can modify it like normal array
#set
post.tags = ['food', 'travel']
#add
post.tags += ['food']
post.tags += ['food', 'travel']
post.tags << 'food'
#remove
post.tags -= ['food']
Queries
any_#{tag_name}
Find records with any of the tags.
Post.where(any_tags: ['food', 'travel'])
You can use with not
Post.where.not(any_tags: ['food', 'travel'])
all_#{tag_name}
Find records with all of the tags
Post.where(all_tags: ['food', 'travel'])
#{tag_name}_in
Find records that have all the tags included in the list
Post.where(tags_in: ['food', 'travel'])
#{tag_name}_eq
Find records that have exact same tags as the list, order is not important
Post.where(tags_eq: ['food', 'travel'])
Assume a post has tags: 'A', 'B'
Method | Query | Matched |
---|---|---|
any_tags | A | True |
any_tags | A, B | True |
any_tags | B, A | True |
any_tags | A, B, C | True |
all_tags | A | True |
all_tags | A, B | True |
all_tags | B, A | True |
all_tags | A, B, C | False |
tags_in | A | False |
tags_in | A, B | True |
tags_in | B, A | True |
tags_in | A, B, C | True |
tags_eq | A | False |
tags_eq | A, B | True |
tags_eq | B, A | True |
tags_eq | A, B, C | False |
Class Methods
taggable(name, unique: true)
Indicate that attribute is "taggable".
unique: true
You can use unique
option to ensure that tags are unique. It will be deduplicated before saving. The default is true
.
# taggable :tags, unique: true
post = Post.create(tags: ['food', 'travel', 'food'])
post.tags
# => ['food', 'travel']
# taggable :tags, unique: false
post = Post.create(tags: ['food', 'travel', 'food'])
post.tags
# => ['food', 'travel', 'food']
#{tag_name}
Return unnested tags. The column name will be tag
, For example:
Post.tags
# => #<ActiveRecord::Relation [#<Post tag: "food", id: nil>, #<Post tag: "travel", id: nil>, #<Post tag: "travel", id: nil>, #<Post tag: "technology", id: nil>]>
Post.tags.size
# => 4
Post.tags.select(:tag).distinct.size
# => 3
Post.tags.distinct.pluck(:tag)
# => ["food", "travel", "technology"]
Post.tags.group(:tag).count
# => {"food"=>1, "travel"=>2, "technology"=>1}
distinct_#{tag_name}
Return an array of distinct tag records. It can be used for paging, count or other query.
Post.distinct_tags
# => #<ActiveRecord::Relation [#<Post tag: "food", id: nil>, #<Post tag: "travel", id: nil>, #<Post tag: "technology", id: nil>]>
# equal to
Post.tags.select(:tag).distinct
uniq_#{tag_name}
Return an array of unique tag strings.
Post.uniq_tags
# => ["food", "travel", "technology"]
# equal to
Post.tags.distinct.pluck(:tag)
count_#{tag_name}
Calculates the number of occurrences of each tag.
Post.count_tags
# => {"food"=>1, "travel"=>2, "technology"=>1}
# equal to
Post.tags.group(:tag).count
any_#{tag_name}(value, delimiter = ',')
It will create some scopes, this is useful for using ransack
Post.any_tags(['food', 'travel'])
# equal to
Post.where(any_tags: ['food', 'travel'])
Scope support string input
Post.any_tags('food,travel')
Post.any_tags('food|travel', '|')
all_#{tag_name}(value, delimiter = ',')
Post.all_tags(['food', 'travel'])
# equal to
Post.where(all_tags: ['food', 'travel'])
#{tag_name}_in(value, delimiter = ',')
Post.tags_in(['food', 'travel'])
# equal to
Post.where(tags_in: ['food', 'travel'])
#{tag_name}_eq(value, delimiter = ',')
Post.tags_eq(['food', 'travel'])
# equal to
Post.where(tags_eq: ['food', 'travel'])
not_any_#{tag_name}(value, delimiter = ',')
Post.not_any_tags(['food', 'travel'])
# equal to
Post.where.not(any_tags: ['food', 'travel'])
not_all_#{tag_name}(value, delimiter = ',')
Post.not_all_tags(['food', 'travel'])
# equal to
Post.where.not(all_tags: ['food', 'travel'])
not_#{tag_name}_in(value, delimiter = ',')
Post.not_tags_in(['food', 'travel'])
# equal to
Post.where.not(tags_in: ['food', 'travel'])
not_#{tag_name}_eq(value, delimiter = ',')
Post.not_tags_eq(['food', 'travel'])
# equal to
Post.where.not(tags_eq: ['food', 'travel'])
Case Insensitive
If you use string
type, it is case sensitive.
# tags is string[]
post = Post.create(tags: ['food', 'travel', 'Food'])
post.tags
# => ['food', 'travel', 'Food']
If you want case insensitive, you need to use citext
class CreatePosts < ActiveRecord::Migration[8.0]
enable_extension('citext') unless extensions.include?('citext')
def change
create_table :posts do |t|
t.citext :tags, array: true, default: []
t.timestamps
t.index :tags, using: 'gin'
end
end
end
You will get the diffent result
# tags is citext[]
post = Post.create(tags: ['food', 'travel', 'Food'])
post.tags
# => ['food', 'travel']
Ransack
You can use with ransack
class Post < ActiveRecord::Base
def self.ransackable_scopes(_auth_object = nil)
%i[all_tags]
end
end
And you can search
Post.ransack(all_tags: 'foold,travel')
License
The gem is available as open source under the terms of the MIT License.
Contact
The project's website is located at https://github.com/emn178/pg_taggable
Author: Chen, Yi-Cyuan (emn178@gmail.com)