# frozen_string_literal: true # == Schema Information # # Table name: polysearches # # id :uuid not null, primary key # searchable_type :string not null # value :tsvector # words :text # created_at :datetime not null # updated_at :datetime not null # searchable_id :uuid not null # # Indexes # # index_polysearches_on_created_at (created_at) # index_polysearches_on_searchable_type_and_searchable_id (searchable_type,searchable_id) UNIQUE # index_polysearches_on_updated_at (updated_at) # index_polysearches_on_value (value) USING gin # index_polysearches_on_words (words) USING gin # module Polysearch class Record < ActiveRecord::Base # extends ................................................................... # includes .................................................................. # relationships ............................................................. belongs_to :searchable, polymorphic: true, inverse_of: "polysearch" # validations ............................................................... # callbacks ................................................................. # scopes .................................................................... scope :select_full_text_search_rank, ->(value, *selects) { plainto_tsquery = Arel::Nodes::NamedFunction.new("plainto_tsquery", [Arel::Nodes::SqlLiteral.new("'simple'"), arel_search_value(value)]) ts_rank = Arel::Nodes::NamedFunction.new("ts_rank", [arel_table[:value], plainto_tsquery]) selects << Arel.star if selects.blank? selects << ts_rank.as("search_rank") select(*selects).reorder("search_rank desc") } scope :full_text_search, ->(value) { if value.blank? all else plainto_tsquery = Arel::Nodes::NamedFunction.new("plainto_tsquery", [Arel::Nodes::SqlLiteral.new("'simple'"), arel_search_value(value)]) where(Arel::Nodes::InfixOperation.new("@@", arel_table[:value], plainto_tsquery)) end } scope :select_similarity_rank, ->(value, *selects) { similarity = Arel::Nodes::NamedFunction.new("similarity", [arel_table[:words], arel_search_value(value)]) selects << Arel.star if selects.blank? selects << similarity.as("search_rank") select(*selects).order("search_rank desc") } scope :similarity_search, ->(value) { if value.blank? all else where Arel::Nodes::NamedFunction.new("similarity", [arel_table[:words], arel_search_value(value)]).gt(0) end } scope :combined_search, ->(value) { subquery = <<~SQL ( #{select_full_text_search_rank(value).full_text_search(value).except(:order).to_sql} UNION ALL #{select_similarity_rank(value).similarity_search(value).except(:order).to_sql} ) AS #{table_name} SQL from(subquery).order("search_rank desc") } scope :polysearch, ->(value) { if value.blank? all else full_text_search(value).exists? ? select_full_text_search_rank(value).full_text_search(value) : select_similarity_rank(value).similarity_search(value) end } # additional config (i.e. accepts_nested_attribute_for etc...) .............. self.table_name = "polysearches" self.primary_key = :id # class methods ............................................................. class << self def arel_search_value(value) value = value.to_s.gsub(/\W/, " ").squeeze(" ").downcase.strip Arel::Nodes::SqlLiteral.new(sanitize_sql_array(["?", value])) end end # public instance methods ................................................... def update_value(tsvector_sql) sql = <<~SQL UPDATE polysearches SET value = (#{tsvector_sql}) WHERE searchable_type = :searchable_type AND searchable_id = :searchable_id; SQL self.class.connection.execute self.class.sanitize_sql_array([ sql, searchable_type: searchable_type, searchable_id: searchable_id ]) end # protected instance methods ................................................ # private instance methods .................................................. end end