Hello everyone! Hope you liked the first part of our tutorial and it didn’t cost you a great deal of time and effort to create products.

Are you ready to continue the development and follow the next part of our “recipe”? If so, we can move forward to the product search.

What if you will have to search products by first letters of their names (autocomplete) or by full words (Full Text Search)? We will review both options in this guide for you not to have unnecessary difficulties when creating an e-commerce platform.

So, the first task assigned for today is to show a user products, names of which start with the letters he/she enters in the search box. This could be implemented with the help of SQL operators LIKE and ILIKE (available in PostgreSQL). The difference between them is that ILIKE ignores the register, so we will use this very operator in the development.

Let’s write a class method for our model app/models/product.rb and name it .search_by.

class Product < ActiveRecord::Base
class << self
def search_by params = {}
params = params.try(:symbolize_keys) || {}
end
end
end

This method will take hash-parameters that come from the client’s side (entered by a user). If the keys of the hash (associative array) are in the form of lines, transfer them into symbols.

class Product < ActiveRecord::Base
class << self
def search_by params = {}
params = params.try(:symbolize_keys) || {}
collection = all
if params[:term].present?
collection = collection.where('name ILIKE ?', "#{ params[:term] }%")
end
collection
end
end
end

term is the first letter(s) or a phrase of a product name. If it is seen in if params[:term].present? and does not appear an empty line it means that we do the search.

The sign % at the end of #{{'{'}} params[:term] {{'}'}}% means that the phrase (term), according to which the search is done, is placed at the beginning of the product’s name.

Let’s make a brief summary of the above information. The .search_by method when run will return either search results if to pass the term parameter there or all products from the database.

Do not forget to write tests that will cover the .search_by method. Go to spec/models/product_spec.rb to do that.

require 'rails_helper'
RSpec.describe Product, type: :model do
describe '.search_by' do
let(:relation) { double }
before { expect(Product).to receive(:all).and_return(relation) }
context do
it { expect { Product.search_by }.to_not raise_error }
end
context do
before { expect(relation).to receive(:where).with('name ILIKE ?', 'abc%') }
it { expect { Product.search_by 'term' => 'abc' }.to_not raise_error }
end
end
end
view raw Write Tests hosted with ❤ by GitHub

The above test covers both options of the .search_by method performing. You should run it using the following command:

$ rake

Do you remember the controller app/controllers/api/products_controllers.rb we created last time? Let’s go there and correct the #collection method.

class Api::ProductsController < ApplicationController
private
def collection
@products ||= Product.search_by(params)
end
....
end

Don’t forget to correct the spec/controllers/api/products_controller_spec.rb tests afterwards too.

RSpec.describe Api::ProductsController, type: :controller do
....
describe '#collection' do
before { expect(subject).to receive(:params).and_return(:params) }
before { expect(Product).to receive(:search_by).with(params) }
it { expect { subject.send :collection }.to_not raise_error }
end
....
end
view raw Correct Tests hosted with ❤ by GitHub

Excellent! The search is ready. We need to check its performance with the curl command, but let’s add more products to the database first, 1000 for example. The faker gem will help us do it. You can find the description of it here.

Connect the gem to the development and test modes in Gemfile.

group :development, :test do
gem 'faker'
end

Don’t forget to run the command:

$ bundle install

To continue go to the db/seeds.rb file. It is used to fill the database with the initial information. Write the following there:

1000.times do
Product.create \
name: Faker::Commerce.product_name,
price: Faker::Number.between(1, 150),
description: Faker::Commerce.department
end

Then run the command in the console:

$ rake db:seed

It will be a bit time-consuming, but we are not in a hurry. The most important is the quality of development. Just wait and start the server after the process is finished.

$ rails server

Then enter the curl command in the console:

$ curl "http://localhost:3000/api/products" -H "Accept: application/json" -X GET -d "term=a"

If your try is successful you will see the information about all products names of which start with “a”.

The task is accomplished. We have 1002 products in the database at the moment but the request can return information about all of them together. This amount of data is too big for a person to learn it at a heat. So let’s do it in such a way that the products return in bundles of about 25 product names in each.

Let’s implement pagination. We will use the kaminari gem for that. You could find its description here.

Add it to Gemfile.

gem 'kaminari'

Run the following command in the console:

$ bundle install

We need to get back to the .search_by method and make some changes there:

def search_by params = {}
params = params.try(:symbolize_keys) || {}
collection = page(params[:page])
if params[:term].present?
collection = collection.where('name ILIKE ?', "#{ params[:term] }%")
end
collection
end

The .page method considers the number of a page a user wants to see as a parameter. By default, the number of elements per page is 25 and it means we will not need to change anything. Don’t forget to correct the tests.

describe '.search_by' do
let(:relation) { double }
before { expect(Product).to receive(:page).with(1).and_return(relation) }
context do
it { expect { Product.search_by 'page' => 1 }.to_not raise_error }
end
context do
before { expect(relation).to receive(:where).with('name ILIKE ?', 'abc%') }
it { expect { Product.search_by 'page' => 1, 'term' => 'abc' }.to_not raise_error }
end
end
view raw Correct the Tests hosted with ❤ by GitHub

Let’s check it with the curl command in the console:

$ curl "http://localhost:3000/api/products" -H "Accept: application/json" -X GET -d "page=1"

If your have followed the instructions correctly you will see the information about the first 25 products from the database on the screen.

Let’s move to the next task which is searching products by full words. It may seem quite easy to do but don’t forget that a word can be at the beginning, in the middle and at the end of a name. To accomplish this task we will use Full Text Search in PostgreSQL.

Full Text Search is an automated documentary search where instead of a document search image its full text or its significant text parts with a morphological vocabulary are used. You can learn more here.

Do you mind making this task more complicated? Hopefully, you don’t because we are going to search products not only by names but by their description as well. Moreover, results by name have to be displayed first and those by description should go next. So, we will sort them actually.

To do Full Text Search we need the pg_search gem. Its description can be found here.

Add it to Gemfile.

gem 'pg_search'

Then run the following command in the console:

$ bundle install

Go to the file app/models/product.rb and add there:

class Product < ActiveRecord::Base
include PgSearch
pg_search_scope :search,
against: {
name: :A,
description: :B
},
using: {
tsearch: { dictionary: :english }
}
....
end
view raw Add Code to File hosted with ❤ by GitHub

Let’s analyze and explain the code. There is nothing unusual in the line

include PgSearch

It connects the module necessary for the pg_search gem work. Then we create the .search method that will accept a line - a phrase or one word (search parameters). After that the search is specified by two fields: name and description. The importance of the name field has A priority and the importance of the description field has B priority. It is done so to make the results by name appear first. FTS has A, B,C and D priorities. A means the highest priority and D has the lowest one. Then the code states that we will use just FTS search from the three types available in this gem: Full text search, trigram - Trigram search, dmetaphone - Double Metaphone search. Finally, we specify that FTS should use the English dictionary.

Remember to correct the .search_by method:

def search_by params = {}
params = params.try(:symbolize_keys) || {}
collection = page(params[:page])
if params[:term].present?
collection = collection.where('name ILIKE ?', "#{ params[:term] }%")
end
if params[:name].present?
collection = collection.search(params[:name])
end
collection
end

If name returns in the parameters the method will do FTS search by name and description.

You should also keep in mind to correct the test spec/models/product_spec.rb:

RSpec.describe Product, type: :model do
it { should be_a PgSearch }
describe '.search_by' do
let(:relation) { double }
before { expect(Product).to receive(:page).with(1).and_return(relation) }
context do
it { expect { Product.search_by 'page' => 1 }.to_not raise_error }
end
context do
before { expect(relation).to receive(:where).with('name ILIKE ?', 'abc%') }
it { expect { Product.search_by 'page' => 1, 'term' => 'abc' }.to_not raise_error }
end
context do
before { expect(relation).to receive(:search).with('word') }
it { expect { Product.search_by 'page' => 1, 'name' => 'word' }.to_not raise_error }
end
end
end
view raw Correct the test hosted with ❤ by GitHub

Don’t forget about the command:

$ rake

Everything seems to be done so far, but we need to make sure it really works. The curl command can be of great help here:

$ curl "http://localhost:3000/api/products" -H "Accept: application/json" -X GET -d "name=apples&page=1"

Wonderful! Today’s mission looks like fully completed.

We are sure you have managed to do it. Your e-commerce API works at this stage and you are one more step closer to being a professional Ruby on Rails developer.

Keep training and don’t forget to follow next parts of our tutorial! Our team believes in you!

Looking for a professional team of back-end developers?

At MLSDev, we have talented and skilled back-end developers who are ready to deal with the most challenging tasks and create great products. Contact us to discuss your project.

Get in touch