When implementing a complex search we often struggle with disorganization and a number of actions in a controller, or with complex classes that handle the search terms. How to prevent this mess? Filtering records is a simple task, it is enough to correctly construct a query using ranges, and make them depend on received parameters.

searching

I prepared a library (https://github.com/tkowalewski/criteriable) which facilitates creating search criteria and searching by them, broadening the scope of a search according to given parameters.

Let’s use criteriable to create a simple task search engine:

We create a new project by executing the command:

1
$ [example/master]: rails new .

Using a generator, let’s create a model, controller and database migrations:

1
$ [example/master]: rails generate scaffold Task title:string description:text

Add the necessary gems to Gemfile:

1
2
gem 'criteriable', git: 'https://github.com/tkowalewski/criteriable.git'
gem 'active_type'

Create representations of the search form:

1
2
3
4
5
6
7
# app/filters/tasks_filter.rb

class TasksFilter < ActiveType::Object
 attribute :title
 attribute :description
 attribute :text
end

Create your own search criteria, based on checking phrases in specified columns.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# app/criterions/custom_criterion.rb

class CustomCriterion < Criteriable::BaseCriterion
 def apply(scope)
   condition = nil
   fields.each do |field|
     if condition
       condition = condition.or(scope.arel_table[field].matches("%#{value}%"))
     else
       condition = scope.arel_table[field].matches("%#{value}%")
     end
   end

   scope.where(condition)
 end

 private

 def fields
   @fields ||= options.fetch(:fields, [name])
 end
end

Update the model of tasks by defining the search criteria:

1
2
3
4
5
6
7
# app/models/task.rb

class Task < ActiveRecord::Base
 criterion :title
 criterion :description
 criterion :text, type: CustomCriterion, fields: [:title, :description]
end

Update the view of the task list by adding a filter form:

1
2
3
4
5
6
7
8
9
10
11
12
# app/views/tasks/index.html.erb

<%= form_for @filter, url: tasks_path, method: :get do |form| %>
   <%= form.text_field :text, placeholder: "..." %>

   <%= form.text_field :title, placeholder: "Title" %>

   <%= form.text_field :description, placeholder: "Description" %>

   <%= form.submit "Filter" %>
   <%= link_to "Clear", tasks_path %>
<% end %>

Update the tasks controller:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# app/controllers/tasks_controller.rb

class TasksController < ApplicationController
 def index
   @filter = TasksFilter.new(tasks_filter_params)
   @tasks = Task.search(tasks_filter_params)
 end

# ...

 def tasks_filter_params
   params.key?(:tasks_filter) ? params.require(:tasks_filter).permit(:title, :description, :text) : {}
 end
end

Criteriable module adds two methods to models, i.e. criterion and search. Criterion method facilitates defining search criteria, and the search method applies the criteria to the search scope.

To begin with, I defined one type of search criterion applying a condition with SQL WHERE instruction to the scope of the search.

1
2
3
4
5
6
7
module Criteriable
 class DefaultCriterion < BaseCriterion
   def apply(scope)
     scope.where(name => value)
   end
 end
end

The search engine doesn’t do anyhing complicated. It gets criteria available for the model, checks whether there was given a necessary parameter and applies a criterion to the scope of the search.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
module Criteriable
 class Engine
   attr_reader :params

   def initialize(base, params = {})
     @base, @params = base, params.to_hash.symbolize_keys!
   end

   def search
     scope = @base.all

     criteria.each do |criterion|
       next if criterion.present?

       result = criterion.apply(scope)
       scope = result if result
     end

     scope
   end

   private

   def criteria
     @criteria ||= @base.criteria.map { |definition| definition.build(params) }
   end
 end
end

This example and library prove that search can be simple and tidy.