Multiple keywords database search - ruby-on-rails

I am currently using scopes in my model to perform searches within a database. I can stack these scopes and it will output results matching all parameters.
scope :search_between, lambda{|begin_date, end_date|
where "sub.date BETWEEN ? AND ?", begin_date, end_date
}
What I am having trouble with is integrating a keywords search that will search the entire database and output the results that contain the sum of the keywords. I would like to do something like this (displayed for simplicity):
scope :keywords, lambda{|search|
search.chomp.split(/,\s*/) do |item|
where "date like ? or city like ? or state like ?", "%#{item}%" and
where "date like ? or city like ? or state like ?", "%#{item}%" and
where "date like ? or city like ? or state like ?", "%#{item}%"
end
}
I am currently using something like this to take care of searching multiple columns:
scope :keywords, lambda{|search|
search.chomp.split(/,\s*/) do |item|
where(Sub.column_names.map {|cn| "#{cn} like ?" }.join("or "), "%#{item}%"] Sub.column_names.size)).join(' AND ')
end
}
My problem is that I want to do multiple "where()'s" in the scope. Is it possible and if so, how?

Just turn it into a method that returns a scope:def self.keywords(search)
scope = self.scoped
search.chomp.split(/,\s*/).each do |item|
scope = scope.where(["date like ? or
city like ? or
state like ?", "%#{item}%","%#{item}%","%#{item}%"])
end
scope
end
The drawback is that you can't chain keywords off of other scopes, but you can chain other scopes off of keywords.

I think you will be hitting the limit with using the database for full text search.
Have you looked into using Solr or Sphinx for your full text searches? There's also IndexTank if you want to use a 3rd party service.
For solr, there are rubygems available to help you: sunspot, solrsan(i'm the author)
Sunspot is definitely more full-featured and solrsan is a barebones layer.

Related

Rails 5. How to search in associated models?

So I have 2 models: City and Country. City belongs_to Country, you know how it works.
How to find Country when I'm putting City name into search field?
I know how it works with 1 model
#countries = Country.where(["name LIKE ?", "%#{params[:search]}%"])
but what to do in this case?
I'll be grateful for any useful links or tips, cos I can't find something like that.
Try this:
#countries = Country.joins(:cities).where("cities.name LIKE ?", "%#{params[:search]}%")
This may be what you're looking for:
#countries = Country.all.includes(:cities).where("cities.name" => params[:search]).all
By the way, it is a bad idea to put search form entry directly into an SQL query as written in your original post as it opens your app up to SQL injection. Read this article here under the header "A1 SQL Injection":
https://blog.codeship.com/preproduction-checklist-for-a-rails-app/

Why does result become redundant if I try to use Active Record?

I want to retrieve all the community_topics that contains the search keyword within either community_topics.title or community_topics.body or comments.body
But this code below retrieve and shows bunch of doubled, tripled or more! records:(
I only want them as single record. How can I?
#community_topics = CommunityTopic.joins(:community, :comment_threads).merge(Community.not_deleted).where('community_topics.title like ? OR community_topics.body like ? OR comments.body like ?', "%"+params[:search]+"%", "%"+params[:search]+"%", "%"+params[:search]+"%").order('last_active_at DESC').page(params[:page]).per(10)
I'd use something such as:
#community_topics = CommunityTopic.joins(:community, :comment_threads)
.merge(Community.not_deleted)
.where('community_topics.title like :term
OR community_topics.body like :term
OR comments.body like :term',
term: "%#{params[:search]}%")
.order('last_active_at DESC')
.uniq.page(params[:page]).per(10)
However, the query is overcomplicated. Why is Community.not_deleted being merged? Why are you passing in page and per? Is this being implemented in a controller? If so, extract into your model. Create relevant scopes and methods. Here is an example (untested):
model
model CommunityTopic
scope :latest_active, order('last_active_at DESC')
scope :eager, joins(:community, :comment_threads).merge(Community.not_deleted)
def self.search(term)
eager.where('community_topics.title like :term
OR community_topics.body like :term
OR comments.body like :term',
term: "%#{term}%").uniq
end
end
controller
#community_topics = CommunityTopic.latest_active.search(params[:search]).page(params[:page]).per(10)
That's just a rough example. If you can explain why you're using Community.not_deleted merge, could probably clean that up. Could you add details with the actual sql query produced from the above?

Rails: Sorting Objects From Different Models

I've populated a hash with two different models. I then try to sort them like so:
#search_results = User.find(:all, :conditions => ['name LIKE ?', "%#{params[:query]}%"])
#search_results += Book.find(:all, :conditions => ['title LIKE ?', "%#{params[:query]}%"])
#search_results.sort! { |a,b| a.impressions_count <=> b.impressions_count }
This throws the following error:
comparison of User with Book failed
Both users and books have an integer-based impressions_count. Why can't I sort via this attribute? What other options do I have?
I faced a similar problem recently and ended up writing some custom sql because all other ways returned an array. Pretty sure its not a good idea to use the sort method since it will always be more efficient to sort in SQL than ruby, especially when the data set gets large
#combined_results = User.find_by_sql(["SELECT title, id, impressions_count, NULL as some_attribute_of_book
FROM user
WHERE title LIKE ?
UNION SELECT title, id, impressions_count, some_attribute_of_book FROM book
WHERE title LIKE ?
ORDER BY impressions_count", params[:query], params[:query]])
The above is completely untested code, more of an example than anything

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.

How to filter results by multiple fields?

I am working on a survey application in ruby on rails and on the results page I want to let users filter the answers by a bunch of demographic questions I asked at the start of the survey.
For example I asked users what their gender and career was. So I was thinking of having dropdowns for gender and career. Both dropdowns would default to all but if a user selected female and marketer then my results page would so only answers from female marketers.
I think the right way of doing this is to use named_scopes where I have a named_scope for every one of my demographic questions, in this example gender and career, which would take in a sanitized value from the dropdown to use at the conditional but i'm unsure on how to dynamically create the named_scope chain since I have like 5 demographic questions and presumably some of them are going to be set to all.
You can chain named scopes together:
def index
#results = Results.scoped
#results = #results.gender(params[:gender]) unless params[:gender].blank?
#results = #results.career(params[:career]) unless params[:career].blank?
end
I prefer however to use the has_scope gem:
has_scope :gender
has_scope :career
def index
#results = apply_scopes(Results).all
end
If you use has_scope with inherited_resources, you don't even need to define the index action.
named_scope :gender,lambda { |*args|
unless args.first.blank?
{ :conditions => [ "gender = ?", args.first] }
end
}
If you write named scopes in this way, you can have all them chained, and if one of your params will be blank wont breaks.
Result.gender("Male") will return male results.
Result.gender("") will return male and female too.
And you can chain all of your methods like this. Finally as a filtering you can have like:
Result.age(16).gender("male").career("beginer")
Result.age(nil).gender("").career("advanced") - will return results with advanced career, etc.
Try some like this:
VistaFact.where( if active then {:field => :vista2} else {} end)
Or like this:
VistaFact.where(!data.blank? ? {:field=>data.strip} : {}).where(some? ? {:field2 => :data2} : {}).where ...
That work for me very nice!

Resources