I have a Post model, with two attributes: title and content.
I'm trying to make a search form that will look for keywords in the title and/or the content columns.
Let's say there's this record:
title: Foobar bar foo
content: foobar baz foo
Now, if a user inputs the "bar baz" or "baz bar" keywords (order is not important), that record should be found.
What sort of query do I need to accomplish this?
If you are certain the user input is space separated (per keyword):
If you use Postresql this would work:
keywords = user_input.split(" ").map{ |val| "%#{val}%"}
Post.where("title iLIKE ANY ( array[?] )", keywords)
For multiple columns:
Post.where("title iLIKE ANY ( array[?] ) OR content iLIKE ANY ( array[?] )", keywords, keywords)
If you have a Post model with title and content attributes then you can search records like :
Post.where("(title ILIKE (?)) or (content ILIKE (?))","%#{params[:title_param]}%","%#{params[:content_param]}")
Related
I am doing a query over my POSTGRESQL DB. My app has Articles and the Articles can have a number of Hashtags. Those relations are saved in a joined table of Hashtags and Articles.
I have a working method which gives me back Articles which have certain hashtags, or gives me back all articles who do not contain certain hashtags
def test(hashtags, include = true)
articles= []
hashtags.split(' ').each do |h|
articles+= Article.joins(:hashtags).where('LOWER(hashtags.value) LIKE LOWER(?)', "#{h}")
end
if include
articles.uniq
else
(Article.all.to_set - articles.uniq.to_set).to_a
end
end
I could call it like this:
test("politics people china", true)
And it would give me all Articles who have one of those hashtags related to
Or I could call it like that
test("politics people china", false)
And it would give me all Articles EXCEPT those who have one of these hashtags
It works well, but I dont think this is very efficient as I do so much in Ruby and not on DB level.
I tried this:
def test2(hashtags, include = true)
articles= []
pattern = ''
hashtags.split(' ').each do |h|
pattern += "#{h}|"
end
pattern = '(' + pattern[0...-1] + ')'
if include
articles = Article.joins(:hashtags).where('hashtags.value ~* ?', "#{pattern}")
else
articles = Article.joins(:hashtags).where('hashtags.value !~* ?', "#{pattern}")
end
articles.uniq
end
But it does not behave like I thought it would. First of all if I call it like that:
test2("politics china", true)
It wouldn't only give me all Articles who have a hashtags politics or china, but also all artcles who have a hashtag containing one of the letters in politics or china like so:
(p|o|l|i|t|c|s|h|n|a)
but it should check for this actually, and the pattern looks actually like this, what I can see in the console:
(politics|china)
which it doesnt what I find is strange tbh...
And with
test2("politics", false)
It only gives me articles who have one or more hashtags associated to, BUT leaves out the ones who have no hashtag at all
Can someone help me make my working method more efficient?
EDIT:
Here is my updated code like suggested in an answer
def test2(hashtags, include = false)
hashtags =
if include
Hashtag.where("LOWER(value) iLIKE ANY ( array[?] )", hashtags)
else
Hashtag.where("LOWER(value) NOT iLIKE ANY ( array[?] )", hashtags)
end
Slot.joins(:hashtags).merge(hashtags).distinct
end
It still lacks to give me Articles who have NO hashtags at all if incude is false unfortunately
You are right about
I dont think this is very efficient as I do so much in Ruby and not on DB level.
ActiveRecord works nice for simple queries, but when things are getting complex it's reasonable to use plain SQL. So let's try to build a query that matches your test cases:
1) For this call test("politics people china", true) the query may look like:
SELECT DISTINCT ON (AR.id) AR.*
FROM articles AR
JOIN articles_hashtags AHSH ON AHSH.article_id = AR.id
JOIN hashtags HSH ON HSH.id = AHSH.hashtag_id
WHERE LOWER(HSH.value) IN ('politics', 'people', 'china')
ORDER BY AR.id;
(I'm not sure how your join table is named, so assuming it is articles_hashtags).
Plain and simple: we take data from articles table using 2 inner joins with articles_hashtags and hashtags and where conditions, which filters hashtags we want to see; and eventually it brings us all articles with that hashtags. No matter on how many hashtags we want to filter: IN statement works well even if there is only one hashtag in the list.
Please note DISTINCT ON: it's necessary for removing duplicate articles from resultset, in case the same article has more than one hashtag from given hashtag list.
2) For the call test("politics people china", false) the query is a bit more complex. It needs to exclude articles which have given hashtags. Hence it should return articles with different hashtags, as well as articles without hashtags at all. Trying to keep things simple we could use the previous query for that:
SELECT A.*
FROM articles A
WHERE A.id NOT IN (
SELECT DISTINCT ON (AR.id) AR.id
FROM articles AR
JOIN articles_hashtags AHSH ON AHSH.article_id = AR.id
JOIN hashtags HSH ON HSH.id = AHSH.hashtag_id
WHERE LOWER(HSH.value) IN ('politics', 'people', 'china')
ORDER BY AR.id
);
Here we're fetching all articles, but those who have any of given hashtags.
3) Converting these queries to a Ruby method gives us the following:
def test3(hashtags, include = true)
# code guard to prevent SQL-error when there are no hashtags given
if hashtags.nil? || hashtags.strip.blank?
return include ? [] : Article.all.to_a
end
basic_query = "
SELECT DISTINCT ON (AR.id) AR.*
FROM #{Article.table_name} AR
JOIN articles_hashtags AHSH ON AHSH.article_id = AR.id
JOIN #{Hashtag.table_name} HSH ON HSH.id = AHSH.hashtag_id
WHERE LOWER(HSH.value) IN (:hashtags)
ORDER BY AR.id"
query = if include
basic_query
else
"SELECT A.*
FROM #{Article.table_name} A
WHERE A.id NOT IN (#{basic_query.sub('AR.*', 'AR.id')})"
end
hashtag_arr = hashtags.split(' ').map(&:downcase) # to convert hashtags string into a list
Article.find_by_sql [query, { hashtags: hashtag_arr }]
end
The method above will return an array of articles matching your conditions, empty or not.
Try this:
def test(hashtags, include = true)
hashtags =
if include
Hashtag.where("LOWER(value) iLIKE ANY ( array[?] )", hashtags)
else
Hashtag.where("LOWER(value) NOT iLIKE ANY ( array[?] )", hashtags)
end
Article.joins(:hashtags).merge(hashtags).distinct
end
Try makes the following query:
title = "%#{params[:title]}%"
group = params[:group]
#foods = Food.order('visits_count DESC').where("title ILIKE ? OR group ILIKE ?", title, group).decorate
and in return I get the following error:
ActionView::Template::Error (PG::SyntaxError: ERROR: syntax error at or near "group"
LINE 1: ..."foods".* FROM "foods" WHERE (title ILIKE '%%' OR group ILIK...
^
: SELECT "foods".* FROM "foods" WHERE (title ILIKE '%%' OR group ILIKE '') ORDER BY visits_count DESC):
Any ideas?
Try
"group"
with double-quotes.
Since this is PostgreSQL, you need to quote identifiers using double quotes when case sensitivity is an issue or you're using a keyword as an identifier:
...where('title ILIKE ? OR "group" ILIKE ?', title, group)...
You'd be better off not using a keyword as a column IMO so that you don't have to worry about this sort of thing. I'd go with he quoting for now and then rename the column as soon as possible but that's just me.
While I'm here, you might want to leave out those checks when they don't make sense. This part of your query:
title ILIKE '%%'
will always match when title is not null so you might want to use an explicit not null check or leave it out entirely when there is no title. Similarly for "group" ILIKE '' which only matches when "group" is empty.
I would like to write an ActiveRecord query to fetch all records which include specific foreign language words (case insensitive) in the description column of records table.
I think I can use mb_chars.downcase which converts international chars to lower case successfully.
> "ÖİÜŞĞ".mb_chars.downcase
=> "öiüşğ"
However, when I try to use mb_chars.downcase in ActiveRecord query I receive the following error:
def self.targetwords
target_keywords_array = ['%ağaç%','%üzüm%','%çocuk%']
Record.where('description.mb_chars.downcase ILIKE ANY ( array[?] )', target_keywords_array)
end
ActiveRecord::StatementInvalid (PG::UndefinedTable: ERROR: missing FROM-clause entry for table "mb_chars"
I will appreciate if you can guide me how to solve this problem.
You're trying too hard, you should let the database do the work. PostgreSQL knows how to work with those characters in a case-insensitive fashion so you should just let ilike deal with it inside the database. For example:
psql> select 'ÖİÜŞĞ' ilike '%öiüşğ%';
?column?
----------
t
(1 row)
You could say simply this:
def self.targetwords
target_keywords_array = ['%ağaç%','%üzüm%','%çocuk%']
Record.where('description ILIKE ANY ( array[?] )', target_keywords_array)
end
or even:
def self.targetwords
Record.where('description ilike any(array[?])', %w[ağaç üzüm çocuk].map { |w| "%#{w}%" })
end
or:
def self.targetwords
words = %w[ağaç üzüm çocuk]
likeify = ->(w) { "%#{w}%" }
Record.where('description ilike any(array[?])', words.map(&likeify))
end
As far as why
Record.where('description.mb_chars.downcase ILIKE ANY ( array[?] )', ...)
doesn't work, the problem is that description.mb_chars.downcase is a bit of Ruby code but the string you pass to where is a piece of SQL. SQL doesn't know anything about Ruby methods or using . to call methods.
By using a railscast video i create a simple search that works on same model. But now i have a model that shows associated model data as well and i would like to search on them as well.
Right now i managed to make it semi work, but i assume i have conflict if i add the field "name" into the joins as i have two models that have a column named "name"
def self.search(search)
if search
key = "'%#{search}%'"
columns = %w{ city station venue area country plate_number }
joins(:services).joins(:washer).joins(:location).where(columns.map {|c| "#{c} ILIKE #{key}" }.join(' OR '))
else
where(nil)
end
end
What do i need to change to be sure i can search across all columns?
I think when you have ambiguous field name after join then you can mention table_name.field_name so it remove the ambiguity and works. something like.
joins(:services).joins(:washer).where("services.name = ? or washer.name = ?", "test", "test")
I currently have a scope:
scope :named,->(query) do
where("(first_name || last_name) ~* ?", Regexp.escape(query || "a default string to prevent Regexp.escape(nil) errors"))
end
This works, except for when first_name or last_name is nil. How do I find with these columns if one of the columns is nil?
I think you can try something like this:
scope :named,->(query) do
where("first_name IS NOT NULL OR last_name IS NOT NULL AND (first_name || last_name) ~* ?", Regexp.escape(query || "a default string to prevent Regexp.escape(nil) errors"))
end
This is a basic scope to search for a name, it handles a search for 'first_name only', 'last_name only' or a search for full name.
If you split the query into an array, you can grab the first and last of the array and does not matter whether the query is one or two in length.
You could replace the split with your regex if needed
You could obviously expand on this (e.g. case insensitive, ordering), but gives the basic idea.
Also handles a nil query by defaulting to a empty string, if the strip fails.
scope :named,->(query) do
query.strip! rescue query = ""
query_words = query.split(' ')
where(["first_name LIKE ? OR last_name LIKE ?", "#{query_words.first}%", "#{query_words.last}%"])
end
Hope this helps