Rails comma separated search with scopes - ruby-on-rails

I would like my search to allow for multiple inputs. I have a scope in my model:
scope :by_description, lambda { |description| where('description LIKE ?', "%#{description}%") unless description.nil? }
Currently if I search for "abc, efg", it will look for that exact string. How can I modify my scope to allow "abc, efg" to search for any records that have either "abc" OR "efg" in the description field?

Something like this (based on other answers):
scope :by_description, ->(desc=nil) {
if desc.blank?
all
else
terms = desc.split(/\s*,\s*/).map { |t| t.strip }.map { |t| "%#{t}%" }
where( ( ["#{table_name}.description like ?"] * terms.count).join(' or '), *terms )
end
}

Try this:
scope :by_description, lambda { |description|
description.split(",").
map(&:strip).
inject(self) { |memo, term|
memo.where("description like ?", term)
}
unless description.nil?
}
What this does:
takes the incoming string and splits it into an array of comma-separated strings
calls strip on each string to remove leading and trailing spaces
uses inject to chain together a collection of where clauses, one for each search term in the array
I haven't actually tried so it may require some adjustment. But the approach is sound. The key bit is that you need multiple WHERE clauses to get the "and" behavior you're looking for.

This is based off Todd's answer:
scope :by_description, ->(desc=nil) {
if desc.present?
desc.split(",").map(&:strip).inject(self) do |memo, term|
memo.where("#{table_name}.description LIKE ?", "%#{term}%")
end
else
all # or none, if you want no results returned
end
}
Logic wise essentially the same, but I used the stabby syntax, and ensured that the scope would always return something chainable. I also like to be safe and add the table_name to manually built queries, to prevent ambiguous table errors (when 2 or more tables have a description field).

Related

Exact Term Search In Rails

I'm trying to build a basic search where only the entire exact search term shows results. Currently it is showing results based on individual words.
Here's the code from the model:
def search
find(:all, :conditions => ['term' == "%#{search}%"])
end
Sorry in advance. I'm very new to rails!
Thank you.
Remove the % from "%#{search}%" so it's "#{search}".
% is a wildcard that matches every result containing the word. So "%tea%" for example would match tear, nestea, and steam, when that's not what you want.
This should yield an exact match:
def search
find(:all, :conditions => ['term' == "#{search}"])
end
Your code doesn't work for several reasons.
You do not pass any value to that method. Therefore search will always be nil.
The ['term' == "%#{search}%"] condition doesn't make much sense because - as I said before - search is undefined and therefore the condition will is the same as ['term' == "%%"]. The string term is not equal to %% therefore the whole condition is basically: [false].
Rails 5.0 uses a different syntax for queries. The syntax you used is very old and doesn't work anymore.
I would do something like this:
# in your model
scope :search, -> (q) {
q.present? ? where("column_name LIKE :query", query: "%#{q}%") :none
}
# in your controller
def set_index
#b = Best.search(params[:search]).order(:cached_weighted_score => :desc)
end

rails dynamic where sql query

I have an object with a bunch of attributes that represent searchable model attributes, and I would like to dynamically create an sql query using only the attributes that are set. I created the method below, but I believe it is susceptible to sql injection attacks. I did some research and read over the rails active record query interface guide, but it seems like the where condition always needs a statically defined string as the first parameter. I also tried to find a way to sanitize the sql string produced by my method, but it doesn't seem like there is a good way to do that either.
How can I do this better? Should I use a where condition or just somehow sanitize this sql string? Thanks.
def query_string
to_return = ""
self.instance_values.symbolize_keys.each do |attr_name, attr_value|
if defined?(attr_value) and !attr_value.blank?
to_return << "#{attr_name} LIKE '%#{attr_value}%' and "
end
end
to_return.chomp(" and ")
end
Your approach is a little off as you're trying to solve the wrong problem. You're trying to build a string to hand to ActiveRecord so that it can build a query when you should simply be trying to build a query.
When you say something like:
Model.where('a and b')
that's the same as saying:
Model.where('a').where('b')
and you can say:
Model.where('c like ?', pattern)
instead of:
Model.where("c like '#{pattern}'")
Combining those two ideas with your self.instance_values you could get something like:
def query
self.instance_values.select { |_, v| v.present? }.inject(YourModel) do |q, (name, value)|
q.where("#{name} like ?", "%#{value}%")
end
end
or even:
def query
empties = ->(_, v) { v.blank? }
add_to_query = ->(q, (n, v)) { q.where("#{n} like ?", "%#{v}%") }
instance_values.reject(&empties)
.inject(YourModel, &add_to_query)
end
Those assume that you've properly whitelisted all your instance variables. If you haven't then you should.

Ruby - less expensive way for chained ActiveRecord queries

