Skip to content

Instantly share code, notes, and snippets.

@rmm5t
Last active March 16, 2018 12:16
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
Star You must be signed in to star a gist
Save rmm5t/28ce3b5615c974e5ad4fc88b8d0ff4ef to your computer and use it in GitHub Desktop.
# Author: Ryan McGeary : http://github.com/rmm5t
# License: MIT : https://rmm5t.mit-license.org/
#
# 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
# searchable_includes :contacts, :assets
# searchable_text "companies.name", "contacts.name", "assets.name"
# searchable_literals "contacts.email"
# end
#
# Company.search("john doe acme")
# Company.search("john@acme")
module PgSearchable
extend ActiveSupport::Concern
included do
@searchable_text = ["''"]
@searchable_literals = ["''"]
end
module ClassMethods
def searchable_includes(*associations)
@searchable_includes = associations
end
def searchable_text(*columns)
@searchable_text = columns
end
def searchable_literals(*columns)
@searchable_literals = columns
end
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