PostgreSQL precendence when querying with ILIKE - ruby-on-rails

Two fixtures:
one:
firstname: John
lastname: Doe
user_id: 1
two:
firstname: Jane
lastname: Doe
user_id: 1
In my unit test, I have the following:
test "Searching for a contact gets the right person" do
search = Contact.search("John")
assert_equal 1, search.count
search = Contact.search("Doe")
assert_equal 2, search.count
search = Contact.search("John Doe")
assert_equal 1, search.count
search = Contact.search("John ")
assert_equal 1, search.count
search = Contact.search("Doe John")
assert_equal 1, search.count
end
...and finally my model, which calls the search method looks like:
def self.search(search)
# Check if the search query has more than one word
search_split = search.split(" ")
if search_split.count > 1
# User must be searching for a first and last name
first_word = '%' + search_split[0] + '%'
second_word = '%' + search_split[1] + '%'
conditions = "
firstname ILIKE ? OR lastname ILIKE ?
AND
firstname ILIKE ? OR lastname ILIKE ?",
first_word, first_word, second_word, second_word
where(conditions)
else
# Just searching for a first OR last name
# Strip any whitespace
str = search_split[0]
query = '%' + str + '%'
conditions = "firstname ILIKE ? OR lastname ILIKE ?", query, query
where(conditions)
end
end
All tests pass except the one that does a test for "John Doe". It actually ends up pulling "Jane Doe" as well.
I'm thinking there is some kind of precedence issue with calling the two OR's before the adjoining AND, but does it make sense of what I'm trying to accomplish?
I'm not at the point where I'm refactoring just yet; just trying to get to green so I can move forward. Thanks for any help in advance!

You need a bit of reorganization to get the logic you're after:
conditions = "
(firstname ILIKE ? AND lastname ILIKE ?)
OR
(firstname ILIKE ? AND lastname ILIKE ?)",
first_word, second_word, second_word, first_word
That should match both "John Doe" and "Doe John" but not "Jane Doe" when you call:
Contact.search('Jo Do')
You don't actually need the parentheses because AND has a higher precedence than OR in Boolean Algebra (and SQL's logic is based on Boolean Algebra) but there's no good reason not to include them.

Related

How to put each element from array as a parameter of SQL query?

I have an array like this
matches = [1,2,3,4]
now i want to put that array in my query
.where(name LIKE ? AND last_name LIKE ? AND skin LIKE ?, matches[0], matches[1], matches[2]...)
sometime my query is
name LIKE ? AND last_name LIKE ?
for that only have to be the match two
matches[0], matches[1]
or
name LIKE ? for this only have to be the match matches[0]
Just use splat-operator. It will convert array to arguments
matches = %w[a b c]
Person.where('name LIKE ? AND last_name LIKE ? AND skin LIKE ?', *matches)
Person.where('name LIKE ? AND last_name LIKE ?', *matches[0..1])
Or may be hash with double splat
matches = { last_name: 'a', name: 'b', skin: 'c' }
Person.where('name :name ? AND last_name LIKE :last_name AND skin LIKE :skin', **matches)
Person.where('name :name ? AND last_name LIKE :last_name', **matches.slice(:name, :last_name))

Rails4: Is it possible to do an optional ActiveRecord search?

Say i had a record in my database like
+----+-----------+----------+
| id | firstname | lastname |
+----+-----------+----------+
| 1 | 'Bill' | nil |
+----+-----------+----------+
(note last name is nil)
Is there any where I can retrieve the above record using the following hash structure as search parameters:
vals = {firstname: "Bill", lastname: "test"}
Table.where(vals)
(ie: find the closest match, ignoring the nil column value in the table)
(I'm thinking of checking each key in the hash individually and stopping when a match is found, but just wondering if there is a more efficient way, specially for larger tables)
You could make custom search.
def self.optional_where params
query_params = params.keys.map do |k|
"(#{k} = ? OR #{k} IS NULL)"
end.join(" AND ")
where(query_params, *params.values)
end
Then you would use it like
Table.optional_where(vals)
This will produce next query
SELECT "tables".* FROM "tables" WHERE ((firstname = 'Bill' OR first_name IS NULL) AND (lastname = 'test' OR last_name IS NULL))
Let make a custom search like this:
scope :custom_search, -> (params) {
params.each do |k, v|
params[k] = if
if v.is_a? Array
(v << nil).uniq
else
[v, nil]
end
where(params)
end
}
Then we use it like:
search_params = {firstname: "Bill", lastname: "test"}
Table.custom_search(search_params)
The generated sql will be:
SELECT * FROM tables where firstname IN ['Bill', null] AND lastname IN ['test', null]
This means you don't care if one or more fields are nil

Rails Search ActiveRecord with Logical Operators

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.

Better search query for two columns forename and name

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!

Get multiple records with one query

User table:
name lastname
Bob Presley
Jamie Cox
Lucy Bush
Roman Cox
Find users
q = Query.new("Bob Presley, Cox, Lucy")
q.find_users => {0=>{:name=>"Bob", :lastname=>"Presley"}, 1=>{:lastname=>"Cox"}, 2=>{:name=>"Lucy"}}
Question:
I've got hash with few names and lastnames. I need to build Activerecord query to fetch all users from that hash.
If i have name and lastname I should find user with exactly the same name and lastname.
If I have only lastname or name I should find all users with this name or lastname. So when i search for :lastname => Cox it should return two users [Roman Cox,Jamie Cox]
I can do
object = []
hash = q.find_users
hash.each do |data|
#Pseudocode
# object << User.where(:name => if data[:lastname] exist, :lastname => if data[:name] exist)
end
But I think it is higly inefficient. How should I do this ?
Environment
rails: 3.0.3
ruby: 1.9.2-head
gem: meta_search https://github.com/ernie/meta_search
I'm sure this can be refactored nicely (hint!), but this code below will construct a SQL which can be used in a sub-select.
Code below does not sanitize the input values.
Note that you should sanitize the values in the h hash!
h = {0=>{:name=>"Bob", :lastname=>"Presley"}, 1=>{:lastname=>"Cox"}, 2=>{:name=>"Lucy"}}
conditions = ""
h.each_pair do |k,v|
if not conditions.empty?
conditions += " or "
end
conditions += "("
a_condition = ""
v.each_pair do |a,b|
if not a_condition.empty?
a_condition += " and "
end
a_condition += "#{a.to_s} = '#{b}'"
end
conditions += a_condition
conditions += ")"
end
conditions = "("+conditions+")"
p conditions
# => "((name = 'Bob' and lastname = 'Presley') or (lastname = 'Cox') or (name = 'Lucy'))"
# use the generated SQL conditions to find the users
#users = User.find(:all, :conditions => "(#{conditions})")

Resources