Advanced search on multiple associations in Rails - sql

Im trying to build advanced search finder for my Candidate model.
Lets imagine it has couple fields + multiple associations like has_many: languages & has_many: skills. Now I'm building query like this:
query = Candidate.select("*")
if position_name
query = query.where('position_name LIKE ? OR position_name IS NULL',"%#{position_name}%")
end
if salary
query = query.where('salary <= ? OR salary IS NULL',salary)
end
and so on...
Now I want to add more advanced conditions like to find users who only have such skills like PHP and Java (so return only those users who have both skills)
This works but only when I insert OR
query = query.joins(:skills)
query = query.where('`skills`.`name` = ? OR `skills`.`name` = ?',"Java","PHP")
Additionally I'd like the same also for languages (plus, language have language.name & language.level)
Can someone points me in which direction to look? And also how to build such condition where I can multiple skills or multiple languages?

Have a look at the various search gems like Ransack, Metawhere or Searchlogic
http://rubygems.org/gems/ransack
https://github.com/railsdog/searchlogic
Both Ransack and Searchlogic allow searching on associated models and you can use scopes to restrict the search parameters.
Example Search params for Searchlogic.
[search][admitted_gte]
[search][admitted_lte]
[search][aetiology_like_any][] VIRUS
[search][at_risk_gte]
[search][at_risk_lte]
[search][died_gte]
[search][died_lte]
[search][gezi_reference_like]
[search][id]
[search][incidents_location_encrypted_postcode_like]
[search][lab_confirmed_gte]
[search][lab_confirmed_lte]
[search][onset_first_after]
[search][onset_first_before]
[search][onset_last_after]
[search][onset_last_before]
[search][outbreak_type_equals_any][] FOODBORNE
[search][point_source_date_after]
[search][point_source_date_before]
[search][total_affected_gte]
[search][total_affected_lte]
[search][user_reference_like]
[search][year_equals_any][] 2010
search[order] descend_by_id
Outbreak_Controller.rb Index action returns the results of the Search query. From 17 Search params only a single searchlogic call is required #search = Outbreak.search(params[:search]). The params are whitelisted against a list of allowed search params - code not shown.
def index
#set the default index order to be descending Outbreak id
if !params[:search][:order]
params[:search][:order] = "descend_by_id"
end
if params[:search][:bacterial_agents_bacterium_name_like_any] != nil && !params[:search][:bacterial_agents_bacterium_name_like_any].empty?
params[:search][:bacterial_agents_category_like] = "CAUSATIVE"
end
if params[:search][:viral_agents_virus_name_like_any] != nil && !params[:search][:viral_agents_virus_name_like_any].empty?
params[:search][:viral_agents_category_like] = "CAUSATIVE"
end
if params[:search][:protozoal_agents_protozoa_name_like_any] != nil && !params[:search][:protozoal_agents_protozoa_name_like_any].empty?
params[:search][:protozoal_agents_category_like] = "CAUSATIVE"
end
if params[:search][:toxic_agents_toxin_name_like_any] != nil && !params[:search][:toxic_agents_toxin_name_like_any].empty?
params[:search][:toxic_agents_category_like] = "CAUSATIVE"
end
#Outbreak.search takes all of the given params and runs it against the Outbreak model and it's associated models
#search = Outbreak.search(params[:search])
end

Related

SQL ALchemy: Constuct or_ with dynamic list of conditionals

