Pencarian case-insensitive dalam model Rails


211

Model produk saya mengandung beberapa item

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

Saya sekarang mengimpor beberapa parameter produk dari dataset lain, tetapi ada ketidakkonsistenan dalam pengejaan nama. Misalnya, dalam dataset lain, Blue jeansbisa dieja Blue Jeans.

Saya ingin Product.find_or_create_by_name("Blue Jeans"), tetapi ini akan membuat produk baru, hampir identik dengan yang pertama. Apa pilihan saya jika saya ingin mencari dan membandingkan nama yang lebih kecil.

Masalah kinerja tidak terlalu penting di sini: Hanya ada 100-200 produk, dan saya ingin menjalankan ini sebagai migrasi yang mengimpor data.

Ada ide?

Jawaban:


368

Anda mungkin harus lebih banyak bicara di sini

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

5
Komentar @ botbot tidak berlaku untuk string dari input pengguna. "# $$" adalah jalan pintas yang tidak banyak diketahui untuk keluar dari variabel global dengan interpolasi string Ruby. Ini sama dengan "# {$$}". Tetapi interpolasi string tidak terjadi pada string input pengguna. Coba ini di Irb untuk melihat perbedaannya: "$##"dan '$##'. Yang pertama adalah interpolasi (tanda kutip ganda). Yang kedua bukan. Input pengguna tidak pernah diinterpolasi.
Brian Morearty

5
Hanya untuk catatan yang find(:first)sudah usang, dan opsi sekarang adalah menggunakan #first. Dengan demikian,Product.first(conditions: [ "lower(name) = ?", name.downcase ])
Luís Ramalho

2
Anda tidak perlu melakukan semua pekerjaan ini. Gunakan pustaka Arel bawaan atau Squeel
Dogweather

17
Di Rails 4 sekarang Anda dapat melakukannyamodel = Product.where('lower(name) = ?', name.downcase).first_or_create
Derek Lucas

1
@DerekLucas walaupun dimungkinkan untuk melakukannya di Rails 4, metode ini mungkin menyebabkan perilaku yang tidak terduga. Misalkan kita memiliki after_createcallback dalam Productmodel dan di dalam callback, kita memiliki whereklausa, misalnya products = Product.where(country: 'us'). Dalam hal ini, whereklausa dirantai ketika callback dieksekusi dalam konteks lingkup. Hanya FYI.
elquimista

100

Ini adalah pengaturan lengkap dalam Rails, untuk referensi saya sendiri. Saya senang jika itu membantu Anda juga.

pertanyaan:

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

validator:

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

indeks (jawaban dari indeks unik Case-insensitive di Rails / ActiveRecord? ):

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

Saya berharap ada cara yang lebih indah untuk melakukan yang pertama dan yang terakhir, tetapi sekali lagi, Rails dan ActiveRecord adalah open source, kita tidak boleh mengeluh - kita bisa menerapkannya sendiri dan mengirim permintaan tarik.


6
Terima kasih atas kreditnya membuat indeks case-insensitive di PostgreSQL. Berikan kredit kembali kepada Anda karena menunjukkan cara menggunakannya di Rails! Satu catatan tambahan: jika Anda menggunakan pencari standar, mis. Find_by_name, itu masih cocok dengan persis. Anda harus menulis pencari khusus, mirip dengan baris "kueri" di atas, jika Anda ingin pencarian Anda tidak peka huruf besar-kecil.
Mark Berry

Menimbang bahwa find(:first, ...)sekarang sudah usang, saya pikir ini adalah jawaban yang paling tepat.
pengguna

apakah perlu nama.kamar kecil? Tampaknya bekerja denganProduct.where("lower(name) = ?", name).first
Jordan

1
@ Jordan, apakah Anda sudah mencobanya dengan nama yang memiliki huruf besar?
oma

1
@ Jordan, mungkin tidak terlalu penting, tapi kita harus berusaha keras untuk keakuratan pada SO karena kita membantu orang lain :)
oma

28

Jika Anda menggunakan Postegres dan Rails 4+, maka Anda memiliki opsi untuk menggunakan tipe kolom CITEXT, yang akan memungkinkan kueri case-case tidak sensitif tanpa harus menuliskan logika kueri.

Migrasi:

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

Dan untuk mengujinya Anda harus mengharapkan yang berikut:

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

Anda mungkin ingin menggunakan yang berikut ini:

validates_uniqueness_of :name, :case_sensitive => false

Harap dicatat bahwa secara default pengaturannya adalah: case_sensitive => false, jadi Anda bahkan tidak perlu menulis opsi ini jika Anda belum mengubah cara lain.

Temukan lebih lanjut di: http://api.rubyonrails.org/classes/ActiveRecord/Validations/ClassMethods.html#method-i-validates_uniqueness_of


5
Dalam pengalaman saya, berbeda dengan dokumentasi, case_sensitive benar secara default. Saya telah melihat bahwa perilaku di postgresql dan yang lainnya telah melaporkan hal yang sama di mysql.
Troy

1
jadi saya mencoba ini dengan postgres, dan tidak berhasil. find_by_x sensitif huruf apa pun ...
Louis Sayers

Validasi ini hanya saat membuat model. Jadi jika Anda memiliki 'HAML' di database Anda, dan Anda mencoba menambahkan 'haml', itu tidak akan melewati validasi.
Dudo

14

Dalam postgres:

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

