Search for substring in two columns - ruby-on-rails

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

Related

String concatenation of two attributes in Rails where query

Let's say I have the attrs first_name and last_name for a User. I know the user's full name is "Johnny Appleseed", as I have a large Hash of user's full names as strings.
Sure, I could split each string first, and search like so:
User.where(first_name: 'Johnny', last_name: "Appleseed")
BUT, I'm wondering if there is a way to basically concat the two in a query, essentially like so:
User.where('first_name + last_name = ?', 'Johnny Appleseed')
Or if I could have a full_name method on the User model, and search by that?
def full_name
"#{self.first_name} #{self.last_name}"
end
User.where('full_name = ?', 'Johnny Appleseed')
Probably the easiest solution would be:
User.where(%q(concat_ws(' ', first_name, last_name) = ?), 'Johnny Appleseed')

Find all records where first_name column does NOT start with A-Z

I need to extract a subset of results where the value for the first_name column starts with anything other than standard English alphabet letters (i.e. A-Z), for example È. I am banging my head against a brick wall with this - for example, this is how I've been able to get all the records starting with the letter A:
#results = User.where("first_name LIKE ?", "A%").order("first_name ASC")
Any help would be wonderful.
EDIT: I am using a PostgreSQL database.
You can use ~ operator to match regular expression. ~ is used to match case sensitive data. So in your case first_name ~ '^[^A-Z]' will give users whose first_name doesn't start with A-Z. To match case insensitive you need to use ~*.
#results = User.where("first_name ~ ?", "^[^A-Z]").order("first_name ASC")
or
You can also use !~ which gives the unmatched results (i.e.) first_name !~ ^[A-Z] equivalent to first_name ~ ^[^A-Z]. To match case insensitive use !~*.
#results = User.where("first_name !~ ?", "^[A-Z]").order("first_name ASC")
For more information see documentation
This should work in Postgresql
#results = User.where("first_name SIMILAR TO ?", "[A-Z]%")
More info here.

Rails and Postgreql Concatenate space

I have an search field for user with autocomplete, it was working but I migrated my database from mySQL to Postgresql but cannot get the concatenate working. When I type a first name or a last name is working but when my user are typing Firstname Lastname (with a space between them) it's not working anymore and cannot get it working or figure out what to do.
That's my SQL command :
#users = User.find(:all,:conditions => ['(last_name LIKE ? OR first_name LIKE ? OR (first_name || last_name) LIKE ? OR (last_name || first_name) LIKE ?) AND adult = ?', "%#{params[:term]}%", "%#{params[:term]}%", "%#{params[:term]}%", "%#{params[:term]}%", false])
My issue is here : (first_name || last_name) that will return correct value when the user type FirstNameLastName so the concat works here but when I try to add the space I cannot figure out what is wrong I tried :
(first_name ||' '|| last_name) or (first_name ||" "|| last_name)
But none of these will work I found another thread on stackoverflow but cannot get it working and I don't understand why...
Thanks,
No idea about Rails but how about
'....(first_name || \' \' || last_name)...'

Rails ActiveRecord - Search on Multiple Attributes

I'm implementing a simple search function that should check for a string in either the username, last_name and first_name. I've seen this ActiveRecord method on an old RailsCast:
http://railscasts.com/episodes/37-simple-search-form
find(:all, :conditions => ['name LIKE ?', "%#{search}%"])
But how do I make it so that it searches for the keyword in name, last_name and first name and returns the record if the one of the fields matched the term?
I'm also wondering if the code on the RailsCast is prone to SQL injections?
Thanks a lot!
I assumed your model name is Model - just replace it with your real model name when you do the actual query:
Model.where("name LIKE ? OR last_name LIKE ? OR first_name LIKE ?", "%#{search}%","%#{search}%","%#{search}%")
About your worries about SQL injections - both of code snippets are immune to SQL injections. As long as you do not directly embed strings into your WHERE clause you are fine. An example for injection-prone code would be:
Model.where("name LIKE '#{params[:name]}'")
Although the selected answer will work, I noticed that it breaks if you try to type a search "Raul Riera" because it will fail on both cases, because Raul Riera is not either my first name or my last name.. is my first and last name... I solved it by doing
Model.where("lower(first_name || ' ' || last_name) LIKE ?", "%#{search.downcase}%")
With Arel, you can avoid writing the SQL manually with something like this:
Model.where(
%i(name first_name last_name)
.map { |field| Model.arel_table[field].matches("%#{query}%")}
.inject(:or)
)
This would be particularly useful if the list of fields to match against was dynamic.
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
The best way to do this is:
Model.where("attr_a ILIKE :query OR attr_b ILIKE :query", query: "%#{query}%")

Rails gem rails3-jquery-autocomplete: How do I query multiple fields

I'm using the rails3-jquery-autocomplete gem found here: http://github.com/crowdint/rails3-jquery-autocomplete
The instructions are clear for how to query a single attribute of a model and I am able to make that work without a problem.
My Person model has two attributes that I would like to combine and query, however. They are first_name and last_name. I would like to combine them into a pseudo-attribute called full_name. Currently, I receive this error:
ActiveRecord::StatementInvalid (SQLite3::SQLException: no such column: full_name: SELECT "people".* FROM "people" WHERE (LOWER(full_name) LIKE 'cla%') ORDER BY full_name ASC LIMIT 10):
There is no full_name attribute of the Person model, though I have the following method in the Person model file:
def full_name
"#{self.first_name} #{self.last_name}"
end
How do I modify the Person model file so that calls to full_name queries the database to match a combination of first_name and last_name?
Your pseudo attribute works only on records already retrieved, but it has no bearing on searching for records. Probably the easiest solution is a named named scope like:
scope :search_by_name, lambda { |q|
(q ? where(["first_name LIKE ? or last_name LIKE ? or concat(first_name, ' ', last_name) like ?", '%'+ q + '%', '%'+ q + '%','%'+ q + '%' ]) : {})
}
Thus, a call like:
Person.search_by_name(params[:q])
will return an appropriate result set. It will also return all entries if no param was passed (or more specifically, this scope will add nothing extra), making it an easy drop-in for the index action.
Sadly, the scope method mentioned above didn't work for me. My solution was to simply overwrite the get_autocomplete_items method (formerly get_items).
For what it's worth, there is a MySQL function (other db's have it as well, but we're talking MySQL for the moment) that is better suited to the type of concatenation you're using:
def get_autocomplete_items(parameters)
items = Contact.select("DISTINCT CONCAT_WS(' ', first_name, last_name) AS full_name, first_name, last_name").where(["CONCAT_WS(' ', first_name, last_name) LIKE ?", "%#{parameters[:term]}%"])
end
MySQL's CONCAT_WS() is intended to join strings together with some sort of separator and is ideal for full names.
This chunk of code basically says return the "first last" formatted names of contacts that match whatever the user is searching by when we pair up the database's contact records by a concatenated pairs of first and last names. I feel it's better than the SQL statement above since it does a full search that will match first AND/OR last name in one statement, not three OR statements.
Using this "hn sm" would match "John Smith" since indeed "hm sm" is LIKE "John Smith". Furthermore, it has the added benefit of also returning the concatenated first and last name of each contact. You may want the full record. If that's the case, remove the select() query from the line above. I personally had the need for the user to search for a name and have an autocomplete field return all possible matches, not the records.
I know this is a bit late, but I hope it helps someone else!
Full implementation of multiple-field autocomplete :
Following my comment, my solution to integrate into jquery-autocomplete was to have a custom implementation of the "internal" autocomplete.
1. Query the database
If you're using ActiveRecord, you can use DGM's solution for your named_scope
If you're using Mongoid, you can use this syntax instead:
scope :by_first_name, ->(regex){
where(:first_name => /#{Regexp.escape(regex)}/i)
}
scope :by_last_name, ->(regex){
where(:last_name => /#{Regexp.escape(regex)}/i)
}
scope :by_name, ->(regex){
any_of([by_first_name(regex).selector, by_last_name(regex).selector])
}
EDIT : if you want a stronger autocomplete that can handle accents, matching parts of text, etc; you should try the mongoid text index. Credits to the original answer there
index(first_name: 'text', last_name: 'text')
scope :by_name, ->(str) {
where(:$text => { :$search => str })
}
And don't forget to build the indexes after adding that rake db:mongoid:create_indexes
So you can basically do User.by_name(something)
2. Create an autocomplete action in your controller
Because the one provided by jquery-autocomplete... ain't gonna do what we want.
Note that you'll have to convert the result to JSON so it can be used in the frontend jquery-autocomplete. For this I have chosen to use the gem ActiveModel::Serializer, but feel free to use something else if you prefer, and skip step 3
In your controller :
def autocomplete
#users = User.by_name(params[:term])
render json: #users, root: false, each_serializer: AutocompleteSerializer
end
3. Reformat the response
Your serializer using the gem activemodel:
I provided the link to the 0.9 version, as the master mainpage doesn't contain the full documentation.
class AutocompleteSerializer < ActiveModel::Serializer
attributes :id, :label, :value
def label
object.name
# Bonus : if you want a different name instead, you can define an 'autocomplete_name' method here or in your user model and use this implementation
# object.respond_to?('autocomplete_name') ? object.autocomplete_name : object.name
end
def value
object.name
end
4. Create a route for your autocompletion
In your routes :
get '/users/autocomplete', to: 'users#autocomplete', as: 'autocomplete_user'
5. Have fun in your views
Finally in your views you can use the default syntax of jquery-rails, but remember to change the path !
<%= form_tag '' do
autocomplete_field_tag 'Name', '', autocomplete_user_path, :id_element => "#{your_id}", class: "form-control"
end %>
RQ : I used some 2-level deep nested forms, so it was a bit tricky to get the right id element your_id. In my case I had to do somethig complicated, but most likely it will be simple for you. You can always have a look at the generated DOM to retrieve the field ID
This is a hack and I would very much like this function to be included in the gem but as slant said overwriting the get_autocomplete_items works as he wrote it but it will only return first_name and last_name from the model column. In order to restore functionality that frank blizzard asked for you also need to return the id of the row.
items = Contact.select("DISTINCT CONCAT_WS(' ', first_name, last_name) AS full_name, first_name, last_name, id").where(["CONCAT_WS(' ', first_name, last_name) LIKE ?", "%#{parameters[:term]}%"])
The difference between mine and slant's answer is id as the last argument of the select method. I know it is late but I hope it helps somebody in the future.
If you don't need the functionality of comparing your search string against the concatenated string and are trying to just do a query on three separate columns you can use this fork of the gem : git://github.com/slash4/rails3-jquery-autocomplete.git. Oh also that fork will only work with ActiveRecord which is probably why they didn't pull it.
To perform a case-insensitive search using #Cyril's method for rails4-autocomplete, I did a slight modification to the named scope #DGM provided
scope :search_by_name, lambda { |q|
q.downcase!
(q ? where(["lower(first_name) LIKE ? or lower(last_name) LIKE ? or concat(lower(first_name), ' ', lower(last_name)) like ?", '%'+ q + '%', '%'+ q + '%','%'+ q + '%' ]) : {})
}
This converts the record's entry to lowercase and also converts the search string to lowercase as it does the comparison
An alternative to the previous suggestions is to define a SQL view on the table holding first_name and last_name. Your SQL code (in SQLite) might look like:
CREATE VIEW Contacts AS
SELECT user.id, ( user.first_name || ' ' || users.last_name ) AS fullname
FROM persons;
Then define a model for the table:
class Contact < ActiveRecord::Base
set_table_name "contacts"
end
You can now use rails3-jquery-autocomplete 'out-of-the box'. So in your controller you would write:
autocomplete :contact, :fullname
In your view file you can simply write:
f.autocomplete_field :contact, autocomplete_contact_fullname_path
I will leave configuration of the route as an exercise for the reader :-).

Resources