Yesterday, I found myself struggling with “litesearch”, and in my frustration, I decided to create a simple “Searchable” module to perform a “LIKE” search but with a cleaner interface/API.
If you’re looking for a straightforward way to implement a search feature in your application using “LIKE”, this post might be helpful to you.
…However, it turns out the issue wasn’t with “litesearch” at all. It was actually related to the parallelization of the specs. For now, I’ll continue using “litesearch” instead of the code provided in this post. Nevertheless, I wanted to share it because it might be useful to you (or even to myself in the future).
You can include the following code in app/models/concerns/searchable.rb
:
module Searchable
extend ActiveSupport::Concern
included do
cattr_accessor :searchable_fields, :searchable_associations_fields
end
class_methods do
def search_on(*searchable_fields, **searchable_associations_fields)
self.searchable_fields = searchable_fields
self.searchable_associations_fields = searchable_associations_fields
scope :search, ->(query) do
joins(searchable_joins).where(searchable_where_clause, query: "%#{query}%")
end
scope :maybe_search, ->(query) do
search(query) if query.present?
end
end
def searchable_joins
searchable_associations_fields.keys
end
def searchable_where_clause
all_normalized_searchable_fields.map { |field| "#{field} LIKE :query" }.join(" OR ")
end
def all_normalized_searchable_fields
normalized_searchable_fields + normalized_searchable_associations_fields
end
def normalized_searchable_fields
searchable_fields.map { |field| [table_name, field].join(".") }
end
def normalized_searchable_associations_fields
searchable_associations_fields.flat_map do |association, fields|
table_name = reflect_on_association(association).klass.table_name
Array.wrap(fields).map { |field| [table_name, field].join(".") }
end
end
end
end
You can use the module in your models like this:
class Contact < ApplicationRecord
include Searchable
search_on :name, :last_name, :full_name, :email, :current_company, :phone
end
You can also search on associations using a hash key with the name of the association and a list of attributes to search on.
For example:
class QuoteRequest < ApplicationRecord
include Searchable
belongs_to :requester, class_name: "Contact"
search_on :custom_id, :brand_name, :product_name, :more_info, :quantity, requester: :full_name
end
In this example, you’ll also search on the requester.full_name
attribute, which corresponds to the "contacts.full_name"
column.
If you want to search on multiple fields of the “requester”, you can do something like this:
class QuoteRequest < ApplicationRecord
include Searchable
belongs_to :requester, class_name: "Contact"
search_on :custom_id, :brand_name, :product_name, :more_info, :quantity, requester: [:email, :full_name]
end
I didn’t write a lot of tests for it, and I didn’t write them in a generic way. However, I will show them to you because they can help you understand the purpose of each part of the code.
require "test_helper"
class SearchableTest < ActiveSupport::TestCase
test "quote_requests metadata" do
# Keeps the list of fields to search on the record
assert_equal QuoteRequest.searchable_fields, [:custom_id, :brand_name, :product_name, :more_info, :quantity]
# Keeps a hash with the list of fields to search for each association
assert_equal QuoteRequest.searchable_associations_fields, {requester: :full_name}
# Builds the list of fields with the table_name, to be used later in the query
assert_equal QuoteRequest.normalized_searchable_fields, ["quote_requests.custom_id", "quote_requests.brand_name", "quote_requests.product_name", "quote_requests.more_info", "quote_requests.quantity"]
# Builds the list of fields from the associations with the table_name, to be used later in the query
assert_equal QuoteRequest.normalized_searchable_associations_fields, ["contacts.full_name"]
# The string to be used in the where clause
assert_equal QuoteRequest.searchable_where_clause, "quote_requests.custom_id LIKE :query OR quote_requests.brand_name LIKE :query OR quote_requests.product_name LIKE :query OR quote_requests.more_info LIKE :query OR quote_requests.quantity LIKE :query OR contacts.full_name LIKE :query"
end
test "contacts example" do
result = Contact.search("Pedro")
assert_equal result.count, 1
assert_equal result.first, contacts(:one)
result = Contact.search("Perez")
assert_equal result.count, 1
assert_equal result.first, contacts(:two)
end
test "quote_requests example" do
result = QuoteRequest.search("Pedro")
assert_equal 1, result.count
assert_equal quote_requests(:one), result.first
result = QuoteRequest.search("Perez")
assert_equal 1, result.count
assert_equal quote_requests(:two), result.first
end
end
While I won’t be using this module for now due to my resolution of the issue with “litesearch”, perhaps you may find it useful.
Whether you’re interested in adapting it for PostgreSQL or exploring alternative search solutions, I hope this code may serve as a helpful reference or source of inspiration.
Here I try to share knowledge and fixes to common problems and struggles for ruby on rails developers, like How to fetch the latest-N-of-each record or How to test that an specific mail was sent or a Capybara cheatsheet. You can see more examples on Most recent posts or all posts.