Rails: Search Filtering

779 Views Asked by At

Please, I need some help for a digital library project that I working on. I want to add some search filtering functionality to the application.

The application primarily has books that belong to sub-categories, and sub-categories that belong to categories.

I want to create a search filtering function where you can filter books based on their categories when you search for a particular book.

So first, you create categories, and then next you create subcategories and select the categories that these subcategories fall under, and then you create books and only select the subcategories that each book falls under, which automatically places that book in the category that that subcategory falls under.

For now, the search filters only work if I add categories to the books new form. That is if I modify the books new form, and then include category selection to the form. But I just want to include only subcategories to the books new form, since each subcategory belongs to a category already.

The codes are as follows

Book Model

class Book < ApplicationRecord
  belongs_to :category, required: false
  belongs_to :subcategory, required: false

  def self.search(keywords)
    if keywords
      where('name LIKE ? OR description LIKE ? OR author LIKE ? OR abstract LIKE ?', "%#{keywords}%", "%#{keywords}%", "%#{keywords}%", "%#{keywords}%").order('id DESC')
    else
      order('id DESC')
    end
  end
end

Sub-Category Model

class Subcategory < ApplicationRecord
  belongs_to :category
  has_many :books
end

Category Model

class Category < ApplicationRecord
  has_many :subcategories
  has_many :books
end

Books Controller (Truncated)

class BooksController < ApplicationController
  before_action :set_book, only: [:show, :edit, :update, :destroy]

  def index
    if params[:category].blank? or params[:category][:id].blank? 
      @books_view = Book.all
    else  
      @books_view = Category.find(params[:category][:id]).books
    end
    @books = @books_view.search(params[:keywords]).paginate(page: params[:page], per_page: 12)
  end
end

Books Index Page Seach Form

<%= form_tag(books_path, method: :get) do %>
      <%= text_field_tag :keywords, params[:keywords], {placeholder: 'eg: Nursing', :class => 'searchForm'} %>
      <%= collection_select :category, :id, Category.all.order('name ASC'), :id, :name,{include_blank: 'Select Category'}, { :class => 'form-control'} %>
    <button type="submit">
      Search
      <%= image_tag("searchIcon.svg", :alt => "search", :class => "") %>
    </button>
  <% end %>

Please I need some assistance. It will be highly appreciated. Thank you.

Update

I have tried the solution from Dileep Nandanam, by updating by Books Controller this way

class BooksController < ApplicationController
    before_action :set_book, only: [:show, :edit, :update, :destroy]

    def index
        if params[:category].blank? or params[:category][:id].blank? 
            @books_view = Book.all
        else  
            @books_view = Book.joins(:subcatagory).joins('catagories on catagories.id = subcatagories.catagory_id').where(catagories: {id: params[:catagory][:id]})
        end
        @books = @books_view.search(params[:keywords]).paginate(page: params[:page], per_page: 12)
    end
end 

But it really didn't work as expected. I ran into some issues. Below is a screenshot of the issue.

![NoMethodError in BooksController#index undefined method `[]' for nil:NilClass]1

2

There are 2 best solutions below

2
Michael Kosyk On

I believe your problems come from this particular thing:

class Book < ApplicationRecord
  belongs_to :category, required: false # <= This is wrong
  belongs_to :subcategory, required: false
end

The answer to your particular question is:

<%= form_tag(books_path, method: :get) do %>
      <%= text_field_tag :keywords, params[:keywords], {placeholder: 'eg: Nursing', :class => 'searchForm'} %>
      <%= collection_select :subcategory, :id, Subcategory.all.order('name ASC'), :id, :name,{include_blank: 'Select Category'}, { :class => 'form-control'} %>
    <button type="submit">
      Search
      <%= image_tag("searchIcon.svg", :alt => "search", :class => "") %>
    </button>
  <% end %>

But let's elaborate on the answer:

Look, there is a big problem in your abstraction. If a Book belongs both to Subcategory (with a subcategory_id) and to Category (with a category_id), you really don't have a coherent data structure. If you use a relational database, you are violating db normalisation. What really should happen here is to split Category from Book and reference it through Subcategory, like this:

class Book < ApplicationRecord
  belongs_to :category, required: false, through: :subcategory
  belongs_to :subcategory, required: false
end
2
dileep nandanam On

Be strict about storing category_id in subcatagories and subcatagory_id in books, but not catagory_id in books.

We slightly modifies the index action this way

class BooksController < ApplicationController
    before_action :set_book, only: [:show, :edit, :update, :destroy]

    def index
        if params[:category].blank? or params[:category][:id].blank? 
            @books_view = Book.all
        else  
            @books_view = Book.joins(:subcatagory).joins('catagories on catagories.id = subcatagories.catagory_id').where(catagories: {id: params[:catagory][:id]})
        end
        @books = @books_view.search(params[:keywords]).paginate(page: params[:page], per_page: 12)
    end
end

and the search method becomes

def self.search(keywords)
    if keywords
        where('books.name LIKE ? OR book.description LIKE ? OR book.author LIKE ? OR book.abstract LIKE ?', "%#{keywords}%", "%#{keywords}%", "%#{keywords}%", "%#{keywords}%").order('books.id DESC')
    else
        order('books.id DESC')
    end
end