Over a million developers have joined DZone.

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

For any beginners looking to develop e-commerce applications, check out this multi-part ruby tutorial and learn how.

· Web Dev Zone

Start coding today to experience the powerful engine that drives data application’s development, brought to you in partnership with Qlik.

For those returning, I hope you liked the first part of our tutorial and it was easy enough to follow. If you're new, please use the link above to check out part 1. 

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

What if you need the capabilities 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 so you can avoid any unnecessary difficulties when creating your e-commerce platform.

So, the first task assigned for today will show you how to reveal a user's products, autocompleting product names with the first 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 as 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. When run, the .search_by method 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, for example 1000. 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. 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 information about all of the products' names 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 digest. 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 can 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 which 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 you 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 this 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 is accomplished. 

I'm sure you 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 more of our tutorials! The MLSDev team believes in you!
 

Links:

Ruby on Rails E-commerce API on GitHub

MLSDev Blog: 

Ruby on Rails E-commerce API for Beginners — Part 1

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

Ruby on Rails E-commerce API for Beginners — Part 3

Create data driven applications in Qlik’s free and easy to use coding environment, brought to you in partnership with Qlik.

Topics:
ruby ,api ,ruby on rails ,rails web development ,web dev ,beginner

Published at DZone with permission of YuriyBlokhin. See the original article here.

Opinions expressed by DZone contributors are their own.

The best of DZone straight to your inbox.

SEE AN EXAMPLE
Please provide a valid email address.

Thanks for subscribing!

Awesome! Check your inbox to verify your email so you can start receiving the latest in tech news and resources.
Subscribe

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}