require "active_support/core_ext/module/delegation" module PgSearch class ScopeOptions attr_reader :config, :feature_options, :model def initialize(config) @config = config @model = config.model @feature_options = config.feature_options end def apply(scope) scope = include_table_aliasing_for_rank(scope) rank_table_alias = scope.pg_search_rank_table_alias(:include_counter) scope .joins(rank_join(rank_table_alias)) .order("#{rank_table_alias}.rank DESC, #{order_within_rank}") .extend(DisableEagerLoading) .extend(WithPgSearchRank) .extend(WithPgSearchHighlight[feature_for(:tsearch)]) end # workaround for https://github.com/Casecommons/pg_search/issues/14 module DisableEagerLoading def eager_loading? return false end end module WithPgSearchHighlight def self.[](tsearch) Module.new do include WithPgSearchHighlight define_method(:tsearch) { tsearch } end end def tsearch raise TypeError.new("You need to instantiate this module with []") end def with_pg_search_highlight scope = self scope.select(pg_search_highlight_field) end def pg_search_highlight_field "(#{highlight}) AS pg_search_highlight, #{table_name}.*" end def highlight tsearch.highlight.to_sql end end module WithPgSearchRank def with_pg_search_rank scope = self scope = scope.select("#{table_name}.*") unless scope.select_values.any? scope.select("#{pg_search_rank_table_alias}.rank AS pg_search_rank") end end module PgSearchRankTableAliasing def pg_search_rank_table_alias(include_counter = false) components = [arel_table.name] if include_counter count = pg_search_scope_application_count_plus_plus components << count if count > 0 end Configuration.alias(components) end private attr_writer :pg_search_scope_application_count def pg_search_scope_application_count @pg_search_scope_application_count ||= 0 end def pg_search_scope_application_count_plus_plus count = pg_search_scope_application_count self.pg_search_scope_application_count = pg_search_scope_application_count + 1 count end end private delegate :connection, :quoted_table_name, :to => :model def subquery model .unscoped .select("#{primary_key} AS pg_search_id") .select("#{rank} AS rank") .joins(subquery_join) .where(conditions) .limit(nil) .offset(nil) end def conditions conditions = config.features.reject do |_feature_name, feature_options| feature_options && feature_options[:sort_only] end conditions.map! do |feature_name, _feature_options| feature_for(feature_name).conditions end conditions = conditions.inject do |accumulator, expression| Arel::Nodes::Or.new(accumulator, expression) end conditions.to_sql end def order_within_rank config.order_within_rank || "#{primary_key} ASC" end def primary_key "#{quoted_table_name}.#{connection.quote_column_name(model.primary_key)}" end def subquery_join if config.associations.any? config.associations.map do |association| association.join(primary_key) end.join(' ') end end FEATURE_CLASSES = { :dmetaphone => Features::DMetaphone, :tsearch => Features::TSearch, :trigram => Features::Trigram } def feature_for(feature_name) # rubocop:disable Metrics/AbcSize feature_name = feature_name.to_sym feature_class = FEATURE_CLASSES[feature_name] raise ArgumentError.new("Unknown feature: #{feature_name}") unless feature_class normalizer = Normalizer.new(config) feature_class.new( config.query, feature_options[feature_name], config.columns, config.model, normalizer ) end def rank (config.ranking_sql || ":tsearch").gsub(/:(\w*)/) do feature_for($1).rank.to_sql end end def rank_join(rank_table_alias) "INNER JOIN (#{subquery.to_sql}) AS #{rank_table_alias} ON #{primary_key} = #{rank_table_alias}.pg_search_id" end def include_table_aliasing_for_rank(scope) return scope if scope.included_modules.include?(PgSearchRankTableAliasing) scope.all.spawn.tap do |new_scope| new_scope.class_eval { include PgSearchRankTableAliasing } end end end end