I am building a sports web app. I want a query that takes a list of players, and checks whether their game for that week has started yet.
The following query works for me, but I need to explicitly type out each player in the list. If I want my list to be dynamically sized, this will not work. Is there anything I can construct this query in a smarter way?
SQL Alchemy query:
db.session.query(Game.start).filter(Game.week_id == self.week_id).filter(or_(Game.home_team == self.players[1].team, Game.away_team == self.players[1].team, Game.home_team == self.players[0].team, Game.away_team == self.players[0].team)).all()
The query then looks like this:
SELECT game.start AS game_start
FROM game
WHERE game.week_id = %(week_id_1)s
AND (%(param_1)s = game.home_team_name OR
%(param_2)s = game.away_team_name OR
%(param_3)s = game.home_team_name OR
%(param_4)s = game.away_team_name)
Got it using in_
db.session.query(Game).filter(Game.week_id == self.week_id, Game.start==True).filter(or_(Game.home_team_name.in_([p.team_name for p in self.players]), Game.away_team_name.in_([p.team_name for p in self.players]))).all()

How to Write a Where Clause to Handle Either a Specific Value or Any Value

I have a report with a table in Rails where users can optionally set filters like selecting a location or picking a range of dates and update the table via an ajax request.
Can I write this where clause so that it any date/blanks or all locations?
#orders = Order.where('created_at <= ? AND ? <= created_at AND location_id = ?', date_order_start, date_order_end, loc_filter)
The query above fails on blanks (e.g., "") and if I put nils they translate to nulls in the SQL.
To solve this problem right now I have a bunch of conditional statements that check whether value is present in the ajax request and then creates a different where clause depending on the case. My current conditionals are unwieldy, error prone and not scalable.
Searches on things like "wildcard sql" end up leading me to text searches (i.e., %) which I don't think fits in this case.
I am running on Rails 3.2 with postgresql.
I sometimes use an array of query statements and arguments like this:
queries = []
args = []
if some_condition
queries.push("created_at <= ?")
args.push(whatever_date)
end
if another_condition
queries.push("created_at >= ?")
args.push(another_date)
end
#order = Order.where(queries.join(" AND "), *args)

Rails Active Record Search - Name includes a word

Im trying to pull all records from a Project model that includes in the project_name the word 'Fox'. I can do an active record search and return specific project_names, like 'Brown Fox':
#projects = Project.where("project_name like ?", "Brown Fox")
But if I want to return all the names that INCLUDE 'Fox', this does not work unless the complete project name is 'Fox':
#projects = Project.where("project_name like ?", "Fox")
How do I do a search that returns all the objects with the word 'Fox' in the name?
Try using:
variable = "Fox"
Project.where("project_name like ?", "%#{variable}%")
You can use the SQL % operator:
#projects = Project.where("project_name like ?", "%Fox%")
Note that if you want your query to return results ignoring the word case, you can use PostgreSQL ilike instead of like.
Did you try ransack ?
With ransack you can do something like
#projects = Project.search(:project_name_cont => "Fox")
If you think it is too much for what you need. you can use the % operator as MurifoX said
Here's a version that will allow you to handle any number of input words and to search for all of them within a name. I was looking for this answer and didn't find the more complicated case, so here it is:
def self.search(pattern)
if pattern.blank? # blank? covers both nil and empty string
all
else
search_functions = []
search_terms = pattern.split(' ').map{|word| "%#{word.downcase}%"}
search_terms.length.times do |i|
search_functions << 'LOWER(project_name) LIKE ?'
end
like_patterns = search_functions.join(' and ')
where("#{like_patterns}", *search_terms)
end
end

Best way to combine first_name and last_name columns in Ruby on Rails 3?

I have a method in my User model:
def self.search(search)
where('last_name LIKE ?', "%#{search}%")
end
However, it would be nice for my users to be able to search for both first_name and last_name within the same query.
I was thinking to create a virtual attribute like this:
def full_name
[first_name, last_name].join(' ')
end
But is this efficient on a database level. Or is there a faster way to retrieve search results?
Thanks for any help.
Virtual attribute from your example is just class method and cannot be used by find-like ActiveRecord methods to query database.
Easiest way to retrive search result is modifying Search method:
def self.search(search)
q = "%#{query}%"
where("first_name + ' ' + last_name LIKE ? OR last_name + ' ' + first_name LIKE ?", [q, q])
end
where varchar concatenation syntax is compatible with your database of choice (MS SQL in my example).
The search functionality, in your example, is still going to run at the SQL level.
So, to follow your example, your search code might be:
def self.search_full_name(query)
q = "%#{query}%"
where('last_name LIKE ? OR first_name LIKE ?', [q, q])
end
NOTE -- these sorts of LIKE queries, because they have a wildcard at the prefix, will be slow on large sets of data, even if they are indexed.
One way this can be implemented is by tokenizing (splitting) the search query and creating one where condition per each token:
def self.search(query)
conds = []
params = {}
query.split.each_with_index do |token, index|
conds.push "first_name LIKE :t#{index} OR last_name LIKE :t#{index}"
params[:"t#{index}"] = "%#{token}%"
end
where(conds.join(" OR "), params)
end
Also make sure you prevent SQL injection attacks.
However, it's better to use full-text searching tools, such as ElasticSearch and its Ruby gem named Tire to handle searches.
EDIT: Fixed the code.
A scope can be made to handle complex modes, here's an example from one project I'm working on:
scope :search_by_name, lambda { |q|
if q
case q
when /^(.+),\s*(.*?)$/
where(["(last_name LIKE ? or maiden_name LIKE ?) AND (first_name LIKE ? OR common_name LIKE ? OR middle_name LIKE ?)",
"%#{$1}%","%#{$1}%","%#{$2}%","%#{$2}%","%#{$2}%"
])
when /^(.+)\s+(.*?)$/
where(["(last_name LIKE ? or maiden_name LIKE ?) AND (first_name LIKE ? OR common_name LIKE ? OR middle_name LIKE ?)",
"%#{$2}%","%#{$2}%","%#{$1}%","%#{$1}%","%#{$1}%"
])
else
where(["(last_name LIKE ? or maiden_name LIKE ? OR first_name LIKE ? OR common_name LIKE ? OR middle_name LIKE ?)",
"%#{q}%","%#{q}%","%#{q}%","%#{q}%","%#{q}%"
])
end
else
{}
end
}
As you can see, I do a regex match to detect different patterns an build different searches depending on what is provided. As an added bonus, if nothing is provided, it returns an empty hash which effectively is where(true) and returns all results.
As mentioned elsewhere, the db cannot index the columns when a wildcard is used on both sides like %foo%, so this could potentially get slow on very large datasets.

How to specify multiple values in where with AR query interface in rails3

