Multi term Ransack search in same field not working - ruby-on-rails

I have implemented Ransack for my site's search function and want to be able to search in one database column for multiple terms entered in the same input field.
Here is the code in my controller:
if params[:q]
params[:q][:groupings] = []
split_genres = params[:q][:genres_name_cont].split(' ')
params[:q][:genres_name_cont].clear
split_genres.each_with_index do |word, index|
params[:q][:groupings][index] = {genres_name_cont: word}
end
end
#q = Band.ransack(params[:q])
#bands = #q.result(distinct: true).includes(:genres).sort_by{|band| band.name}
And here is the query which is returned when I enter multiple genres in the search:
SELECT "bands".* FROM "bands" LEFT OUTER JOIN "bands_genres"
ON "bands_genres"."band_id" = "bands"."id" LEFT OUTER JOIN "genres"
ON "genres"."id" = "bands_genres"."genre_id"
WHERE ("genres"."name" ILIKE '%rock%' AND "genres"."name" ILIKE '%blues%')
The query looks right to me, especially with it being more or less identical to the query generated when only one term is entered, which works fine.
Can anybody shed any light on what I may need to change to get this search working when multiple terms are entered within the field?

Welcome to Stack Overflow 👋.
The query doesn't work for multiple genres because it's only picking rows from the genres table where the name of a genre contains both "rock" and "blues", so it would only match genres like "rock/blues" or "bluesy rock ballads".
It sounds like what you're after is WHERE ("genres"."name" ILIKE '%rock%' OR "genres"."name" ILIKE '%blues%'), where OR means it will match either, so both the "rock" and "blues" genres will be matched.
In terms of getting this to work in Ransack, this comment on the GitHub project looks similar to yours, but includes the following line before you set your groupings:
params[:q][:combinator] = 'or'
I haven't tested this locally, but it appears that adding that line will convert the group combination from AND to OR, which should get you the right search results.
Let me know how it goes and I'll remove the tentative "should" and "appears" if it works 😉.

Related

How to use ilike for phone number filter

Hi im fetching the user input and displaying the records that matches the condition, my query will look like
customers = customers.where('customers.contact_num ilike :search', {search: "%#{options[:search_contact]}%"})
here in db the contact number is stored in string with the format (091)-234-5678 like that
on while searching the user on basis of contact number if i search like this
091 it filters the number correctly, but while searching like 0912, it doesn't display record due to the braces, so how to modify the query to neglect the ) and - while searching..
As im new to the domain please help me out
thanks in advance
What about using REGEXP_REPLACE to remove all non-digit chars from the search - something like below?
customers = customers.where("REGEXP_REPLACE(customers.contact_num,'[^[:digit:]]','','g') ilike :search", {search: "%#{options[:search_contact]}%"})
Changing the query is hard. Let's not do that.
Instead right a quick script to transforms your numbers into
1112223333 form. No formatting at all. Something like:
require 'set';
phone = "(234)-333-2323"
numbers = Set.new(["1","2","3","4","5","6","7","8","9","0"])
output = phone.chars().select{|n| numbers.include?(n)}.join("")
puts output
=> "2343332323"
Then write a little function to transform them into display form for use in the views.
This will make your query work as is.

How to search associations, only IF they exist (so not using a join)

