Rails 6: How to search and filter index results
Tags: Rails, Tutorial, Ruby on Rails, Search

This story covers the following topics:

Filter results of an index list with a query string

Filter results of an index list with additional filter masks (checkboxes and dropdowns)

Setting up our example blog

We start with a simple Rails 6 application that has a list of stories. Each story has these attributes:

Title: The title of the story

Body: The written content of the story

Draft: A boolean flag indicating the state of the story

Category: An enum with the values “coding”, “social” or “politics”

rails _6.0.3.4_ new --database=postgresql --skip-turbolinks --skip-test my_blog && cd my_blog bundle exec bin/setup

bundle exec rails generate scaffold story title body:text draft:boolean category:string

bundle exec rails db:migrate bundle exec rails runner "Story.create!(title: 'Rails 6 new features', body: 'Multi database support...', draft: false, category: 'coding')"

bundle exec rails runner "Story.create!(title: 'Rails 7 new features', body: 'Not yet finished...', draft: true, category: 'coding')"

bundle exec rails runner "Story.create!(title: 'Traveling by train', body: 'Using the rails instead of the road has...', draft: false, category: 'social')"

bundle exec rails runner "Story.create!(title: 'New president', body: 'Another president...', draft: false, category: 'politics')" bundle exec rails server

Navigating to http://localhost:3000/stories should now give you a simple CRUD with the seeded stories.

CRUD index view without a filter

Adding a service model to filter the story records

Our next step is to add a filter logic to the Story model. The interface should allow us to submit a query like Story.filtered(text: 'Rails', draft: false, category: 'coding') .

Therefore we add a scope method to the model app/models/story.rb :

scope :filtered, ->(query_params) { Story::Filter.new.filter(self, query_params) }

It delegates its call to a new class app/models/story/filter.rb with the following content:

class Story::Filter



def filter(scope, query_params)

if query_params[:text].present?

scope = scope.where("title ILIKE :text OR body ILIKE :text", text: "%#{query_params[:text]}%")

end



unless query_params[:draft].nil? || query_params[:draft] == ''

scope = scope.where(draft: query_params[:draft])

end



if query_params[:category].present?

scope = scope.where(category: query_params[:category])

end



scope

end



end

It uses the ILIKE expression from PostgreSQL, which is the case-insensitive variant of LIKE. The percent sign ( % ) matches any sequence of zero or more characters. Let’s look at an example output for this query:

Story.filtered(text: 'Rails', draft: false, category: 'coding').to_sql #=> SELECT "stories".* FROM "stories" WHERE (title ILIKE '%Rails%' OR body ILIKE '%Rails%') AND "stories"."draft" = FALSE AND "stories"."category" = 'coding'

Submitting the query params to the model

Finally we can create a filter mask in the index view app/views/stories/index.html.erb .

<%= form_for :query, url: stories_path, method: :get do |form| %>

<div class="field">

<%= form.text_field :text, placeholder: 'Search...', value: params.dig('query', 'text') %>

</div>



<div class="field">

<%= form.label :draft %>

<%= form.check_box :draft, {checked: ActiveModel::Type::Boolean.new.cast(params.dig('query', 'draft'))} %>

</div>



<div class="field">

<%= form.label :category %>

<%= form.select :category, [['Coding', 'coding'], ['Social', 'social'], ['Politics', 'politics']], include_blank: true, selected: params.dig('query', 'category') %>

</div>



<div class="actions">

<%= form.submit 'Search' %>

</div>

<% end %>

And apply the filter params to the scope in app/controllers/stories_controller.rb .

def index

@stories = Story.filtered(query_params)

end private def query_params

query_params = params[:query]

query_params ? query_params.permit(:text, :draft, :category) : {}

end

That’s it. We now have a simple filter mask!

CRUD index view with filters

Outline

There are many things you can improve in this example. Here are some suggestions for the next steps, which are not part of this story:

Add some Javascript that listens on change events in the query form and refreshes the list without clicking the search button

Allow the user to reset all search filters and store the filters for a user across different requests (cookie or database)

Allow the user to see which filters are active (e.g. the draft flag is not applied when the site is loaded the first time)

Use a form model for the query and remove the logic where we retrieve the previous values from the params hash

For a more advanced search try to use a fulltext / traigram search in PostgreSQL or setup an additional Elasticsearch cluster.

If you like to see a Rails application with a simple search, check out my side project wintermute.