Skip to content

Instantly share code, notes, and snippets.

@rmm5t
Last active September 9, 2019 16:47
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save rmm5t/73bb75028b6c2bb057a57d6d33c68217 to your computer and use it in GitHub Desktop.
Save rmm5t/73bb75028b6c2bb057a57d6d33c68217 to your computer and use it in GitHub Desktop.
# TL;DR: A poor-man's full-text search.
#
# This module employs a mechanism by which we can easily add left outer joins
# to queries (typically for full-text-like searches that need to dig into
# specific associations).
#
# One way to do this is to just use ActiveRecord's `eager_load` as part of a
# default scope, but this triggers multiple left outer joins for every single
# query, regardless of whether a search is being performed.
#
# These related associations are often displayed as part of any generic query,
# so a default scope that `includes` the associations often boosts performance
# and eliminates N+1 query problems.
#
# This module gives the best of both worlds. It makes it easy to set up a
# default scope using `includes`, but also provides a mechanism to easily
# perform an `eager_load` when necessary (using this module's custom
# `searching` criteria method). This works because combining `includes` with
# `references` gives the same behavior as `eager_load`.
#
# Usage:
#
# class Company < ActiveRecord::Base
# include PgSearchable
# self.searchable_includes = [:contacts, :assets]
# self.searchable_text = %w[companies.name contacts.name assets.name]
# self.searchable_literals = %w[contacts.email]
# end
#
# Company.search("john doe acme")
# Company.search("john@acme")
#
# Author: Ryan McGeary : http://github.com/rmm5t
# License: MIT : https://rmm5t.mit-license.org/
module PgSearchable
extend ActiveSupport::Concern
included do
mattr_accessor :searchable_includes
mattr_accessor :searchable_text, default: ["''"]
mattr_accessor :searchable_literals, default: ["''"]
end
class_methods do
def searching(*args)
query = where(*args)
if searchable_includes.present?
query = query.includes(*searchable_includes)
query = query.references(*searchable_includes) if args.compact.present?
end
query
end
def search(query)
return searching(nil) if query.blank?
text = "array_to_string(Array[#{searchable_text.join(', ')}], ' ')"
literals = "array_to_string(Array[#{searchable_literals.join(', ')}], ' ')"
searching("(#{text}) @@ :q OR (#{literals}) ilike :word", q: query, word: "%#{query}%")
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment