how to use LIKE in included relations - ruby-on-rails

I want to use search condition with included relations, just like below
Post.includes(:tags).where( tags: { title: '%token%' }).all
The posts and tags table has been associated with a 3rd table named post_tag_relations.
The schema is like below:
posts
id: pk
title: string
content: text
tags
id: pk
title: string
post_tag_relations
id: pk
tag_id: integer
post_id: integer
The syntax only works with equal condition, I really dont know how to use LIKE search condition.
When using Post.joins(:tags) and Tag.area_table[:title].matches('%token%') it will works fine, but some post that has no tags will not be fetch out.
Could anyone help me? Thanks a lot.
UPDATE:
The Rails version is 4.1.
I want to search the post like posts.title LIKE '%token%' OR tags.title LIKE '%token%', so if use Post.joins(:tags) will not be functional if some posts have no tags. So I need use Post.includes(:tags) instead.
UPDATED AGAIN:
looks cannot use one-query to fetch, so I had already try another database schema...

Why not do this:
Post.includes(:tags).where(Tag.arel_table[:title].matches('%token%').or(Tag.arel_table[:post_id].eq(nil)))

Since ruby-on-rails-2 the joins operation is used in all cases before the includes operation during performance, but since includes uses LEFT OUTER JOIN operator, you should use exactly it. May be you need also to use not LEFT, but FULL join. So try this with arel gem:
class Post
scope :with_token(token), -> do |token|
re = Regexp.union(token).to_s
cond = Arel.sql("title REGEXP ? OR content REGEXP ?", re, re)
includes(:tags).where(Tag.arel_table[:title].eq(token).or(cond))
end
end
Of course original condition could be replaced to use LIKE operator:
class Post
scope :with_token(token), -> do |token|
includes(:tags).where(arel_table[:title].matches("%#{token}%")
.or(arel_table[:content].matches("%#{token}%")
.or(Tag.arel_table[:title].eq(token))))
end
end
NOTE: If there are some errors, provide please result SQL.

Something like this:
Post.includes(:tags).where( "tags.title LIKE ?", "%#{token}%" )
could work.
(The syntax might be a little wrong, sorry, but you get the idea)

Related

Rails .where any field contains specific text

Is there a short-hand way of querying a Rails database for any record that has a field containing a specific piece of text? I know I could code every field with a .where("field_name LIKE ?", "my text"), but I have several fields and am wondering if there is a shorter way of doing this.
Thanks in advance.
I do not know of a framework-way to do so. You could code something using
my_attributes = YourModel.attributes
# delete attributes you do not need, like `id` etc.
# or just create an array with your desired attributes,
# whichever way is faster
queries = my_attributes.map { |attr| "#{attr} LIKE %insert_your_text_here%" }
# Do not use this if the text your looking for is provided by user input.
built_query = queries.join(" OR ")
YourModel.where(built_query)
This could bring you closer to your goal. Let me know if this makes sense to you.
edit: The answer https://stackoverflow.com/a/49458059/299781 mentions Ransack. That's a nice gem and takes the load off of you. Makes it easier, nicer and performs better :D
Glad you like this, but pay attention that you make your app open for sql injection, if you take user-input as the text you are looking for. (with this solution) Ransack would alleviate that.
class MyModel
scope :search_like, -> (field_name, search_string) {where("#{field_name} LIKE ?", "%#{search_string}%")}
end
then you can call it like:
MyModal.search_like('name', 'foobar')
UPDATE based on #holgar answer but beware if not indexed these searches can be slow on large data sets:
class MyModel
def self.multi_like(search_string)
my_attributes = [:first_name, :last_name] # probalby only use string fields here
queries = my_attributes.map { |attr| "#{attr} LIKE '%#{search_string}%'" }
where(queries.join(" OR "))
end
end
If you want full fledge text search based on params then you can use ransack gem

activerecord not like query

