0.0
The project is in a healthy, maintained state
A simple tagging gem for Rails using PostgreSQL array.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
 Dependencies

Runtime

>= 8.0.0
 Project Readme

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)