多列模糊搜索实现
Rails Query Interface: where
Rails提供了一套非常便利而又强大的查询接口(Query Interface)。例如我们经常同其打交道的where方法,可以帮我们解决大多数查询条件(Conditions)相关的问题:
# Equality Conditions
Device.where('is_root' => true)
# Range Conditions
Device.where(created_at: (Time.now.midnight - 1.day)..Time.now.midnight)
# Subset Conditions
Device.where(os_type: [1,3,5])
# NOT Conditions
Device.where.not(brand: brand)
# OR Conditions(Rails5 Only)
# see https://github.com/rails/rails/pull/16052/
Device.where('id = 1').or(Device.where('id = 2'))
问题背景
今天我遇到了这么一个的查询问题:
假设数据库中存在一张设备表(devices),表中有多个字段:设备品牌(brand)、系统类型(os type)、分辨率(resolution)、序列号(serial number)、内存(memory)等十多个字段。我们已经实现了一个index页面,负责展示所有的设备,现在希望在页面上实现一个搜索框,根据搜索框中输入的文字,针对设备表中的设备品牌、分辨率、序列号这三个字段进行模糊搜索。
大体上长这个样子:

解决思路
根据需求我实现了一个search方法,看起来基本可以满足需求了:
# models/devices.rb
def self.search(search)
if search
search.strip!
where("device_brand like ? OR serial_number like ? OR resolution like ?", "%#{search}%", "%#{search}%", "%#{search}%")
else
all
end
end
然而深入思考一下,如果有一天需求发生了变更:不再仅仅只针对三个字段进行模糊搜索,而是将模糊搜索的范围扩大到更多字段,那是不是需要拼一个非常长的where查询子句:
where("aaa like ? OR bbb like ? OR ccc like ? OR ddd like ? OR eee like ? OR fff like ?", "%#{param}%", "%#{param}%", "%#{param}%", "%#{param}%", "%#{param}%", "%#{param}%")
有没有更优雅一些的实现方案呢?
Arel
Arel是Rails里用来管理生成AST(Abstract Syntax Tree 抽象语法树)的组件,负责将Rails里一些SQL查询的DSL转化为底层的SQL语句。
我们可以使用Arel灵活地实现一些复杂的查询。
Rails提供了一个叫做arel_table的方法,用来访问底层Arel的相关接口:
Device.arel_table # => #<Arel::Table:0x00000004a03e18>
这里返回了一个Arel::Table对象,我们可以把它理解成一个包含了数据库表中每一列的hash对象,可以通过正常的访问hash元素的方式来访问这些列。
Arel提供了一系列的方法作用于这些列上,用于构造SQL语句。下面是一段简单的示例:
devices = Device.arel_table
devices.where(devices[:brand].eq('HUAWEI'))
# SELECT * FROM devices WHERE devices.brand = 'HUAWEI'
回到我们的需求上,我们可以用Arel将原先的代码重构下:
# 使用Arel实现多字段模糊查询
def self.search(search)
if search
search.strip!
search_fields = %w(device_brand serial_number resolution)
# 每个待查询字段都会有一个对应的 matches 匹配条件,最后这些条件之间用or运算合并,语义即“device_brand matches xxx or serial_number matches xxx or ...”
condition = search_fields.map do |field|
arel_table[field].matches("%#{search}%")
end.inject(:or)
# 使用 Arel 构造的查询子句可以直接用于更高层级的 Query Method,也就是where方法
where(condition)
else
all
end
end
这样即使模糊搜索的范围扩大到了更多的字段,我们只需要将相应的字段名赋值给search_fields就可以了,其它代码不需要做任何修改。
Any way else?
需要注意到的一点是,使用like '%keyword%'这种查询方式是无法使用索引的,如果数据库表中的数据非常多,那么必然会成为性能上的瓶颈。
也可以考虑使用一些搜索引擎来实现模糊查询,例如ElasticSearch。
