Ruby on Rails E-commerce API for Beginners. Part 2.

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 |
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 |
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 |
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 |
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 |
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.