Liberate Your Search in Rails with Tags

Vinoth
Vinoth
Share

Search is an essential part of any website, even more so for a content-centric website. A good search system is a fast one that provides accurate results. Search and filter are mostly the same depending on how you look at it. For this article, I’ll treat search and filter as the same kind of function.

There are many existing methods to implement search from the tools we use like PostgreSQL full text search, which you can use in Rails. Today, however, we’ll see how to implement tag-based filtering in an Rails app using PostgreSQL.

Let’s get started.

About the Implementation

Let’s see how the tag based implementation works. Assume we have a table called companies and it has the following relation association chain:

companies -> BELONGS_TO -> cities -> BELONGS_TO -> states -> BELONGS_TO -> countries

The companies table has columns such as yearly_revenue, employee_strength, started_year, etc.

Say we have a page for companies and on that page are filters. We now want to filter all the companies that are in a certain country. We could achieve this by joining the tables and filtering based on the country ID provided, easy enough. But what about scenarios where the filters are combined and get more complicated?

For example, we want the following filter:

Companies that are in New York and started before 2001 and have revenue of more than $200 million and employ more than 1000 people.

It’s still achievable, but the query gets extended and, in all likelihood, a bit uglier. The real problem is facilitating the filter options for the users. One way is putting relevant elements (text boxes and selects) for each filter we allow the user to perform, which becomes a problem when we want to include more and more filters. IF we want to do 50 different kinds of filters on the information, it won’t be a pretty sight using one element per filter. Also, on the query part, consider that there are relations from companies going in 10 different directions, each nesting its own set of associations? I am sure you can see how fast this gets ugly. This is where a tagging solution is a much better answer.

In a tag-based implementation, we basically add a tag column with an array datatype to the table in which we want to perform the filtering. In our case, this is the companies table. The column consists of an array of Universally Unique Identifiers (UUIDs) which correspond to the IDs of tags to which the record relates. We’ll have a separate table called tags consisting of the tag id, name, and type. Now, the search and filter is as easy as searching on the tags table, finding the UUIDs, and then filtering the companies based on those tag IDs.

Let’s implement this with help of an example.

Example Rails App

Our Rails app will have five tables

  • companies
  • cities
  • states
  • countries
  • tags

Begin by creating the Rails app along with the necessary models:

rails new tag-example -d postgresql

For the tag implementation to work best, we’ll stick to using a UUID as the primary key in all the tables, so let’s create a migration to enable PostgreSQL’s uuid-ossp extension:

rails g migration enable_uuid_ossp

After generating the migration file, add the following command to the file:

enable_extension "uuid-ossp"

Run the migration:

rake db:create && rake db:migrate

Create our models, starting with companies:

rails g scaffold companies name founding_year:integer city_id:uuid

As I mentioned above, we need to maintain the id field as a UUID for the tables. Head over to the create_companies migration file and modify the create_table line to specify that the id column should be a uuid. PostgreSQL will take care of auto-generating the UUIDs. Also, add a line to add a tags column to the companies table, since including it in a scaffold will make it display to the user.

create_table :companies, id: :uuid do |t|
    t.string :tags, array: true, default: []

Next up, create the cities, states, and countries models, each making the ID a UUID, as mentioned above, by modifying the migrations:

rails g model cities name state_id:uuid
rails g model states name country_id:uuid
rails g model countries name
rails g model tags name tag_type

After you run the migrations, quickly establish the relations in the model files:

## company.rb
belongs_to :city

## city.rb
has_many :companies
belongs_to :state

## state.rb
has_many :cities
belongs_to :country

## country.rb
has_many :states

The example will be clearer if we generate some seed data. For this exercise, I’m using the faker gem to generate the company, city, and state information. Here is the seed (/db/seed.rb) file I’ve used, you can use this for reference;

(1..10).each do |i|
  country = Country.create(name: Faker::Address.country)
  (1..10).each do |j|
    state = State.create(name: Faker::Address.state, country: country)
    (1..10).each do |k|
      city = City.create(name: Faker::Address.city, state: state)
    end
  end
end

City.all.each do |city|
  (1..10).each do |count|
    company_name = Faker::Company.name
    Company.create(name: company_name, founding_year: rand(1950..2015), city: city)
    p "Saved - #{company_name} - #{count}"
  end
end

Now, let’s add our before_save callback to the companies model which will generate the tags for the company every time it’s saved. This takes care of creating the entry in the tags table, too. In models/company.rb add the following code:

class Company < ActiveRecord::Base
  belongs_to :city

  before_save :update_tags

  def save_location_tags
    [city, city.state, city.state.country].map do |loc|
      (Tag.it({id: loc.id, name: loc.name, tag_type: 'LOCATION'}).id rescue nil)
    end
  end

  def update_tags
    self.tags = [
      save_location_tags,
      (Tag.it({name: founding_year, tag_type: 'COMPANY_FOUNDING_YEAR'}).id rescue nil)
    ].flatten.compact
  end
end

The above code actually re-generates and updates the company tags every time it’s saved. In apps/models/tag.rb, add the following:

class Tag < ActiveRecord::Base
  def self.it content
    Tag.where(content).first_or_create! rescue nil
  end
end

You can add as many tags as you want to the update_tag method. We’re all set from the data update standpoint. Let’s now implement the tag search and filter.

Tag Filter

We’ll have a page to display the list of companies and have an autocomplete on the input for the tag names. Also, the code will invoke the filter every time the user makes a selection, displaying the currently selected filters as we go.

In app/controllers/companies_controller.rb add the lines below to the index action:

def index
  if params[:tags]
    @companies = Company.tagged(params[:tags])
  else
    @companies = Company.all.limit(10)
  end
end

In app/models/company.rb, add the tagged scope as follows:

scope :tagged, -> (tags) {where('companies.tags @> ARRAY[?]::varchar[]', [tags].flatten.compact)}

This query accepts an array of tags and filters all the records where the given array is present in its tags column.

We’re now going to use the pg_trgm extension to create the autocomplete for tags. Just like the other extension, we’ll need a migration and enable it:

enable extension "pg_trgm"

Let’s create a tags controller and add an autocomplete endpoint to it, which we’ll make use of from the front-end to get the matching tag IDs for user queries:

rails g controller tags autocomplete

Add the following code to the autocomplete method in tags controller:

def autocomplete
  results = Tag.select("*, string <-> #{ActiveRecord::Base.sanitize(params[:q])} as distance").order('distance').limit(5)
  render json: results
end

Now we have a complete working set of tag-based filter endpoints and tag autocomplete endpoints. You can check them out by starting the Rails server (rails s) and trying the below endpoints:

  • Tag autocomplete example: http://localhost:3000/tags/autocomplete?q=port
  • Filtered companies based on tag example: http://localhost:3000/companies?tags[]=ANYUUIDFROMTAGSTABLE

I’ll leave you to implement the autocomplete in the front-end, since there are many ways and many good tutorials out there on how to achieve this. Combine it with our above filter set and you’ll have a powerful filtering system for your data.

Conclusion

With that we have come to the conclusion of our tutorial. We now have a powerful and efficient filtering and search system readily available to be integrated to any number of tables. It is easy to use it in a new project or to integrate into an existing one.

All the code shown in the example is available in github.

Thanks for reading through and I hope you’ve learned something today.

Frequently Asked Questions (FAQs) about Search in Rails with Tags

How can I implement a tagging system in Rails?

Implementing a tagging system in Rails involves several steps. First, you need to create a Tag model and a Tagging model. The Tag model will store the tags, while the Tagging model will connect the tags with the objects they are tagging. You will also need to set up associations between these models and the objects you want to tag. Once the models and associations are set up, you can create methods to add, remove, and display tags. Finally, you can add a search function that allows users to search for objects based on their tags.

What are the benefits of using tags in Rails?

Tags in Rails provide a flexible way to categorize and organize data. They allow users to search for specific items based on their tags, making it easier to find relevant information. Tags can also be used to filter results, allowing users to narrow down their search to a specific category or topic. Additionally, tags can be used to create relationships between different objects, providing a more interconnected and dynamic data structure.

Can I use tags to improve the search functionality of my Rails application?

Yes, tags can significantly improve the search functionality of your Rails application. By tagging your data, you can provide users with a more targeted and relevant search experience. Users can search for specific tags to find exactly what they’re looking for, or they can use tags to filter their search results. This can make your application more user-friendly and efficient.

How can I add a tag to an object in Rails?

To add a tag to an object in Rails, you first need to create a method in your model that accepts a list of tags. This method should split the list into individual tags, remove any duplicates, and then associate each tag with the object. You can then call this method when creating or updating an object, passing in the list of tags as a parameter.

How can I remove a tag from an object in Rails?

To remove a tag from an object in Rails, you can create a method in your model that accepts a tag as a parameter. This method should find the tag in the object’s list of tags and remove it. You can then call this method when you want to remove a tag from an object.

How can I display the tags associated with an object in Rails?

To display the tags associated with an object in Rails, you can create a method in your model that returns a list of the object’s tags. You can then call this method in your view to display the tags.

How can I search for objects based on their tags in Rails?

To search for objects based on their tags in Rails, you can create a method in your model that accepts a tag as a parameter. This method should return all objects that are associated with the given tag. You can then call this method in your controller to perform the search.

Can I use tags to create relationships between different objects in Rails?

Yes, tags can be used to create relationships between different objects in Rails. By tagging objects with the same tag, you can create a connection between them. This can be useful for creating a more interconnected and dynamic data structure.

How can I filter search results based on tags in Rails?

To filter search results based on tags in Rails, you can create a method in your model that accepts a list of tags as a parameter. This method should return all objects that are associated with any of the given tags. You can then call this method in your controller to perform the search.

Can I use tags to categorize data in Rails?

Yes, tags can be used to categorize data in Rails. By tagging objects with relevant tags, you can organize your data into different categories. This can make it easier for users to find relevant information and can also improve the overall structure of your data.