I could not find an activerecord equivalent of "Not Like". I was able to find a where.not, but that will check if a string does not match a value, as so:
User.where.not(name: 'Gabe')
is the same as:
User.where('name != ?', 'Gabe')
I was looking for a NOT LIKE, where the value is not contained in the string. The equivalent sql query would look like as follows:
SELECT * FROM users WHERE name NOT LIKE '%Gabe%'
In ActiveRecord I can currently get away with the following:
User.where("name NOT LIKE ?", "%Gabe%")
But that leaves a lot to be desired. Any new additions to Rails 4 to facilitate this?
Well, you can do something like:
User.where.not("name LIKE ?", "%Gabe%")
Note: This is only available in Rails 4.
As others have pointed out ActiveRecord does not have a nice syntax for building like statements. I would suggest using Arel as it makes the query less database platform specific (will use ilike for sqlite & like for other platforms).
User.where(User.arel_table[:name].does_not_match('%Gabe%'))
You could also implement this as a scope to contain the implementation to the model:
class User < ActiveRecord::Base
scope :not_matching,
-> (str) { where(arel_table[:name].does_not_match("%#{str}%")) }
end
Unfortunately ActiveRecord does not have a like query builder. I agree that the raw 'NOT LIKE' leaves a lot to be desired; you could make it a scope (scope :not_like, (column, val) -> { ... }), but AR itself does not do this.
Just addition to the answer of "where.not" of active record. "where.not" will exclude null values also. i.e. Query User.where.not(name: 'Gabe') will leave record with name 'Gabe' but it also exclude name column with NULL values. So in this scenario the solution would be
User.where.not(name: 'Gabe')
.or(User.where(name: nil))

How can I match a partial string to a database's object's attribute? Regexp?

I have a database containing a list of movies. A typical entry look like this:
id: 1,
title: "Manhatten and the Murderer",
year: 1928,
synopsis: 'some text...'
rating: 67,
genre_id, etc. etc.
Now I'm trying to make a series of search tests pass and so far I have made a single test case pass where if you type the title "Manhatten and the Murderer" in a text field it will find the movie that you want. The problem is with partial matching.
Now I'd like a way to search "Manhat" and match the record "Manhatten and the Murderer". I also want it to match with any movie that has "Manhat" in it. For example, it would return maybe 2 or 3 others like title: "My life in Manhattan", title: "The Big Apple in Manhattan" etc. etc.
Below is the code that I have so far in my Movie model:
def self.search(query)
# Replace this with the appropriate ActiveRecord calls...
if query =~ where(title:)
#where(title: query)
binding.pry
end
end
My question is, how can I set this up? My problem is the "where(title:) line. One thought was to use Regexp to match the title attribute. Any help would be appreciated! Thanks.
Use a query that searches a substring in between:
name = "Manhattan"
Movie.where("title like ?", "%#{name}%")
For example:
%Manhattan will get you: Love in Manhattan
Manhattan% will get: Manhattan and Company
%Manhattan% will get you both: [Love in Manhattan, Manhattan and Company]
But, if you're searching through movies synopsis, you should use Thinking Sphinx or Elastic Search
For example, with Elastic Search, you could set the synopsis like this:
Add app/indices/movie_index.rb:
ThinkingSphinx::Index.define :movie, :with => :active_record do
# fields
indexes title, :sortable => true
indexes synopsis
end
Index your data with rake ts:index
And then run Sphynx with: rake ts:start
You can search just like this:
Movie.search :conditions => {:synopsis => "Manhattan"}
Elastic Search is a great alternative to ThinkingSphinx, there's even a RailsCast about it, so you should definitely take a look to see what really suites you best... Hope this helps!
You do not need regex to find movies that have the search string. You can use SQL query like this:
Movie.where('title LIKE ?','Batman%')
That would return all movies start with "Batman"
Movie.where('title LIKE ?','%Batman%')
That would return all movies that have Batman anywhere in it's title.
I think you figured out the '%' is a joker character in the query.
One option is to run a search server alongside your Rails application. It is certainly my go to solution. This route offers a ton of features not found within Rails itself and might be overkill, but worth consideration.
I use Sphinx and implement it using the thinking-sphinx gem.
Resources:
http://pat.github.io/thinking-sphinx/
http://sphinxsearch.com/

Postgres accent insensitive LIKE search in Rails 3.1 on Heroku

How can I modify a where/like condition on a search query in Rails:
find(:all, :conditions => ["lower(name) LIKE ?", "%#{search.downcase}%"])
so that the results are matched irrespective of accents? (eg métro = metro). Because I'm using utf8, I can't use "to_ascii". Production is running on Heroku.
Proper solution
Since PostgreSQL 9.1 you can just:
CREATE EXTENSION unaccent;
Provides a function unaccent(), doing what you need (except for lower(), just use that additionally if needed). Read the manual about this extension.
More about unaccent and indexes:
Does PostgreSQL support "accent insensitive" collations?
Poor man's solution
If you can't install unacccent, but are able to create a function. I compiled the list starting here and added to it over time. It is comprehensive, but hardly complete:
CREATE OR REPLACE FUNCTION lower_unaccent(text)
RETURNS text
LANGUAGE sql IMMUTABLE STRICT AS
$func$
SELECT lower(translate($1
, '¹²³áàâãäåāăąÀÁÂÃÄÅĀĂĄÆćčç©ĆČÇĐÐèéêёëēĕėęěÈÊËЁĒĔĖĘĚ€ğĞıìíîïìĩīĭÌÍÎÏЇÌĨĪĬłŁńňñŃŇÑòóôõöōŏőøÒÓÔÕÖŌŎŐØŒř®ŘšşșߊŞȘùúûüũūŭůÙÚÛÜŨŪŬŮýÿÝŸžżźŽŻŹ'
, '123aaaaaaaaaaaaaaaaaaacccccccddeeeeeeeeeeeeeeeeeeeeggiiiiiiiiiiiiiiiiiillnnnnnnooooooooooooooooooorrrsssssssuuuuuuuuuuuuuuuuyyyyzzzzzz'
));
$func$;
Your query should work like that:
find(:all, :conditions => ["lower_unaccent(name) LIKE ?", "%#{search.downcase}%"])
For left-anchored searches, you can use an index on the function for very fast results:
CREATE INDEX tbl_name_lower_unaccent_idx
ON fest (lower_unaccent(name) text_pattern_ops);
For queries like:
SELECT * FROM tbl WHERE (lower_unaccent(name)) LIKE 'bob%';
Or use COLLATE "C". See:
PostgreSQL LIKE query performance variations
Is there a difference between text_pattern_ops and COLLATE "C"?
For those like me who are having trouble on add the unaccent extension for PostgreSQL and get it working with the Rails application, here is the migration you need to create:
class AddUnaccentExtension < ActiveRecord::Migration
def up
execute "create extension unaccent"
end
def down
execute "drop extension unaccent"
end
end
And, of course, after rake db:migrate you will be able to use the unaccent function in your queries: unaccent(column) similar to ... or unaccent(lower(column)) ...
First of all, you install postgresql-contrib. Then you connect to your DB and execute:
CREATE EXTENSION unaccent;
to enable the extension for your DB.
Depending on your language, you might need to create a new rule file (in my case greek.rules, located in /usr/share/postgresql/9.1/tsearch_data), or just append to the existing unaccent.rules (quite straightforward).
In case you create your own .rules file, you need to make it default:
ALTER TEXT SEARCH DICTIONARY unaccent (RULES='greek');
This change is persistent, so you need not redo it.
The next step would be to add a method to a model to make use of this function.
One simple solution would be defining a function in the model. For instance:
class Model < ActiveRecord::Base
[...]
def self.unaccent(column,value)
a=self.where('unaccent(?) LIKE ?', column, "%value%")
a
end
[...]
end
Then, I can simply invoke:
Model.unaccent("name","text")
Invoking the same command without the model definition would be as plain as:
Model.where('unaccent(name) LIKE ?', "%text%"
Note: The above example has been tested and works for postgres9.1, Rails 4.0, Ruby 2.0.
UPDATE INFO
Fixed potential SQLi backdoor thanks to #Henrik N's feedback
There are 2 questions related to your search on the StackExchange:
https://serverfault.com/questions/266373/postgresql-accent-diacritic-insensitive-search
But as you are on Heroku, I doubt this is a good match (unless you have a dedicated database plan).
There is also this one on SO: Removing accents/diacritics from string while preserving other special chars.
But this assumes that your data is stored without any accent.
I hope it will point you in the right direction.
Assuming Foo is the model you are searching against and name is the column. Combining Postgres translate and ActiveSupport's transliterate. You can do something like:
Foo.where(
"translate(
LOWER(name),
'âãäåāăąÁÂÃÄÅĀĂĄèééêëēĕėęěĒĔĖĘĚìíîïìĩīĭÌÍÎÏÌĨĪĬóôõöōŏőÒÓÔÕÖŌŎŐùúûüũūŭůÙÚÛÜŨŪŬŮ',
'aaaaaaaaaaaaaaaeeeeeeeeeeeeeeeiiiiiiiiiiiiiiiiooooooooooooooouuuuuuuuuuuuuuuu'
)
LIKE ?", "%#{ActiveSupport::Inflector.transliterate("%qué%").downcase}%"
)

Remove a 'where' clause from an ActiveRecord::Relation

I have a class method on User, that returns applies a complicated select / join / order / limit to User, and returns the relation. It also applies a where(:admin => true) clause. Is it possible to remove this one particular where statement, if I have that relation object with me?
Something like
User.complex_stuff.without_where(:admin => true)
I know this is an old question, but since rails 4 now you can do this
User.complex_stuff.unscope(where: :admin)
This will remove the where admin part of the query, if you want to unscope the whole where part unconditinoally
User.complex_stuff.unscope(:where)
ps: thanks to #Samuel for pointing out my mistake
I haven't found a way to do this. The best solution is probably to restructure your existing complex_stuff method.
First, create a new method complex_stuff_without_admin that does everything complex_stuff does except for adding the where(:admin => true). Then rewrite the complex_stuff method to call User.complex_stuff_without_admin.where(:admin => true).
Basically, just approach it from the opposite side. Add where needed, rather than taking away where not needed.
This is an old question and this doesn't answer the question per say but rewhere is a thing that exists.
From the documentation:
Allows you to change a previously set where condition for a given attribute, instead of appending to that condition.
So something like:
Person.where(name: "John Smith", status: "live").rewhere(name: "DickieBoy")
Will output:
SELECT `people`.* FROM `people` WHERE `people`.`name` = 'DickieBoy' AND `people`.`status` = 'live';
The key point being that the name column has been overwritten, but the status column has stayed.
You could do something like this (where_values holds each where query; you'd have to tweak the SQL to match the exact output of :admin => true on your system). Keep in mind this will only work if you haven't actually executed the query yet (i.e. you haven't called .all on it, or used its results in a view):
#users = User.complex_stuff
#users.where_values.delete_if { |query| query.to_sql == "\"users\".\"admin\" = 't'" }
However, I'd strongly recommend using Emily's answer of restructuring the complex_stuff method instead.
I needed to do this (Remove a 'where' clause from an ActiveRecord::Relation which was being created by a scope) while joining two scopes, and did it like this: self.scope(from,to).values[:joins].
I wanted to join values from the two scopes that made up the 'joined_scope' without the 'where' clauses, so that I could add altered 'where' clauses separately (altered to use 'OR' instead of 'AND').
For me, this went in the joined scope, like so:
scope :joined_scope, -> (from, to) {
joins(self.first_scope(from,to).values[:joins])
.joins(self.other_scope(from,to).values[:joins])
.where(first_scope(from,to).ast.cores.last.wheres.inject{|ws, w| (ws &&= ws.and(w)) || w}
.or(other_scope(from,to).ast.cores.last.wheres.last))
}
Hope that helps someone

Resources