I'm trying to create a 'search box' that matches users by name.
The difficulty is that a user has both a firstname and a surname. Each of those can have spaces in them (eg "Jon / Bon Jovi", or "Neil Patrick / Harris"), and I'm wondering about the most efficient way to ensure the search is carried out on a concatenation of both the firstname and surname fields.
The list of users is quite large, so performance is a concern. I could just throw a "fullname" def in the user model, but I suspect this isn't the wisest move performance wise. My knowledge of multi-column rails indexes is weak, but I suspect there's a way of doing it via an index with a " " in it?
Just to clarify, I don't need fuzzy matching - exact match only is fine...I just need it to be run on a concatenation of two fields.
Cheers...
You could create a new field in your database called full_name with a regular index, then use a callback to populate this whenever the record is saved/updated:
before_save :populate_full_name
protected
def populate_full_name
self.full_name = "#{first_name} #{last_name}"
end
If you can modify the database, you can and should use the solution provided by gjb.
Here is the solution that does not require you to alter the database. Simply gather all the possible first-name/last-name pairs you can get from the search box. Some code:
# this method returns an array of first/last name pairs given the string
# it returns nil when the string does not look like a proper name
# (i.e. "Foo Bar" or "Foo Bar Baz", but not "Foo" or "Foo "
def name_pairs(string)
return nil unless string =~ /^\w+(\s+\w+)+$/
words = string.split(/\s+/) # split on spaces
result = []
# in the line below: note that there is ... and .. in the ranges
1.upto(words.size-1) {|n| result << [words[0...n], words[n..-1]]}
result.collect {|f| f.collect {|nm| nm.join(" ")}}
end
This method gives you an array of two-element arrays, which you can use to create an or query. Here is how the method looks:
#> name_pairs("Jon Bon Jovi")
=> [["Jon", "Bon Jovi"], ["Jon", "Bon Jovi"]]
#> name_pairs("John Bongiovi")
=> [["John", "Bongiovi"]]
#> name_pairs("jonbonjovi")
=> nil
Of course, this method is not perfect (it does not capitalise the names, but you can do it after splitting) and is probably not optimal in terms of speed, but it works. You can also reopen String and add the method there, so you can go with "Jon Bon Jovi".name_pairs.
Related
A user has languages, which is an array of language names. I have this method:
def get_users_by_filtered_langauges(lang)
users = []
User.all.each do |u|
u.languages.each do |l|
users << u if lang.include?(l)
end
end
which takes lang, an array of language names, and returns the users whose languages include some language within lang. For example, if first_user has languages ["uk", "us"] and lang is ["uk"], then the method's return value includes first_user.
Is there a way to minimize this and write a SQL query in a simpler way?
Selecting all records is always an anti-pattern. One should select only records of interest. If you have problems writing the query selecting only records of interest, you should revise your database structure.
write a SQL query in a simple way
That is relatively easy. For single lang to check it would be:
%q|SELECT * FROM users WHERE languages LIKE "%#{lang}%"|
Even using all, your method is abusing each for reducing the input.
In ruby it should be written as:
def get_users_by_filtered_langauges(*langs)
# single lang
# User.all.reject { |u| u.languages.include?(lang) }
# disjunction
User.all.reject { |u| (u.languages & langs).empty? }
end
#mudasobwa's answer is right. But you could also do like this.
User.where("languages like '%?%'", lang)
Well, I have the next Rails scope, when given a word I found the companies that match that word either on the name, description, city or in any of the products related with the company.
includes([:products, {city: :department}]).where("unaccent(LOWER(companies.name))
ILIKE('%#{term.parameterize.underscore.humanize.downcase}%')
OR unaccent(LOWER(companies.description))
ILIKE('%#{term.parameterize.underscore.humanize.downcase}%')
OR unaccent(LOWER(cities.name))
ILIKE('%#{term.parameterize.underscore.humanize.downcase}%')
OR unaccent(LOWER(products.name))
ILIKE('%#{term.parameterize.underscore.humanize.downcase}%')"
.gsub("ñ","n")).references(:products, :city, :department)
This works just fine, but know I need to know in which (name, description, city or products) was the coincidence found.
I have thought in the next solutions but I am not sure if is efficient or good enough.
Solution.
Separate the scope in 4 different queries, then use a loop and an aux column to fill with something like "Coincidence founds in, name and description" on each different query.
then use something like this
query1 | query2 | query3 | query4 # to merge the arrays
For the record my Rails app is using Postgres 9.4
I think you have a good start by separating your results into four queries.
When you merge them, you want to maintain a way to see which query it came from.
If you're fine with your results being hashes, you can do this:
results = []
results.concat query1.map { |record| record.attributes.merge(query: "query1") }
results.concat query2.map { |record| record.attributes.merge(query: "query2") }
# etc
If you want your results to be active record objects, you can add a virtual attribute and do something similar
# in the model, add a virtual attribute (not stored in db)
attr_accessor :query
# in the controller
records = []
records.concat query1.map { |record| record.query = "query1"; record}
records.concat query2.map { |record| record.query = "query2"; record}
# etc.
I use PostgreSQL's full text search capabilities, which works fine. All the relevant columns are indexed so it's nice and speedy:
def self.text_search(query)
if (query.present?)
# search(query)
where(
"to_tsvector('english', title) ## plainto_tsquery(:q)",
q: query
)
else
scoped
end
end
But now I also want to search through related abbreviations:
def self.text_search(query)
if (query.present?)
# search(query)
includes(:abbreviations).where(
"to_tsvector('english', articles.title) ## plainto_tsquery(:q)"+
" or to_tsvector('english', abbreviations.abbreviation) ## plainto_tsquery(:q)",
q: query
)
else
scoped
end
end
This works, but now my queries take 2.5+ seconds! How do I remedy this? I was thinking that maybe this is a Rails inefficiency, so I could best perform raw SQL. But how do I do that and still get back a ActiveRecord relation?
What I did as a workaround, added a str_* column to my main table, and update this column when an element is saved, and then search over that column:
before_validation(on: :create) do
self.str_abbreviations = join_abbreviations()
... etc ...
true
end
I have a form that displays inputs based on user preferences. I am storing the values as an hstore hash since I dont know ahead of time exactly what the form input for each user will be. The problem I am running in to is that even though a user has an input preferenced doesnt mean they have to enter a value for it each time. Which, can result in :foo => "".
All the doc examples show you how to find records you know the key name of. In my case, I dont know the key name...I need to find all the keys in a hash whose value => "".
Then, I should be able to do something like the docs shows...for each empty value
person.destroy_key(:data, :foo).destroy_key(:data, :bar).save
avals(hstore) is likely what I need to user... How do you use avals with rails?
Since hstore is just a hash in rails...you just need to evaluate the hash before saving it.
...in model
before_save :remove_blanks
private
def remove_blanks
self.hstore = self.hstore.reject{ |k,v| v.blank? }
end
replace 'hstore' with your hstore column name
I am trying to save a name to the database and a single word (firstname) works fine but when the user enter both firstname and lastname I want Rails to save it to the database as firstname-lastname instead of firstname lastname (space between).
I know I perhaps should use a before create filter but I am not sure how this need to look like. I want the validation to work to, i.e. no two people should be able to use the same name.
I am using Rails 3.
You can use ActiveSupport's inflector method parameterize on the string.
name = 'john smith' # => john smith
name.parameterize # => john-smith
Further, parameterize takes an option to use for the word-break, so you can replace the dash with an underscore like this:
name.parameterize("_") # => john_smith
An advantage of using parameterize is that it normalizes the characters to the latin, so...
name = "jöhanne såltveç"
name.parameterize # => johanne-saltvec
EDIT: As of Rails 5.0.0.1 the separator needs to be passed as an option. Therefore: name.parameterize(separator: '_')
Why don't you just have first_name and last_name columns in the db, and create your own validation rule to make sure the combination is unique (http://guides.rubyonrails.org/active_record_validations_callbacks.html#creating-custom-validation-methods). You should also create a unique index over those two columns in your db.
Another option would be to us regexp and replace all existing spaces with. You'd put something along the lines of:
self.firstname.gsub(/\s+/, '-')
in your model.
Note: I'm not sure if ruby accepts \s as "any whitespace character" And I think the * should make sure that if someone enters a name with two neighbour spaces(or more) it won't convert each space into a separate dash, but only into one.
Other answer is correct,
But, if you want to preserve case while parameterizing
name = "Donald Duck"
name.parameterize(preserve_case: true) # => Donald-Duck