How to do a LIKE query in Arel and Rails? - ruby-on-rails

I want to do something like:
SELECT * FROM USER WHERE NAME LIKE '%Smith%';
My attempt in Arel:
# params[:query] = 'Smith'
User.where("name like '%?%'", params[:query]).to_sql
However, this becomes:
SELECT * FROM USER WHERE NAME LIKE '%'Smith'%';
Arel wraps the query string 'Smith' correctly, but because this is a LIKE statement it doesnt work.
How does one do a LIKE query in Arel?
P.S. Bonus--I am actually trying to scan two fields on the table, both name and description, to see if there are any matches to the query. How would that work?

This is how you perform a like query in arel:
users = User.arel_table
User.where(users[:name].matches("%#{user_name}%"))
PS:
users = User.arel_table
query_string = "%#{params[query]}%"
param_matches_string = ->(param){
users[param].matches(query_string)
}
User.where(param_matches_string.(:name)\
.or(param_matches_string.(:description)))

Try
User.where("name like ?", "%#{params[:query]}%").to_sql
PS.
q = "%#{params[:query]}%"
User.where("name like ? or description like ?", q, q).to_sql
Aaand it's been a long time but #cgg5207 added a modification (mostly useful if you're going to search long-named or multiple long-named parameters or you're too lazy to type)
q = "%#{params[:query]}%"
User.where("name like :q or description like :q", :q => q).to_sql
or
User.where("name like :q or description like :q", :q => "%#{params[:query]}%").to_sql

Reuben Mallaby's answer can be shortened further to use parameter bindings:
User.where("name like :kw or description like :kw", :kw=>"%#{params[:query]}%").to_sql

Don't forget escape user input.
You can use ActiveRecord::Base.sanitize_sql_like(w)
query = "%#{ActiveRecord::Base.sanitize_sql_like(params[:query])}%"
matcher = User.arel_table[:name].matches(query)
User.where(matcher)
You can simplify in models/user.rb
def self.name_like(word)
where(arel_table[:name].matches("%#{sanitize_sql_like(word)}%"))
end

Related

Search result is not displaying

I have an issue in searching records from the PostgreSQL with particular search keyword but no record is displaying here is the code
filter_text=params[:filter_search]
#outputs = Output.where("name LIKE '%#{filter_text}%'").order("name ASC")
Try this :
filter_text=params[:filter_search]
#outputs = Output.where("name LIKE ?","%#{filter_text}%").order("name ASC")
If you use ransack gem, it will allow you to use simple methods to search. Using ransack, you will only need to do this:
#outputs = Output.search(name_cont: params[:filter_search]).result.order("name ASC")
If you are going for case insensitive search go for ILIKE
filter_text = params[:filter_search]
#outputs = Output.where("name ILIKE ?", "'%#{filter_text}%'").order("name ASC")
Instead of:
filter_text=params[:filter_search]
#outputs = Output.where("name LIKE '%#{filter_text}%'").order("name ASC")
Try the following:
filter_text=params[:filter_search]
#outputs = Output.where(["name LIKE ?", "%#{filter_text}%"]).order("name ASC")
One easy way to search is to use Ransack. Which provides you an efficient search mechanism.

How to search using Rails?

I'm using the following code for now:
def self.search(query)
where("name like ?", "%#{query}%")
end
which works only for name, but basically I need it to work for everything (date of birth, surname, name + surname and so on). Can you please pass me some ideas how I could make this piece of code more suitable for my needs?
Thank you.
If you're using MySQL, then the dusen gem will do what you want.
In your model, add:
def self.search(cols, query)
tbl = self.arel_table
fq = tbl[cols.shift].matches("%#{query}%")
q = cols.inject(fq) do |acc, col|
acc.or(tbl[col].matches("%#{query}%"))
end
where(q)
end
And then you can use it like:
User.search([:name, :surname], 'name')
Which will generate nice SQL like:
SELECT "users".* FROM "users" WHERE (("users"."name" LIKE '%name%'
OR "users"."surname" LIKE '%name%'))
If you need finer control over your query, I sometimes add the following convenience method to AR:
def self.arel_where(&block); where(yield self.arel_table); end
And then you could do:
User.arel_where do |tbl|
name = '%name%'; surname = '%sur%'
tbl[:name].matches(name).and(tbl[:surname].matches(surname))
end

Rails activerecord LIKE AND clause error

Here is an activerecord query i'm trying to use in rails
q = "Manchester"
b = "John Smith"
Model.find(:all, :conditions => ["city ? AND name like ?", q, b])
but i get this error in rails console
ActiveRecord::StatementInvalid: SQLite3::SQLException: near "'Manchester'": syntax error: SELECT "model".* FROM "model" WHERE (city 'Manchester' AND name like 'John Smith')
Please help!
You missed LIKE for city.
Model.where('city LIKE ? AND name LIKE ?', "%#{q}%", "%#{b}%");
You can also use this syntax which is a lot more readable than trying to figure out which ? goes with which variable. I mean if you have 1 or 2 it's fine, but once you have more it gets pretty ugly.
Model.where("city LIKE :city AND name LIKE :name", { city: "%#{q}%", name: "%#{b}%" })
The placeholders and hash key can be anything you like as long as they match (don't use :city and then hamster: in the hash key for example).
The nice thing about this is that you can also use one variable for multiple searches:
where("user LIKE :term OR email LIKE :term OR friends LIKE :term", { term: "%#{params[:term]}%"})
Try this:
Model.find(:all, :conditions => ["city = ? AND name like ?", q, b])

Rails ActiveRecord - Search on Multiple Attributes

I'm implementing a simple search function that should check for a string in either the username, last_name and first_name. I've seen this ActiveRecord method on an old RailsCast:
http://railscasts.com/episodes/37-simple-search-form
find(:all, :conditions => ['name LIKE ?', "%#{search}%"])
But how do I make it so that it searches for the keyword in name, last_name and first name and returns the record if the one of the fields matched the term?
I'm also wondering if the code on the RailsCast is prone to SQL injections?
Thanks a lot!
I assumed your model name is Model - just replace it with your real model name when you do the actual query:
Model.where("name LIKE ? OR last_name LIKE ? OR first_name LIKE ?", "%#{search}%","%#{search}%","%#{search}%")
About your worries about SQL injections - both of code snippets are immune to SQL injections. As long as you do not directly embed strings into your WHERE clause you are fine. An example for injection-prone code would be:
Model.where("name LIKE '#{params[:name]}'")
Although the selected answer will work, I noticed that it breaks if you try to type a search "Raul Riera" because it will fail on both cases, because Raul Riera is not either my first name or my last name.. is my first and last name... I solved it by doing
Model.where("lower(first_name || ' ' || last_name) LIKE ?", "%#{search.downcase}%")
With Arel, you can avoid writing the SQL manually with something like this:
Model.where(
%i(name first_name last_name)
.map { |field| Model.arel_table[field].matches("%#{query}%")}
.inject(:or)
)
This would be particularly useful if the list of fields to match against was dynamic.
A more generic solution for searching in all fields of the model would be like this
def search_in_all_fields model, text
model.where(
model.column_names
.map {|field| "#{field} like '%#{text}%'" }
.join(" or ")
)
end
Or better as a scope in the model itself
class Model < ActiveRecord::Base
scope :search_in_all_fields, ->(text){
where(
column_names
.map {|field| "#{field} like '%#{text}%'" }
.join(" or ")
)
}
end
You would just need to call it like this
Model.search_in_all_fields "test"
Before you start.., no, sql injection would probably not work here but still better and shorter
class Model < ActiveRecord::Base
scope :search_all_fields, ->(text){
where("#{column_names.join(' || ')} like ?", "%#{text}%")
}
end
The best way to do this is:
Model.where("attr_a ILIKE :query OR attr_b ILIKE :query", query: "%#{query}%")

Search multiple columns - Rails

I am currently writing a search method for my rails applications, and at the moment it works fine. I have the following in my game.rb:
def self.search(search)
if search
find(:all, :conditions => ['game_name LIKE ? OR genre LIKE ? OR console LIKE ?', "%#{search}%", "#{search}", "#{search}"])
else
find(:all)
end
end
Now that searches fine, but my problem is that if there is a record in game_name that has the word 'playstation' in it, it will finish the search there. It only returns that record, rather than all games that have 'playstation' stored in console. Now I understand this is because I have 'OR' in my conditions, but I don't know an alternative. 'AND' requires all the conditions to match or none return at all. What is an alternative I can use to AND and OR? Help would be much appreciated.
If there is a solution that has separate search boxes and entries, then that would be fine, I don't necessarily require the search to find it all based on one search form.
If I understand your question correctly, your SQL looks good to me for what you are trying to do. An OR clause will return all records that match in column1, column2, or column3. It doesn't stop at the first match. I do see an issue with your parameters in that the first you are using LIKE with % but in the second two you aren't, maybe that is where your issue is coming from.
Should this be your find (% around second and third search)?
find(:all, :conditions => ['game_name LIKE ? OR genre LIKE ? OR console LIKE ?', "%#{search}%", "%#{search}%", "%#{search}%"])
or better use DRY version (above will not work for Rails 4.2+):
Item.where('game_name LIKE :search OR genre LIKE :search OR console LIKE :search', search: "%#{search}%")
What if you have 15 columns to search then you will repeat key 15 times. Instead of repeating key 15 times in query you can write like this:
key = "%#{search}%"
#items = Item.where('game_name LIKE :search OR genre LIKE :search OR console LIKE :search', search: key).order(:name)
It will give you same result.
Thanks
I think this is a little bit of a cleaner solution. This allows you to add/remove columns more easily.
key = "%#{search}%"
columns = %w{game_name genre console}
#items = Item.where(
columns
.map {|c| "#{c} like :search" }
.join(' OR '),
search: key
)
A more generic solution for searching in all fields of the model would be like this
def search_in_all_fields model, text
model.where(
model.column_names
.map {|field| "#{field} like '%#{text}%'" }
.join(" or ")
)
end
Or better as a scope in the model itself
class Model < ActiveRecord::Base
scope :search_in_all_fields, ->(text){
where(
column_names
.map {|field| "#{field} like '%#{text}%'" }
.join(" or ")
)
}
end
You would just need to call it like this
Model.search_in_all_fields "test"
Before you start.., no, sql injection would probably not work here but still better and shorter
class Model < ActiveRecord::Base
scope :search_all_fields, ->(text){
where("#{column_names.join(' || ')} like ?", "%#{text}%")
}
end
I think this is a more efficient solution if you want to search an array of columns as I do.
First and most importantly you can add a private function to your model that creates a query template:
def self.multiple_columns_like_query(array)
array.reduce('') { |memo, x| #
unless memo == '' #
memo += ' or ' # This is the
end #
memo += "#{x} like :q" # core part
} #
end
Than you can use the function in your search function:
def self.search(query)
if fields = self.searched_fields && query
where multiple_like_query(fields), q: "%#{query}%"
end
end
Here you should also define self.searched_fields as an array of field names.

Resources