ActsAsTaggableOn is a great gem for working with tags in your application, but it can be a PITA when you want to be able to search for tags.
I have always been one to build my own system when it comes to adding a tagging functionality into an application. But after looking back at ActsAsTaggableOn, I decided to pursue for my current project. It's well-supported and has a lot of helpful features.
But then I ran into an issue. I was to build a contextual tag-based field with autocomplete, and there seemed no simple way to search tags. So here's how I went about solving the problem:
If you're using PostgreSQL in your project (and I encourage you to do so), pg_search
is an amazing tool for implementing searching functionality in your models.
So, first thing's first: add pg_search to your Gemfile and install it.
Gemfile
source 'https://rubygems.org'
# ...
gem 'pg_search'
OK, we have pg_search, but now we need to get it into the correct model, which is ActsAsTaggableOn::Tag
. How the heck can we do that?
Well, we have a few options, but let's do this. Create a directory in app/utilities
and drop a file called search_tags.rb
in there.
Here's what it should look like:
app/utilities/search_tags.rb
ActsAsTaggableOn::Tag.class_eval do
include PgSearch
pg_search_scope :search, :against => [:name],
:using => {
:tsearch => {
:prefix => true, :negation => true, :dictionary => 'english'
}
}
end
This looks just like what you'd put in the model, right? Exactly! All we're doing is using Ruby's class_eval
method to open the model class and insert the support for pg_search.
But, there are a few important points to note here:
utilities
name choice was completely arbitrary. You can call it whatever you want because Rails will eager load anything in app
.lib
if you don't want it part of the app
directory.SearchTags
class based on the way it eager loads. It only throws an error if you attempt to reference SearchTags
. But a simple solution is to define a class (class SearchTags; end
) if you don't like the error you're seeing.app
is not eager loaded, you may have to manually stop Spring (bin/spring stop
).Now you can search as simply as:
@tags = ActsAsTaggableOn::Tag.search('YOUR_SEARCH_TERM')
Simple enough, right?
ActsAsTaggableOn supports contexts, which means you can group tags together into a certain type.
All I did to find the context, was access the Tagging
model and then limit my query to only those that fit the context. So, the above query looks like this:
tag_ids = ActsAsTaggableOn::Tagging.where(:context => 'YOUR_CONTEXT').collect(&:tag_id).uniq
@tags = ActsAsTaggableOn::Tag.where(:id => tag_ids).search('YOUR_SEARCH_TERM')
The first query grabs all eligible tags and then we filter in the next query.
The reason I didn't do something like ActsAsTaggableOn::Tagging.where(:context => 'YOUR_CONTEXT').collect(&:tag).uniq
to grab the tags directly is because that would lead to an N+1 problem and perform way too many queries, when we really only need to run two.
That's all. Now go search!
One other note is that you could be a little more clever here if you really wanted. You could build a service object and instead of writing lengthy queries each time, abstract the logic so all you'd have to do is something like SearchTags.call('CONTEXT', 'TERM')
.
I would take that approach if searching logic was going to be placed throughout your app. I was only adding to one portion of my app so there was no need to spend the time abstracting.