How to Build an eCommerce API With Ruby on Rails 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 need 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 to help you avoid unnecessary difficulties when creating an e-commerce platform.

The first task 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. In this example we'll add 1,000. The faker gem will help us do it. You can find the description of it here: https://github.com/stympy/faker.

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 thing 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 1,002 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 are returned 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: https://github.com/amatsuda/kaminari. 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: http://www.postgresql.org/docs/8.3/static/textsearch.html.

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: https://github.com/Casecommons/pg_search. 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 Ruby on Rails professional developer.
Keep training and don’t forget to follow next parts of our tutorial! Our team believes in you!
 
Links:
Ruby on Rails E-commerce API on GitHub

This part of the series has been republished with permission from MLSDev. The original article can be seen here.
 

Yuriy Blokhin I am a Ruby on Rails Developer at MLSDev Inc. - one of the Top European IT companies specializing in mobile and web apps development, UI/UX and consulting.

Comments

Comments(2)

saadbinakhlaq

before { expect(subject).to receive(:params).and_return(params) } line should be 

before { expect(subject).to receive(:params).and_return(:params) }

wsantos

Thanks for pointing that out, we've fixed the line!