Per section 2.2 of rails guide on Active Record query interface here:
which seems to indicate that I can pass a string specifying the condition(s), then an array of values that should be substituted at some point while the arel is being built. So I've got a statement that generates my conditions string, which can be a varying number of attributes chained together with either AND or OR between them, and I pass in an array as the second arg to the where method, and I get:
ActiveRecord::PreparedStatementInvalid: wrong number of bind variables (1 for 5)
which leads me to believe I'm doing this incorrectly. However, I'm not finding anything on how to do it correctly. To restate the problem another way, I need to pass in a string to the where method such as "table.attribute = ? AND table.attribute1 = ? OR table.attribute1 = ?" with an unknown number of these conditions anded or ored together, and then pass something, what I thought would be an array as the second argument that would be used to substitute the values in the first argument conditions string. Is this the correct approach, or, I'm just missing some other huge concept somewhere and I'm coming at this all wrong? I'd think that somehow, this has to be possible, short of just generating a raw sql string.
This is actually pretty simple:
Model.where(attribute: [value1,value2])
Sounds like you're doing something like this:
Model.where("attribute = ? OR attribute2 = ?", [value, value])
Whereas you need to do this:
# notice the lack of an array as the last argument
Model.where("attribute = ? OR attribute2 = ?", value, value)
Have a look at http://guides.rubyonrails.org/active_record_querying.html#array-conditions for more details on how this works.
Instead of passing the same parameter multiple times to where() like this
User.where(
"first_name like ? or last_name like ? or city like ?",
"%#{search}%", "%#{search}%", "%#{search}%"
)
you can easily provide a hash
User.where(
"first_name like :search or last_name like :search or city like :search",
{search: "%#{search}%"}
)
that makes your query much more readable for long argument lists.
Sounds like you're doing something like this:
Model.where("attribute = ? OR attribute2 = ?", [value, value])
Whereas you need to do this:
#notice the lack of an array as the last argument
Model.where("attribute = ? OR attribute2 = ?", value, value) Have a
look at
http://guides.rubyonrails.org/active_record_querying.html#array-conditions
for more details on how this works.
Was really close. You can turn an array into a list of arguments with *my_list.
Model.where("id = ? OR id = ?", *["1", "2"])
OR
params = ["1", "2"]
Model.where("id = ? OR id = ?", *params)
Should work
If you want to chain together an open-ended list of conditions (attribute names and values), I would suggest using an arel table.
It's a bit hard to give specifics since your question is so vague, so I'll just explain how to do this for a simple case of a Post model and a few attributes, say title, summary, and user_id (i.e. a user has_many posts).
First, get the arel table for the model:
table = Post.arel_table
Then, start building your predicate (which you will eventually use to create an SQL query):
relation = table[:title].eq("Foo")
relation = relation.or(table[:summary].eq("A post about foo"))
relation = relation.and(table[:user_id].eq(5))
Here, table[:title], table[:summary] and table[:user_id] are representations of columns in the posts table. When you call table[:title].eq("Foo"), you are creating a predicate, roughly equivalent to a find condition (get all rows whose title column equals "Foo"). These predicates can be chained together with and and or.
When your aggregate predicate is ready, you can get the result with:
Post.where(relation)
which will generate the SQL:
SELECT "posts".* FROM "posts"
WHERE (("posts"."title" = "Foo" OR "posts"."summary" = "A post about foo")
AND "posts"."user_id" = 5)
This will get you all posts that have either the title "Foo" or the summary "A post about foo", and which belong to a user with id 5.
Notice the way arel predicates can be endlessly chained together to create more and more complex queries. This means that if you have (say) a hash of attribute/value pairs, and some way of knowing whether to use AND or OR on each of them, you can loop through them one by one and build up your condition:
relation = table[:title].eq("Foo")
hash.each do |attr, value|
relation = relation.and(table[attr].eq(value))
# or relation = relation.or(table[attr].eq(value)) for an OR predicate
end
Post.where(relation)
Aside from the ease of chaining conditions, another advantage of arel tables is that they are independent of database, so you don't have to worry whether your MySQL query will work in PostgreSQL, etc.
Here's a Railscast with more on arel: http://railscasts.com/episodes/215-advanced-queries-in-rails-3?view=asciicast
Hope that helps.
You can use a hash rather than a string. Build up a hash with however many conditions and corresponding values you are going to have and put it into the first argument of the where method.
WRONG
This is what I used to do for some reason.
keys = params[:search].split(',').map!(&:downcase)
# keys are now ['brooklyn', 'queens']
query = 'lower(city) LIKE ?'
if keys.size > 1
# I need something like this depending on number of keys
# 'lower(city) LIKE ? OR lower(city) LIKE ? OR lower(city) LIKE ?'
query_array = []
keys.size.times { query_array << query }
#['lower(city) LIKE ?','lower(city) LIKE ?']
query = query_array.join(' OR ')
# which gives me 'lower(city) LIKE ? OR lower(city) LIKE ?'
end
# now I can query my model
# if keys size is one then keys are just 'brooklyn',
# in this case it is 'brooklyn', 'queens'
# #posts = Post.where('lower(city) LIKE ? OR lower(city) LIKE ?','brooklyn', 'queens' )
#posts = Post.where(query, *keys )
now however - yes - it's very simple. as nfriend21 mentioned
Model.where(attribute: [value1,value2])
does the same thing