1
Rails on Heroku, jadi menggunakan Postgres ... ILIKE brilian. Terima kasih!
FeifanZ

Pasti menggunakan ILIKE di PostgreSQL.
Dom

12

Beberapa komentar merujuk pada Arel, tanpa memberikan contoh.

Berikut ini adalah contoh Arel dari pencarian case-insensitive:

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

Keuntungan dari solusi ini adalah database-agnostik - ia akan menggunakan perintah SQL yang benar untuk adaptor Anda saat ini ( matchesakan digunakan ILIKEuntuk Postgres, dan LIKEuntuk yang lainnya).


9

Mengutip dari dokumentasi SQLite :

Karakter lain apa pun cocok dengan dirinya sendiri atau setara dengan huruf kecil / huruf besar (yaitu pencocokan case-sensitive)

... yang saya tidak tahu. Tapi itu bekerja:

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

Jadi Anda bisa melakukan sesuatu seperti ini:

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

Tidak #find_or_create, saya tahu, dan itu mungkin tidak ramah lintas-basis data, tetapi layak untuk dilihat?


1
suka case sensitif di mysql tetapi tidak di postgresql. Saya tidak yakin tentang Oracle atau DB2. Intinya adalah, Anda tidak dapat mengandalkannya dan jika Anda menggunakannya dan bos Anda mengubah db Anda yang mendasarinya, Anda akan mulai memiliki catatan "yang hilang" tanpa alasan yang jelas mengapa. Saran bawah (nama) neutrino mungkin adalah cara terbaik untuk mengatasi ini.
masukomi

6

Pendekatan lain yang belum ada yang disebutkan adalah menambahkan case finders tidak sensitif ke dalam ActiveRecord :: Base. Detail dapat ditemukan di sini . Keuntungan dari pendekatan ini adalah Anda tidak perlu memodifikasi setiap model, dan Anda tidak harus menambahkan lower()klausa ke semua pertanyaan case sensitif Anda, Anda hanya menggunakan metode finder yang berbeda sebagai gantinya.


ketika halaman yang Anda tautkan mati, begitu juga jawaban Anda.
Anthony

Seperti @Anthony telah bernubuat, itu juga terjadi. Tautan mati.
XP84

3
@ XP84 Saya tidak tahu seberapa relevan ini lagi, tapi saya sudah perbaiki tautannya.
Alex Korban

6

Huruf besar dan kecil hanya berbeda satu bit. Cara paling efisien untuk mencari mereka adalah dengan mengabaikan bit ini, bukan untuk mengkonversi lebih rendah atau lebih tinggi, dll. Lihat kata kunci COLLATIONuntuk MSSQL, lihat NLS_SORT=BINARY_CIapakah menggunakan Oracle, dll.


4

Find_or_create sekarang sudah usang, Anda harus menggunakan Relasi AR sebagai gantinya ditambah first_or_create, seperti:

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

Ini akan mengembalikan objek yang cocok pertama, atau membuatnya untuk Anda jika tidak ada.



2

Ada banyak jawaban bagus di sini, terutama @ oma. Tetapi satu hal lain yang bisa Anda coba adalah menggunakan serialisasi kolom khusus. Jika Anda tidak keberatan semuanya disimpan huruf kecil di db Anda maka Anda dapat membuat:

# 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

Kemudian dalam model Anda:

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

Manfaat dari pendekatan ini adalah Anda masih dapat menggunakan semua pencari reguler (termasuk find_or_create_by) tanpa menggunakan cakupan kustom, fungsi, atau memiliki lower(name) = ?dalam permintaan Anda.

Kelemahannya adalah Anda kehilangan informasi casing dalam database.


2

Mirip dengan Andrews yang merupakan # 1:

Sesuatu yang bekerja untuk saya adalah:

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

Ini menghilangkan kebutuhan untuk melakukan #wheredan #firstdalam permintaan yang sama. Semoga ini membantu!


1

Anda juga dapat menggunakan lingkup seperti ini di bawah ini dan menempatkannya dalam kekhawatiran dan memasukkan dalam model yang Anda mungkin membutuhkannya:

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

Kemudian gunakan seperti ini: Model.ci_find('column', 'value')



0
user = Product.where(email: /^#{email}$/i).first

TypeError: Cannot visit Regexp
Dorian

@shilovk terima kasih. Ini persis apa yang saya cari. Dan itu terlihat lebih baik daripada jawaban yang diterima stackoverflow.com/a/2220595/1380867
MZaragoza

Saya suka solusi ini, tetapi bagaimana Anda bisa melewati kesalahan "Tidak dapat mengunjungi Regexp"? Saya melihat itu juga.
Gayle

0

Beberapa orang menunjukkan menggunakan LIKE atau ILIKE, tetapi itu memungkinkan pencarian regex. Anda juga tidak perlu downcase di Ruby. Anda dapat membiarkan database melakukannya untuk Anda. Saya pikir mungkin lebih cepat. Juga first_or_createbisa digunakan setelahnya 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

Sejauh ini, saya membuat solusi menggunakan Ruby. Tempatkan ini di dalam model Produk:

  #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

Ini akan memberi saya produk pertama yang cocok dengan nama. Atau nihil.

>> 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
Itu sangat tidak efisien untuk kumpulan data yang lebih besar, karena harus memuat semuanya ke dalam memori. Meskipun bukan masalah bagi Anda dengan hanya beberapa ratus entri, ini bukan praktik yang baik.
lambshaanxy
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.