Let's say I have an Order, that has a title. Order has an optional Company that has a name. Order also has multiple LineItems, that connect to Products, that have a name.
I want to create a search feature that allows a user to query Order.title, Company.name and/or Products.name. If an Order has no connected Company or Products, that should not stop it from matching.
For example, this doesn't work, because if there's no Company, even if the Order.name matches, it won't return because the join wont work.
Order.joins([:company, {line_items: [:product]}]).
where("orders.name = ? OR company.name = ? OR product.name = ?", query)
When you use only joins, Rails does a inner join. You can use left_joins, so that the query will return rows even where the association doesn't exist:
Order.left_joins([:company, {line_items: [:product]}]).
where("orders.name = ? OR company.name = ? OR product.name = ?", query)
As max suggested, if you're gonna use some value from those nested associations (like the company name or something like that) I would recommend you to use eager_load instead of left_joins, so rails will load the data from those tables and it will avoid additional queries
I would also recommend, since you're doing a search, to uppercase both your columns and you query, so it won't be case sensitive. Also, would be good to use LIKE instead of EQUAL comparator (so it will return the record even if it doesn't match entirely). Something like this should work better:
Order.left_joins([:company, {line_items: [:product]}]).
where("UPPER(orders.name) LIKE ? OR UPPER(company.name) LIKE ? OR UPPER(product.name) LIKE ?", query.upcase)
Hope this helps!

How do I find a specific record with a params[] in a resulting query found using multiple joins

I currently have a complex query which successfully returns multiple records for a complete list. This query contains LEFTJOINS including a LEFTJOIN to itself. I now am creating a report which needs to allow the selection of a specific record in the group. I am having difficulty discovering how to form this request when there are multiple joins including left joins. The returned parameter is in params[:search]. The query I am trying to use is:
#horses = Horse.find_by_sql["SELECT horses.id, horses.horse_name, horses.registration_number, horses.registration_number_2, horses.sire_name, horses.dam_name, horses_1.horse_name AS sire_name, horses_2.horse_name AS dam_name, horses.foaling_date, breeds.breed_name, colours.colour_name, genders.gender_name, owners.last_name, horses.arrival_date, horses.date_left, horses.notes FROM (((((horses LEFT JOIN horses AS horses_1 ON horses.sire_id = horses_1.id) LEFT JOIN horses AS horses_2 ON horses.dam_id = horses_2.id) LEFT JOIN breeds ON horses.breed_id = breeds.ID) LEFT JOIN colours ON horses.colour_id = colours.ID) LEFT JOIN genders ON horses.gender_id = genders.ID) LEFT JOIN Owners ON horses.owner_id = Owners.ID WHERE horses.id = ? ORDER BY horses.horse_name", params["search"]]
Updated after questions below:
There is no real advantage to listing the tables since the query works fine without the search parameter, in fact it is the query I use to produce the horse list. Adding the params["search"] is causing the issue, I have confirmed that it returns the correct value using debugger: params["search"] = 5, the intended id.
The error I get gives me a suggestion which is the same as I have done already. I just must be doing something really stupid that I just can't see.
Error Message:
wrong number of arguments (0 for 1..2) suggestion: # Post.find_by_sql ["SELECT title FROM posts WHERE author = ?", start_date]
Thanking you in advance
The answer to this is: There should have been a space between the "find_by_sql" and the "[". When I put this space in, it works fine.

Active Record - Chain Queries with OR

Rails: 4.1.2
Database: PostgreSQL
For one of my queries, I am using methods from both the textacular gem and Active Record. How can I chain some of the following queries with an "OR" instead of an "AND":
people = People.where(status: status_approved).fuzzy_search(first_name: "Test").where("last_name LIKE ?", "Test")
I want to chain the last two scopes (fuzzy_search and the where after it) together with an "OR" instead of an "AND." So I want to retrieve all People who are approved AND (whose first name is similar to "Test" OR whose last name contains "Test"). I've been struggling with this for quite a while, so any help would be greatly appreciated!
I digged into fuzzy_search and saw that it will be translated to something like:
SELECT "people".*, COALESCE(similarity("people"."first_name", 'test'), 0) AS "rankxxx"
FROM "people"
WHERE (("people"."first_name" % 'abc'))
ORDER BY "rankxxx" DESC
That says if you don't care about preserving order, it will just filter the result by WHERE (("people"."first_name" % 'abc'))
Knowing that and now you can simply write the query with similar functionality:
People.where(status: status_approved)
.where('(first_name % :key) OR (last_name LIKE :key)', key: 'Test')
In case you want order, please specify what would you like the order will be after joining 2 conditions.
After a few days, I came up with the solution! Here's what I did:
This is the query I wanted to chain together with an OR:
people = People.where(status: status_approved).fuzzy_search(first_name: "Test").where("last_name LIKE ?", "Test")
As Hoang Phan suggested, when you look in the console, this produces the following SQL:
SELECT "people".*, COALESCE(similarity("people"."first_name", 'test'), 0) AS "rank69146689305952314"
FROM "people"
WHERE "people"."status" = 1 AND (("people"."first_name" % 'Test')) AND (last_name LIKE 'Test') ORDER BY "rank69146689305952314" DESC
I then dug into the textacular gem and found out how the rank is generated. I found it in the textacular.rb file and then crafted the SQL query using it. I also replaced the "AND" that connected the last two conditions with an "OR":
# Generate a random number for the ordering
rank = rand(100000000000000000).to_s
# Create the SQL query
sql_query = "SELECT people.*, COALESCE(similarity(people.first_name, :query), 0)" +
" AS rank#{rank} FROM people" +
" WHERE (people.status = :status AND" +
" ((people.first_name % :query) OR (last_name LIKE :query_like)))" +
" ORDER BY rank#{rank} DESC"
I took out all of quotation marks in the SQL query when referring to tables and fields because it was giving me error messages when I kept them there and even if I used single quotes.
Then, I used the find_by_sql method to retrieve the People object IDs in an array. The symbols (:status, :query, :query_like) are used to protect against SQL injections, so I set their values accordingly:
# Retrieve all the IDs of People who are approved and whose first name and last name match the search query.
# The IDs are sorted in order of most relevant to the search query.
people_ids = People.find_by_sql([sql_query, query: "Test", query_like: "%Test%", status: 1]).map(&:id)
I get the IDs and not the People objects in an array because find_by_sql returns an Array object and not a CollectionProxy object, as would normally be returned, so I cannot use ActiveRecord query methods such as where on this array. Using the IDs, we can execute another query to get a CollectionProxy object. However, there's one problem: If we were to simply run People.where(id: people_ids), the order of the IDs would not be preserved, so all the relevance ranking we did was for nothing.
Fortunately, there's a nice gem called order_as_specified that will allow us to retrieve all People objects in the specific order of the IDs. Although the gem would work, I didn't use it and instead wrote a short line of code to craft conditions that would preserve the order.
order_by = people_ids.map { |id| "people.id='#{id}' DESC" }.join(", ")
If our people_ids array is [1, 12, 3], it would create the following ORDER statement:
"people.id='1' DESC, people.id='12' DESC, people.id='3' DESC"
I learned from this comment that writing an ORDER statement in this way would preserve the order.
Now, all that's left is to retrieve the People objects from ActiveRecord, making sure to specify the order.
people = People.where(id: people_ids).order(order_by)
And that did it! I didn't worry about removing any duplicate IDs because ActiveRecord does that automatically when you run the where command.
I understand that this code is not very portable and would require some changes if any of the people table's columns are modified, but it works perfectly and seems to execute only one query according to the console.

Find record with LIKE on partial attribute

In my app I have invoice numbers like this:
2014.DEV.0001
2014.DEV.0002
2014.TSZ.0003
The three character code is a company code. When a new invoice number needs to be assigned it should look for the last used invoice number for that specific company code and add one to it.
I know the company code, I use a LIKE to search on a partial invoice number like this:
last = Invoice.where("invoice_nr LIKE ?", "#{DateTime.now.year}.#{company_short}.").last
This results in this SQL query:
SELECT "invoices".* FROM "invoices" WHERE "invoices"."account_id" = 1 AND (invoice_nr LIKE '2014.TSZ.') ORDER BY "invoices"."id" DESC LIMIT 1
But unfortunately it doesn't return any results. Any idea to improve this, as searching with LIKE doesn't seem to be correct?
Try wrapping string with % and use lower to convert the query string and result into downcase to avoid any wrong results due to case, try this
last = Invoice.where("lower(invoice_nr) LIKE lower(?)", "%#{DateTime.now.year}.#{company_short}.%").last
You want % for partial match
last = Invoice.where("invoice_nr LIKE ?", "%#{DateTime.now.year}.#{company_short}.%").last
Since you want to match only the left part you need to add one % at the right part of your string
Invoice.where("invoice_nr LIKE ?", "#{DateTime.now.year}.#{company_short}.%").last

Resources