Searching... ? Sort your mess

By Tomasz Kowalewski, 3 Nov 2015

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.

About the author

Tomasz Kowalewski — Mulitasking Astronaut

Tomasz is a traditionalist who loves simplicity. This nice amiable guy never complains and is always helpful - and the high skill he owns makes it a lot to share! His calm, kind attitude and caring nature makes peace in the office, but nobody ever knows whether he is making a joke or talking seriously.

comments powered by Disqus