简体   繁体   中英

Best way to combine first_name and last_name columns in Ruby on Rails 3?

I have a method in my User model:

def self.search(search)
  where('last_name LIKE ?', "%#{search}%")
end

However, it would be nice for my users to be able to search for both first_name and last_name within the same query.

I was thinking to create a virtual attribute like this:

def full_name
  [first_name, last_name].join(' ')
end

But is this efficient on a database level. Or is there a faster way to retrieve search results?

Thanks for any help.

Virtual attribute from your example is just class method and cannot be used by find-like ActiveRecord methods to query database.

Easiest way to retrive search result is modifying Search method:

def self.search(search)
  q = "%#{query}%"
  where("first_name + ' ' + last_name LIKE ? OR last_name + ' ' + first_name LIKE ?", [q, q])
end

where varchar concatenation syntax is compatible with your database of choice (MS SQL in my example).

The search functionality, in your example, is still going to run at the SQL level.

So, to follow your example, your search code might be:

def self.search_full_name(query)
  q = "%#{query}%"
  where('last_name LIKE ? OR first_name LIKE ?', [q, q])
end

NOTE -- these sorts of LIKE queries, because they have a wildcard at the prefix, will be slow on large sets of data, even if they are indexed.

One way this can be implemented is by tokenizing (splitting) the search query and creating one where condition per each token:

  def self.search(query)
    conds  = []
    params = {}
    query.split.each_with_index do |token, index|
      conds.push "first_name LIKE :t#{index} OR last_name LIKE :t#{index}"
      params[:"t#{index}"] = "%#{token}%"
    end

    where(conds.join(" OR "), params)
  end

Also make sure you prevent SQL injection attacks.

However, it's better to use full-text searching tools, such as ElasticSearch and its Ruby gem named Tire to handle searches.

EDIT: Fixed the code.

A scope can be made to handle complex modes, here's an example from one project I'm working on:

   scope :search_by_name, lambda { |q|
      if q
        case q
        when  /^(.+),\s*(.*?)$/
         where(["(last_name LIKE ? or maiden_name LIKE ?) AND (first_name LIKE ? OR common_name LIKE ? OR middle_name LIKE ?)",
           "%#{$1}%","%#{$1}%","%#{$2}%","%#{$2}%","%#{$2}%"
           ])
        when /^(.+)\s+(.*?)$/
          where(["(last_name LIKE ? or maiden_name LIKE ?) AND (first_name LIKE ? OR common_name LIKE ? OR middle_name LIKE ?)",
            "%#{$2}%","%#{$2}%","%#{$1}%","%#{$1}%","%#{$1}%"
            ])
        else
           where(["(last_name LIKE ? or maiden_name LIKE ? OR first_name LIKE ? OR common_name LIKE ? OR middle_name LIKE ?)",
             "%#{q}%","%#{q}%","%#{q}%","%#{q}%","%#{q}%"
             ])
        end
      else
       {}
      end
}

As you can see, I do a regex match to detect different patterns an build different searches depending on what is provided. As an added bonus, if nothing is provided, it returns an empty hash which effectively is where(true) and returns all results.

As mentioned elsewhere, the db cannot index the columns when a wildcard is used on both sides like %foo% , so this could potentially get slow on very large datasets.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM