I'm making a search page where I have a couple of filters on the side and I'm trying to integrate them with Searchkick to query products.
These are my scopes I'm using for the products
models/product.rb
scope :in_price_range, ->(range) { where("price <= ?", range.first) }
scope :in_ratings_range, -> (range) { where("average_rating >= ?", range.first) }
def self.with_all_categories(category_ids)
select(:id).distinct.
joins(:categories).
where("categories.id" => category_ids)
end
This is where I'm actually calling the scopes
controllers/search_controller.rb
#results = Product.search(#query)
#results = #results.with_all_categories(params[:category_ids]) if params[:category_ids].present?
#results = #results.in_price_range(params[:price]) if params[:price].present?
#results = #results.in_ratings_range(params[:rating]) if params[:rating].present?
After running it, I get an error saying the searchkick model doesn't have any methods with the name of my scope.
undefined method `with_all_categories' for #Searchkick::Results:0x00007f4521074c30>
How do I use scopes with my search query?
You can apply scopes to Searchkick results with:
Product.search "milk", scope_results: ->(r) { in_price_range(params[:price]) }
See "Run additional scopes on results" in the readme.
However, if you apply ActiveRecord where filters, it will throw off pagination. For pagination to work correctly, you need to use Searchkick's where option:
Product.search(query, where: {price_range: 10..20})
The error (unknown to me at the time of writing this answer) might be because you defined with_all_categories as a class method on Product, but in your controller you call it on #results which must be an ActiveRecord::Relation.
Turning it into a scope should fix the issue:
Change this:
def self.with_all_categories(category_ids)
select(:id).distinct.
joins(:categories).
where("categories.id" => category_ids)
end
to:
scope :with_all_categories, -> (category_ids) { select(:id).distinct.joins(:categories).where("categories.id" => category_ids) }
Related
I'd like to setup an advanced search of a Rails resource (i.e Product) where I can perform both positive and negative searches. For example:
Product is a phone
Product is not made by Apple
Product was made in the last 16 months
I can pass multiple parameters to a page but is there a way to chain queries?
#results = Product.where("lower(type) LIKE ?", "%#{search_term.downcase}%").where(....
I'd like to use a combination of where and where.not:
def search
word1 = params[:word_1]
word2 = params[:word_2]
if word1.starts_with?('not')
chain1 = where.not("lower(tags) LIKE ?", "%#{word1.downcase}%")
else
chain1 = where("lower(tags) LIKE ?", "%#{word1.downcase}%")
end
if word2.starts_with?('not')
chain2 = where.not("lower(tags) LIKE ?", "%#{word2.downcase}%")
else
chain2 = where("lower(tags) LIKE ?", "%#{word2.downcase}%")
end
#products = Product.chain1.chain2
end
but I get the following error:
undefined method where' for #ProductsController:0x0000000000ac58`
You can chain where like this
Product.
where(type: "phone").
where.not(factory: "Apple").
where(manufactered_at: 16.months.ago..)
Also rails 7 introduces invert_where (it inverts all condtions before it) so you can
Product.
where(factory: "Apple").invert_where.
where(type: "phone").
where(manufactered_at: 16.months.ago..)
You can use scopes
class Product < ApplicationRecord
scope :phone, -> where(type: "phone")
scope :apple, -> where(factory: "Apple")
scope :manufacatured_last, ->(period) { where(manufactered_at: period.ago..) }
end
Product.apple.invert_where.phone.manufacatured_last(16.months)
I'm creating a filtered table for my user model. I've created a few scopes to filter them. I'm :
class User < ApplicationRecord
has_many :invoices
scope :application_approved, -> { ... }
scope :application_denied, -> { ... }
scope :latest_invoice_paid, -> { ... }
scope :latest_invoice_not_paid, -> { ... }
def self.__self__
self
end
end
and in the controller:
def index
filters = params[:statuses] || {}
application_status = filters[:application_status].presence
payments_status = filters[:payments_status].presence
#vehicles = Vehicle.send(application_status || :__self__)
.send(payment_status || :__self__)
.paginate(:page => params[:page], :per_page => 10)
.order('created_at DESC')
end
All of the filters work in isolation, however when chained, filters that are not applied seem to cancel out the earlier filters.
For example, if I set the filter to only show users who have paid, it works. But if I set the filter to only show users who have been approved/unapproved, all users are returned all the time. It seems as though returning self when a filter is not applied just returns all of the users.
So, how can I skip the scope if a filter is not applied for it?
Something like this should do the trick, it also helps secure your send method. Since only whitelisted methods can be executed. The code below does the following:
First create a whitelist with the allowed keys and allowed values.
Get the params[:statuses] or if it doesn't exist create a new Parameters object.
Permit only the allowed keys.
Remove all key-value instances that don't have whitelisted values.
Convert the allowed parameters into a hash.
Reduce the resulting collection. Start with Vehicle.all and send the whitelisted methods (chaining them together). If a key or value isn't present, it won't be looped over so there is no need to call :__self__, or :itself.
Do the rest of your logic.
def index
whitelist = ActiveSupport::HashWithIndifferentAccess.new(
application_status: %w[application_approved application_denied],
payments_status: %w[latest_invoice_paid latest_invoice_not_paid],
)
filters = params[:statuses] || ActionController::Parameters.new
#vehicles =
filters
.permit(*whitelist.keys)
.select { |key, value| whitelist[key].include?(value) }
.to_h
.reduce(Vehicle.all) { |vehicles, (_key, value)| vehicles.send(value) }
.order(created_at: :desc)
.paginate(page: params[:page], per_page: 10)
end
References:
ActiveSupport::HashWithIndifferentAccess
ActionController::Parameters (permit, select and to_h can all be found here)
Enumerable#reduce
The splat operator * in .permit(*whitelist.keys)
I have the following code that I use in my search forms. I want to be able to chain the scoped method with the by_title, but I fail to see how. I want to have the by_title as a method instead of just doing:
# Arel helpers
class << self
def by_city(city)
where(['city_id = ?', city])
end
def by_title(title)
where('title LIKE ?', "%#{title}%")
end
end
def self.search(search_params)
experiences = scoped
experiences.self.by_title(search_params[:title]) if search_params[:title]
end
Why don't you play with scopes this way:
scope :by_title, lambda { |title| where('title LIKE ?', "%#{title}%") }
scope :by_city, lambda { |city| where('city_id = ?', city) }
By just removing self it should work I think:
experiences = scoped
experiences.by_title(search_params[:title]) if search_params[:title]
The scoped method returns an anonymous scope which can be chained with other scopes/class-methods.
In my Widget model I have the following:
scope :accessible_to, lambda { |user|
if user.has_role?('admin')
self.all
else
roles = user.roles
role_ids = []
roles.each { |r| role_ids << r.id }
self.joins(:widget_assignments).where('widget_assignments.role_id' => role_ids)
end
}
Ideally, I would like to use this scope as a filter for Ransack's search results, so in my controller I have:
def index
#q = Widget.accessible_to(current_user).search(params[:q])
#widgets = #q.result.order('created_at DESC')
end
Doing this generates the following error:
undefined method `search' for Array:0x007ff9b87a0300
I'm guessing that Ransack is looking for an ActiveRecord relation object and not an array. Is there anyway that I can use my scope as a filter for Ransack?
Change the self.all for self.scoped. all returns an array.
Update for Rails 4: all will now return a scope.
Trying to do a basic filter in rails 3 using the url params. I'd like to have a white list of params that can be filtered by, and return all the items that match. I've set up some scopes (with many more to come):
# in the model:
scope :budget_min, lambda {|min| where("budget > ?", min)}
scope :budget_max, lambda {|max| where("budget < ?", max)}
...but what's the best way to use some, none, or all of these scopes based on the present params[]? I've gotten this far, but it doesn't extend to multiple options. Looking for a sort of "chain if present" type operation.
#jobs = Job.all
#jobs = Job.budget_min(params[:budget_min]) if params[:budget_min]
I think you are close. Something like this won't extend to multiple options?
query = Job.scoped
query = query.budget_min(params[:budget_min]) if params[:budget_min]
query = query.budget_max(params[:budget_max]) if params[:budget_max]
#jobs = query.all
Generally, I'd prefer hand-made solutions but, for this kind of problem, a code base could become a mess very quickly. So I would go for a gem like meta_search.
One way would be to put your conditionals into the scopes:
scope :budget_max, lambda { |max| where("budget < ?", max) unless max.nil? }
That would still become rather cumbersome since you'd end up with:
Job.budget_min(params[:budget_min]).budget_max(params[:budget_max]) ...
A slightly different approach would be using something like the following inside your model (based on code from here:
class << self
def search(q)
whitelisted_params = {
:budget_max => "budget > ?",
:budget_min => "budget < ?"
}
whitelisted_params.keys.inject(scoped) do |combined_scope, param|
if q[param].nil?
combined_scope
else
combined_scope.where(whitelisted_params[param], q[param])
end
end
end
end
You can then use that method as follows and it should use the whitelisted filters if they're present in params:
MyModel.search(params)