Skip to content

Instantly share code, notes, and snippets.

@copiousfreetime
Last active July 20, 2020 19:23
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save copiousfreetime/6c10110752a596bb612ec9cc1e887531 to your computer and use it in GitHub Desktop.
Save copiousfreetime/6c10110752a596bb612ec9cc1e887531 to your computer and use it in GitHub Desktop.
Pg Search Snippets
class CreatePgSearchDocuments < ActiveRecord::Migration[6.0]
def self.up
create_table :pg_search_documents do |t|
t.text :content
t.string :searchable_type
t.uuid :searchable_id
t.tsvector :content_tsvector
t.timestamps null: false
end
add_index :pg_search_documents, [:searchable_type, :searchable_id]
# index for tsearch query improvment
add_index :pg_search_documents, :content_tsvector, using: 'gin'
execute <<~SQL
CREATE INDEX pg_search_documents_on_content ON pg_search_documents USING gin(coalesce(content, ''::text) gin_trgm_ops)
SQL
create_trigger("pg_search_documents_tsearch_tr", compatibility: 1)
.on(:pg_search_documents)
.before(:insert, :update)
.for_each(:row) do
"new.content_tsvector := to_tsvector('simple', coalesce(new.content::text, ''))"
end
end
def self.down
drop_table :pg_search_documents
# trigger function is last
drop_trigger("pg_search_documents_tsearch_tr", compatibility: 1)
end
end
# Top level setup - we are using the `Multi-search approache
# https://github.com/Casecommons/pg_search#multi-search which requries a table
# specificically dedicated to the search index. And migration to set it up.
#
# We are also using explicit ts_vector colums so the content isn't stemmed on
# every request
#
# https://github.com/Casecommons/pg_search#using-tsvector-columns
#
#
# Model concern in all the models that are searchable so if need be we can just
# search for that model
#
module Searchable
extend ActiveSupport::Concern
# Helper method so that Models can search just themselves from within the
# multisearch index. This is syntactic-sugar around the global
# PgSearch.multisearch(q) + document.searchable to get the original Model
# objects
class_methods do
def search(q)
search_scope = PgSearch.multisearch(q).where(searchable_type: self.name)
# Explicitly select the type, id and rank so we can use them in the join
# with the main model table
search_scope = search_scope.select("searchable_type, searchable_id, rank")
# use the above search_scope as a join table so we can select all the
# models that match, and order them by the rank they match.
self.joins("JOIN (#{search_scope.to_sql}) as pg_search_t ON pg_search_t.searchable_id = #{self.table_name}.id")
.order(rank: :desc)
end
end
end
## Example Book model that is searchable and pulls in text from associated
## models so that those words will also find this book
#
class Book
include PgSearch::Model
multisearchable against: [
:isbn,
:title,
:custom_searchable_text
]
include Searchable # the concern from above
# Additional in additional text to a bag of words field in this model - this
# cold also be done with `additional_attributes` field on the
# pg_search_documents table - but those are indexed, just additional static
# attribuetes
#
def custom_searchable_text
Array.new.tap do |a|
a << author.name
a << publisher.name
a << genres.pluck(:name)
end.join(" ")
end
end
# One thing of note if the author is updated, this will not update the search
# context on the book - so will need to have some post update triggers / jobs to update
# the index on the associated book if the author name is updated.
class Author
include PgSearch::Model
multisearchable against: [
:name,
:bio,
:custom_searchable_text
]
include Searchable
def customs_searchable_text
Array.new.tap do |a|
a << books.pluck(:title)
end
end
end
#
# config/iniitalizers/pg_search.rb
#
#
PgSearch.multisearch_options = {
using: {
tsearch: {
prefix: true, # allow prefix's of words to be found also
negation: true, # allow !word syntax in the search query for negation
tsvector_column: "content_tsvector", # set the tsvector_column
},
trigram: {} # activate trigram matching too
}
}
#
# Example Controllers using the Searchable concern to get back the original
# ActiveRecord objects
#
class BooksController
def search
# q can be anything and will match anything that is multisearchable against
#
search_query = params[:q]
books_scope = Book.search(search_query)
end
end
class AuthorsController
def search
search_query = params[:q]
authors_scope = Author.search(search_query)
end
end
#
# Example pan-site search controlelr to return multipel tyeps of active record
# objects from a single search query.
#
class SearchController
def search
search_query = params[:q]
documents = PgSearch.multisearch(search_query)
# Converting to the models
#
# Option 1:
# this will return all the PgSearch::Document instances which are associated
# to the upstream Models, you could inflate them all individualy by doing:
records = documents.map{ |d| d.searchable } # will make N db queries 1 per record
# Option 2:
# or inflate many by type, do this manually or maybe do it by passing
# through global id? this is totally untested and not sure if it preserves
# results order or not
global_ids = documents.map { |d|
GlobalID.initialize(
URI::GID.build(app: GlobalID.app,model_name: d.searchable_type, model_id: d.searchable_id)
)
}
records = GlobalID::Locator.locate_many(global_ids)
# Now display as approproate in your search view.
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment