Rails模型中不区分大小写的搜索


211

我的产品型号包含一些物品

 Product.first
 => #<Product id: 10, name: "Blue jeans" >

我现在要从另一个数据集中导入一些产品参数,但是名称的拼写不一致。例如,在另一个数据集中,Blue jeans可以拼写为Blue Jeans

我想这样做Product.find_or_create_by_name("Blue Jeans"),但这将创建一个新产品,几乎与第一个相同。如果我想查找和比较小写的名字,该怎么办?

性能问题在这里并不是很重要:只有100-200种产品,我想将此作为导入数据的迁移来运行。

有任何想法吗?

Answers:


368

您可能需要在这里更加详细

name = "Blue Jeans"
model = Product.where('lower(name) = ?', name.downcase).first 
model ||= Product.create(:name => name)

5
@botbot的注释不适用于用户输入的字符串。“#$$”是鲜为人知的捷径,用于使用Ruby字符串插值转义全局变量。等效于“#{$$}”。但是,字符串插入不会发生在用户输入的字符串上。在Irb中尝试这些以查看区别:"$##"'$##'。第一个是插值的(双引号)。第二个不是。用户输入永远不会被插值。
Brian Morearty 2013年

5
只是要注意,find(:first)已弃用,现在可以使用#first。因此,Product.first(conditions: [ "lower(name) = ?", name.downcase ])
路易斯·拉马略

2
您无需完成所有这些工作。使用内置的Arel库或Squeel
Dogweather 2013年

17
在Rails 4中,您现在可以执行model = Product.where('lower(name) = ?', name.downcase).first_or_create
Derek Lucas,

1
@DerekLucas,尽管可以在Rails 4中这样做,但这种方法可能会导致意外的行为。假设我们after_createProduct模型中有回调,并且在回调内部有where子句,例如products = Product.where(country: 'us')。在这种情况下,where子句被链接起来,因为回调在范围内执行。仅供参考。
elquimista'3

100

这是Rails中的完整设置,仅供我参考。如果它也对您有帮助,我很高兴。

查询:

Product.where("lower(name) = ?", name.downcase).first

验证者:

validates :name, presence: true, uniqueness: {case_sensitive: false}

索引(Rails / ActiveRecord中不区分大小写的唯一索引的答案):

execute "CREATE UNIQUE INDEX index_products_on_lower_name ON products USING btree (lower(name));"

我希望有一个更漂亮的方法来做第一个和最后一个,但是再说一次,Rails和ActiveRecord是开源的,我们不应该抱怨-我们可以自己实现它并发送请求请求。


6
感谢您在PostgreSQL中创建不区分大小写的索引。感谢您展示如何在Rails中使用它!还有一点要注意:如果使用标准查找程序,例如find_by_name,它仍然会完全匹配。如果要使搜索不区分大小写,则必须编写自定义查找器,类似于上面的“查询”行。
马克·贝里

考虑到 find(:first, ...)现在已弃用,我认为这是最合适的答案。
用户

需要name.downcase吗?它似乎与Product.where("lower(name) = ?", name).first
Jordan合作

1
@Jordan您是否尝试过使用大写字母命名?
oma 2014年

1
@Jordan,也许不太重要,但是我们应该在帮助其他人的同时争取SO的准确性:)
oma

28

如果使用的是Postegres and Rails 4+,则可以选择使用列类型CITEXT,这将允许不区分大小写的查询,而不必写出查询逻辑。

迁移:

def change
  enable_extension :citext
  change_column :products, :name, :citext
  add_index :products, :name, unique: true # If you want to index the product names
end

要进行测试,您应该期望以下几点:

Product.create! name: 'jOgGers'
=> #<Product id: 1, name: "jOgGers">

Product.find_by(name: 'joggers')
=> #<Product id: 1, name: "jOgGers">

Product.find_by(name: 'JOGGERS')
=> #<Product id: 1, name: "jOgGers">

21

您可能要使用以下内容:

validates_uniqueness_of :name, :case_sensitive => false

请注意,默认情况下,设置为:case_sensitive => false,因此,如果您未进行其他更改,则甚至无需编写此选项。

在以下位置找到更多信息:http : //api.rubyonrails.org/classes/ActiveRecord/Validations/ClassMethods.html#method-i-validates_uniqueness_of


5
根据我的经验,与文档相反,case_sensitive默认为true。我已经看到在postgresql中的行为和其他在mysql中报告的行为相同。
Troy 2012年

1
所以我正在尝试使用postgres,但它不起作用。find_by_x区分大小写,无论如何...
Louis Sayers

仅在创建模型时才进行此验证。因此,如果您的数据库中有“ HAML”,并且您尝试添加“ haml”,它将不会通过验证。
2013年

14

在postgres中:

 user = User.find(:first, :conditions => ['username ~* ?', "regedarek"])

1
在Heroku上使用Rails,因此使用Postgres…ILIKE非常出色。谢谢!
FeifanZ

绝对在PostgreSQL上使用ILIKE。
Dom 2015年

12

有几条评论提到了Arel,但没有提供示例。

这是一个不区分大小写的搜索的Arel示例:

Product.where(Product.arel_table[:name].matches('Blue Jeans'))

这种解决方案的优点是它与数据库无关,它将为您当前的适配器使用正确的SQL命令(matchesILIKE用于Postgres和LIKE其他所有东西)。


9

引用SQLite文档

其他任何字符都匹配其自身或等效的小写/大写字母(即,不区分大小写的匹配)

...我不知道。但是它起作用了:

sqlite> create table products (name string);
sqlite> insert into products values ("Blue jeans");
sqlite> select * from products where name = 'Blue Jeans';
sqlite> select * from products where name like 'Blue Jeans';
Blue jeans

因此,您可以执行以下操作:

name = 'Blue jeans'
if prod = Product.find(:conditions => ['name LIKE ?', name])
    # update product or whatever
else
    prod = Product.create(:name => name)
end

#find_or_create,我知道,它可能不是非常适合跨数据库的,但是值得一看吗?


1
like在mysql中区分大小写,但在postgresql中不区分大小写。我不确定Oracle或DB2。关键是,您不能指望它,并且如果您使用它并且老板更改了基础数据库,您将开始拥有“丢失”记录,而没有明显的原因。@neutrino的lower(name)建议可能是解决此问题的最佳方法。
masukomi 2011年

6

没有人提到的另一种方法是将不区分大小写的查找器添加到ActiveRecord :: Base中。详细信息可以在这里找到。这种方法的优点是您不必修改每个模型,也不lower()必将子句添加到所有不区分大小写的查询中,而只需使用其他finder方法。


当您链接的页面消失时,您的答案也将消失。
安东尼

正如@Anthony的预言一样,它也已通过。链接已死。
XP84

3
@ XP84我不知道这有多重要,但是我已经修复了链接。
亚历克斯·科本

6

大写和小写字母仅相差一个位。搜索它们的最有效方法是忽略此位,而不转换低位或高位等。请参见COLLATIONMSSQL的关键字,请查看NLS_SORT=BINARY_CI是否使用Oracle等。


4

现在不建议使用Find_or_create,您应该改用AR关系加上first_or_create,如下所示:

TombolaEntry.where("lower(name) = ?", self.name.downcase).first_or_create(name: self.name)

这将返回第一个匹配的对象,如果不存在,则为您创建一个。



2

这里有很多很棒的答案,尤其是@ oma's。但是您可以尝试的另一件事是使用自定义列序列化。如果您不介意所有内容都以小写形式存储在数据库中,则可以创建:

# lib/serializers/downcasing_string_serializer.rb
module Serializers
  class DowncasingStringSerializer
    def self.load(value)
      value
    end

    def self.dump(value)
      value.downcase
    end
  end
end

然后在您的模型中:

# app/models/my_model.rb
serialize :name, Serializers::DowncasingStringSerializer
validates_uniqueness_of :name, :case_sensitive => false

这种方法的好处是,您仍然可以使用所有常规查找器(包括find_or_create_by),而无需使用自定义范围,函数或lower(name) = ?在查询中。

缺点是您会丢失数据库中的大小写信息。


2

类似于安德鲁斯,它是第一名:

对我有用的是:

name = "Blue Jeans"
Product.find_by("lower(name) = ?", name.downcase)

这样就无需在同一查询中执行#where#first。希望这可以帮助!


1

您还可以使用下面的范围,将它们放在一个关注范围内,并包括在您可能需要的模型中:

scope :ci_find, lambda { |column, value| where("lower(#{column}) = ?", value.downcase).first }

然后像这样使用: Model.ci_find('column', 'value')




0

有些人显示使用LIKE或ILIKE,但是那些人允许正则表达式搜索。另外,您不需要在Ruby中进行小写转换。您可以让数据库为您完成。我认为可能会更快。也first_or_create可以在之后使用where

# app/models/product.rb
class Product < ActiveRecord::Base

  # case insensitive name
  def self.ci_name(text)
    where("lower(name) = lower(?)", text)
  end
end

# first_or_create can be used after a where clause
Product.ci_name("Blue Jeans").first_or_create
# Product Load (1.2ms)  SELECT  "products".* FROM "products"  WHERE (lower(name) = lower('Blue Jeans'))  ORDER BY "products"."id" ASC LIMIT 1
# => #<Product id: 1, name: "Blue jeans", created_at: "2016-03-27 01:41:45", updated_at: "2016-03-27 01:41:45"> 


-9

到目前为止,我已经使用Ruby提出了解决方案。将其放入产品模型中:

  #return first of matching products (id only to minimize memory consumption)
  def self.custom_find_by_name(product_name)
    @@product_names ||= Product.all(:select=>'id, name')
    @@product_names.select{|p| p.name.downcase == product_name.downcase}.first
  end

  #remember a way to flush finder cache in case you run this from console
  def self.flush_custom_finder_cache!
    @@product_names = nil
  end

这将给我第一个名称匹配的产品。还是没有

>> Product.create(:name => "Blue jeans")
=> #<Product id: 303, name: "Blue jeans">

>> Product.custom_find_by_name("Blue Jeans")
=> nil

>> Product.flush_custom_finder_cache!
=> nil

>> Product.custom_find_by_name("Blue Jeans")
=> #<Product id: 303, name: "Blue jeans">
>>
>> #SUCCESS! I found you :)

2
对于较大的数据集,这是极其低效的,因为它必须将整个内容加载到内存中。虽然只有几百个条目对您来说不是问题,但这不是一个好习惯。
lambshaanxy 2011年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.