How can I add another search value in the scope and add it into the terms.map
and pass it on to the where query, I want to have min and a max value?
scope :search_query, lambda { |query|
return nil if query.blank?
# condition query, parse into individual keywords
terms = query.downcase.split(/\s+/)
# replace "*" with "%" for wildcard searches,
# append '%', remove duplicate '%'s
terms = terms.map { |e|
(e.gsub('*', '%') + '%').gsub(/%+/, '%')
}
num_or_conds = 2
where(
terms.map { |term|
"(LOWER(students.first_name) LIKE ? OR LOWER(students.last_name) LIKE ?)"
}.join(' AND '),
*terms.map { |e| [e] * num_or_conds }.flatten
)
}
What i would like to do
.where(column_name BETWEEN #{value1} AND #{value2})
You can chain scopes in rails by just calling where repeatedly:
Thing.where(a: 1).where(b: 2)
# SELECT things.* FROM things WHERE things.a = ? AND things.b = ?
You can also use .merge to merge scopes:
Thing.where(a: 1).merge(Thing.where(b: 2))
Use a range create a BETWEEN query:
Thing.where(foo: (1..10))
# SELECT things.* FROM things WHERE foo BETWEEN 1 AND 10
This also works for dates and times.
Another thing to bear in mind in that scope is just syntactic sugar for class methods. So if your method does not fit in a one-liner you should use the the "classical" method definition:
class Student < ApplicationRecord
def self.search_query(query)
scope = self.all
terms = query.downcase.split(/\s+/)
terms = terms.map { |e|
(e.gsub('*', '%') + '%').gsub(/%+/, '%')
}
self.all.tap do |scope|
terms.each do |term|
scope.merge(
self.where("(LOWER(students.first_name) LIKE :t OR LOWER(students.last_name) LIKE :t)", t: term)
)
end
end
end
end
Following were my output from the query:
unless #slug == 'all'
#city = '' if #city.blank?
#city = #city.gsub("-", " ")
country = Country.where("lower(name) LIKE ?", "#{#city.downcase}")
gon.country = country
if country.present?
end
if #city.present?
#products = #products.where("lower(city) LIKE ? or lower(country) like ? or lower(state) LIKE ?", "%#{#city.downcase}%", "%#{#city.downcase}%","%#{#city.downcase}%")
#city_obj = City.where("lower(name) LIKE ?", "%#{#city.downcase}%").first
end
end
Here gon.country return result as:
Object
countries
:
Array[2]
0
:
"california"
1
:
"san francisco"
How can I iterate the countries and pass it to get #products result?
You can do it in two ways :
Iterate all country record and then add like query.
#product = []
country.each do |con|
#products << #products.where("lower(city) LIKE ? or lower(country) like ? or lower(state) LIKE ?", "%#{con.downcase}%", "%#{con.downcase}%","%#{con.downcase}%")
end
You can use ILIKE syntax of core sql. Which work like this :
select * from table where value ilike any (array['%foo%', '%bar%', '%baz%']);
# in your case
country_array_with_percent = country.map {|con| "%#{con}%" }
#products.where("name ILIKE ANY ( array[?] )", country_array_with_percent)
You can also make query chaining with or query, More details here
I'm wondering what the best way to parse a text query in Rails is, to allow the user to include logical operators?
I'd like the user to be able to enter either of these, or some equivalent:
# searching partial text in emails, just for example
# query A
"jon AND gmail" #=> ["jonsmith#gmail.com"]
# query B
"jon OR gmail" #=> ["jonsmith#gmail.com", "sarahcalaway#gmail.com"]
# query C
"jon AND gmail AND smith" #=> ["jonsmith#gmail.com"]
Ideally, we could get even more complex with parentheses to indicate order of operations, but that's not a requirement.
Is there a gem or a pattern that supports this?
This is a possible but inefficient way to do this:
user_input = "jon myers AND gmail AND smith OR goldberg OR MOORE"
terms = user_input.split(/(.+?)((?: and | or ))/i).reject(&:empty?)
# => ["jon myers", " AND ", "gmail", " AND ", "smith", " OR ", "goldberg", " OR ", "MOORE"]
pairs = terms.each_slice(2).map { |text, op| ["column LIKE ? #{op} ", "%#{text}%"] }
# => [["column LIKE ? AND ", "%jon myers%"], ["column LIKE ? AND ", "%gmail%"], ["column LIKE ? OR ", "%smith%"], ["column LIKE ? OR ", "%goldberg%"], ["column LIKE ? ", "%MOORE%"]]
query = pairs.reduce([""]) { |acc, terms| acc[0] += terms[0]; acc << terms[1] }
# => ["column LIKE ? AND column LIKE ? AND column LIKE ? OR column LIKE ? OR column LIKE ? ", "%jon myers%", "%gmail%", "%smith%", "%goldberg%", "%MOORE%"]
Model.where(query[0], *query[1..-1]).to_sql
# => SELECT "courses".* FROM "courses" WHERE (column LIKE '%jon myers%' AND column LIKE '%gmail%' AND column LIKE '%smith%' OR column LIKE '%goldberg%' OR column LIKE '%MOORE%' )
However, as I said, searches like this one are extremely inefficient. I'd recommend you use a full-text search engine, like Elasticsearch.
I use such a parser in a Sinatra app, since the queries tend to be complex I produce plain SQL instead of using the activerecords selection methods.
If you can use it, feel free..
You use it like this, class_name is the activerecord class representing the table, params is a hash of strings to parse, the result is sent to the browser as Json
eg
generic_data_getter (Person, {age: ">30",name: "=John", date: ">=1/1/2014 <1/1/2015"})
def generic_data_getter (class_name, params, start=0, limit=300, sort='id', dir='ASC')
selection = build_selection(class_name, params)
data = class_name.where(selection).offset(start).limit(limit).order("#{sort} #{dir}")
{:success => true, :totalCount => data.except(:offset, :limit, :order).count, :result => data.as_json}
end
def build_selection class_name, params
field_names = class_name.column_names
selection = []
params.each do |k,v|
if field_names.include? k
type_of_field = class_name.columns_hash[k].type.to_s
case
when (['leeg','empty','nil','null'].include? v.downcase) then selection << "#{k} is null"
when (['niet leeg','not empty','!nil','not null'].include? v.downcase) then selection << "#{k} is not null"
when type_of_field == 'string' then
selection << string_selector(k, v)
when type_of_field == 'integer' then
selection << integer_selector(k, v)
when type_of_field == 'date' then
selection << date_selector(k, v)
end
end
end
selection.join(' and ')
end
def string_selector(k, v)
case
when v[/\|/]
v.scan(/([^\|]+)(\|)([^\|]+)/).map {|p| "lower(#{k}) LIKE '%#{p.first.downcase}%' or lower(#{k}) LIKE '%#{p.last.downcase}%'"}
when v[/[<>=]/]
v.scan(/(<=?|>=?|=)([^<>=]+)/).map { |part| "#{k} #{part.first} '#{part.last.strip}'"}
else
"lower(#{k}) LIKE '%#{v.downcase}%'"
end
end
def integer_selector(k, v)
case
when v[/\||,/]
v.scan(/([^\|]+)([\|,])([^\|]+)/).map {|p|p p; "#{k} IN (#{p.first}, #{p.last})"}
when v[/\-/]
v.scan(/([^-]+)([\-])([^-]+)/).map {|p|p p; "#{k} BETWEEN #{p.first} and #{p.last}"}
when v[/[<>=]/]
v.scan(/(<=?|>=?|=)([^<>=]+)/).map { |part| p part; "#{k} #{part.first} #{part.last}"}
else
"#{k} = #{v}"
end
end
def date_selector(k, v)
eurodate = /^(\d{1,2})[-\/](\d{1,2})[-\/](\d{1,4})$/
case
when v[/\|/]
v.scan(/([^\|]+)([\|])([^\|]+)/).map {|p|p p; "#{k} IN (DATE('#{p.first.gsub(eurodate,'\3-\2-\1')}'), DATE('#{p.last.gsub(eurodate,'\3-\2-\1')}'))"}
when v[/\-/]
v.scan(/([^-]+)([\-])([^-]+)/).map {|p|p p; "#{k} BETWEEN DATE('#{p.first.gsub(eurodate,'\3-\2-\1')}')' and DATE('#{p.last.gsub(eurodate,'\3-\2-\1')}')"}
when v[/<|>|=/]
parts = v.scan(/(<=?|>=?|=)(\d{1,2}[\/-]\d{1,2}[\/-]\d{2,4})/)
selection = parts.map do |part|
operator = part.first ||= "="
date = Date.parse(part.last.gsub(eurodate,'\3-\2-\1'))
"#{k} #{operator} DATE('#{date}')"
end
when v[/^(\d{1,2})[-\/](\d{1,4})$/]
"#{k} >= DATE('#{$2}-#{$1}-01') and #{k} <= DATE('#{$2}-#{$1}-31')"
else
date = Date.parse(v.gsub(eurodate,'\3-\2-\1'))
"#{k} = DATE('#{date}')"
end
end
The simplest case would be extract an array from the strings:
and_array = "jon AND gmail".split("AND").map{|e| e.strip}
# ["jon", "gmail"]
or_array = "jon OR sarah".split("OR").map{|e| e.strip}
# ["jon", "sarah"]
Then you could construct an query string:
query_string = ""
and_array.each {|e| query_string += "%e%"}
# "%jon%%gmail%"
Then you use a ilike or a like query to fetch the results:
Model.where("column ILIKE ?", query_string)
# SELECT * FROM model WHERE column ILIKE '%jon%%gmail%'
# Results: jonsmith#gmail.com
Of course that could be a little overkill. But it is a simple solution.
Actually i have this search query from MrJoshi here is the associated question:
Search query for (name or forename) and (name forname) and (forname name)
def self.search(query)
return where('FALSE') if query.blank?
conditions = []
search_columns = [ :forname, :name ]
query.split(' ').each do |word|
search_columns.each do |column|
conditions << " lower(#{column}) LIKE lower(#{sanitize("%#{word}%")}) "
end
end
conditions = conditions.join('OR')
self.where(conditions)
end
The problem with this search query is that it returns way to much records. For example if somebody is searching for John Smith this search query returns all records wih the forename John and all records with the name Smith although there is only one person that exactly matches the search query means name is Smith and forename is John
So i changed the code a little bit:
def self.search(query)
return where('FALSE') if query.blank?
conditions = []
query2 = query.split(' ')
if query2.length == 2
conditions << " lower(:forname) AND lower(:name) LIKE ?', lower(#{sanitize("%#{query2.first}%")}) , lower(#{sanitize("%#{query2.last}%")})"
conditions << " lower(:forname) AND lower(:name) LIKE ?', lower(#{sanitize("%#{query2.last}%")}) , lower(#{sanitize("%#{query2.first}%")})"
else
search_columns = [ :forname, :name ]
query2.each do |word|
search_columns.each do |column|
conditions << " lower(#{column}) LIKE lower(#{sanitize("%#{word}%")}) "
end
end
end
conditions = conditions.join('OR')
self.where(conditions)
end
But now i get this error:
SQLite3::SQLException: near "', lower('": syntax error: SELECT "patients".* FROM "patients" WHERE ( lower(:forname) AND lower(:name) LIKE ?', lower('%John%') , lower('%Smith%')OR lower(:forname) AND lower(:name) LIKE ?', lower('%Smith%') , lower('%John%')) LIMIT 12 OFFSET 0
What did i wrong? Thanks!
I'm developing with Rails 2.3.8 and looking for a better way to build find conditions.
On search page, like user search, which user sets search conditions, find conditions are depends on the condition which user have chosen, e.g age, country, zip-code.
I've wrote code below to set multiple find conditions.
# Add condition if params post.
conditions_array = []
conditions_array << ['age > ?', params[:age_over]] if params[:age_over].present?
conditions_array << ['country = ?', params[:country]] if params[:country].present?
conditions_array << ['zip_code = ?', params[:zip_code]] if params[:zip_code].present?
# Build condition
i = 0
conditions = Array.new
columns = ''
conditions_array.each do |key, val|
key = " AND #{key}" if i > 0
columns += key
item_master_conditions[i] = val
i += 1
end
conditions.unshift(columns)
# condiitons => ['age > ? AND country = ? AND zip_code = ?', params[:age], params[country], prams[:zip_code]]
#users = User.find(:all,
:conditions => conditions
)
This code works fine but it is ugly and not smart.
Is there better way to build find conditions?
Named scopes could make it a bit more readable, albeit bulkier, while still preventing SQL injection.
named_scope :age_over, lambda { |age|
if !age.blank?
{ :conditions => ['age > ?', age] }
else
{}
end
}
named_scope :country, lambda { |country|
if !country.blank?
{ :conditions => ['country = ?', age] }
else
{}
end
}
named_scope :zip_code, lambda { |zip_code|
if !zip_code.blank?
{ :conditions => ['zip_code = ?', age] }
else
{}
end
}
And then when you do your search, you can simply chain them together:
#user = User.age_over(params[:age_over]).country(params[:country]).zip_code(params[:zip_code])
I have accidentally run on your questions, and even it is old one, here is the answer:
After defining your conditions, you could use it like this:
# Add condition if params post.
conditions_array = []
conditions_array << ["age > #{params[:age_over]}"] if params[:age_over].present?
conditions_array << ["country = #{params[:country]}"] if params[:country].present?
conditions_array << ["zip_code = #{params[:zip_code]}"] if params[:zip_code].present?
conditions = conditions_array.join(" AND ")
#users = User.find(:all, :conditions => conditions) #Rails 2.3.8
#users = User.where(conditions) #Rails 3+