What I have is currently working, but seems to be very expensive, any ideas on making it less expensive would be great!
A User has many Plans, which has many PlanDates. Each PlanDates has a certain recipe denoted by a recipe_id attribute. Each Plan has a meal_type attribute which is either Meat, Vegetarian, or Choice, the latter means mixed. Each Recipe has a type_of_meal attribute that is either Meat or Vegetarian. Each Recipe also has a friendly name attribute.
For a given PlanDate, I need to build an options_for_select in the following format:
[ [recipe_id, "recipe_name"], [recipe_id, "recipe_name"] ... ]
The options:
must remove all the recipe_ids that have previously been given to the User (regardless of Plan)
must remove all the recipe_ids with a type mismatch (i.e., if a Plan has Meat designated, the options must not have any Vegetarian recipe_ids), certainly this is not true if the Plan has Choice designated
Here's the code I currently have:
# builds an array of all the recipe_ids that have been given to this User on some PlanDate on some Plan
recipes_used_before_for_this_user = PlanDate.select { |pd| pd.plan.user.id == user_id }.map { |pd| pd.recipe_id }
# narrows down the world of recipes to those that do NOT have an id of a recipe_used_before_for_this_user
recipes_not_used_before = Recipe.select { |r| (recipes_used_before_for_this_user.include? r.id) == false }
# going forward, let's assume current_pd = the PlanDate object in question
if current_pd.plan.meal_type == "Choice"
# easiest: if the meal_type is "Choice" then we just take the recipes_not_used_before and map them into the appropriate format
recipe_choices_array = recipes_not_used_before.map { |r| [ r.id, r.name ] }
else
# if the plan has a "Meat" or "Vegetarian" specification, we need to first narrow the recipes_not_used_before down by the right type and then map into the appropriate format
recipe_choices_array = recipes_not_used_before.select { |r| r.type_of_meal == potential_pd.first.plan.meal_type }.map { |r| [ r.id, r.name ] }
end
Again, working, but I have a lot of PlanDates and a lot of Recipes, so if there is any way to streamline even further, would love your ideas. Thanks!
The reason you're experiencing expensive queries is because you're not actually using ActiveRecord's query interface, or even SQL to narrow your query, but instead are loading the entire dataset into Ruby memory objects and then looping over the result in Ruby.
I suspect that if you inspect your logfiles you'll see something like this:
>> PlanDate.select{ |pd| pd.plan.user.id == user_id }.map { |pd| pd.recipe_id }
PlanDate Load (1.3ms) SELECT "plan_dates".* FROM "plan_dates"
=> [#<PlanDate....
What you want to do is to use ActiveRecord's query interface to build the query, something like this:
PlanDate.includes(plan: [:user]).where("plan.user_id == ?", :user_id).pluck('recipe_id')
What that does is first: Specify relationships to be included in the result set, then specify the where conditions of your SQL query, and finally pull out the recipe ids using pluck.
See http://guides.rubyonrails.org/active_record_querying.html for more info.

Rails OR query with postgres array

I have a tagging system in rails using postgres' array data type. I'm trying to write a scope that will return any posts that include a tag. So far I have this scope working:
scope :with_tag, ->(tag) { where("tags #> ARRAY[?]", tag) }
I want to extend this scope so that I can query on multiple tags at the same time, ideally something like:
Post.with_tags(['some', 'tags', 'to', 'query'])
Which would return any Post that have one of those tags. I've thought about making a class method to handle iterating over the input array:
def self.with_tags(args)
# start with empty activerecord relation
# want to output AR relation
results = Post.none
args.each do |tag|
results = results.concat(Post.with_tag(tag))
end
results.flatten
end
but this approach smells funny to me because it's creating a new query for each argument. It also doesn't return an ActiveRecord::Relation because of flatten, which I would really like to have as the output.
Can I accomplish what I'm after in a scope with an OR query?
I'm not running the code but I think the && operator does what you want:
scope :with_tags, ->(tags) { where("tags && ARRAY[?]", tags) }

Rails find with a block

I have seen Rails find method taking a block as
Consumer.find do |c|
c.id == 3
end
Which is similar to Consumer.find(3).
What are some of the use cases where we can actually use block for a find ?
It's a shortcut for .to_a.find { ... }. Here's the method's source code:
def find(*args)
if block_given?
to_a.find(*args) { |*block_args| yield(*block_args) }
else
find_with_ids(*args)
end
end
If you pass a block, it calls .to_a (loading all records) and invokes Enumerable#find on the array.
In other words, it allows you to use Enumerable#find on a ActiveRecord::Relation. This can be useful if your condition can't be expressed or evaluated in SQL, e.g. querying serialized attributes:
Consumer.find { |c| c.preferences[:foo] == :bar }
To avoid confusion, I'd prefer the more explicit version, though:
Consumer.all.to_a.find { |c| c.preferences[:foo] == :bar }
The result may be similar, but the SQL query is not similar to Consumer.find(3)
It is fetching all the consumers and then filtering based on the block. I cant think of a use case where this might be useful
Here is a sample query in the console
consumer = Consumer.find {|c|c.id == 2}
# Consumer Load (0.3ms) SELECT `consumers`.* FROM `consumers`
# => #<Consumer id: 2, name: "xyz", ..>
A good example of a use-case is if you have a JSON/JSONB column and don't want to get involved in the more complex JSON SQL.
required_item = item_collection.find do |item|
item.jsondata['json_array_property'][index]['property'] == clause
end
This is useful if you can constrain the scope of the item_collection to a date-range, for example, and have a smaller set of items that require filtering further.

Resources