Last active
March 16, 2018 12:16
Star
You must be signed in to star a gist
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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