class PgSearch::Features::TSearch
rubocop:disable Metrics/ClassLength
def self.valid_options
def self.valid_options super + %i[dictionary prefix negation any_word normalization tsvector_column highlight] end
def arel_wrap(sql_string)
def arel_wrap(sql_string) Arel::Nodes::Grouping.new(Arel.sql(sql_string)) end
def column_to_tsvector(search_column)
def column_to_tsvector(search_column) tsvector = Arel::Nodes::NamedFunction.new( "to_tsvector", [dictionary, Arel.sql(normalize(search_column.to_sql))] ).to_sql if search_column.weight.nil? tsvector else "setweight(#{tsvector}, #{connection.quote(search_column.weight)})" end end
def columns_to_use
def columns_to_use if options[:tsvector_column] columns.select { |c| c.is_a?(PgSearch::Configuration::ForeignColumn) } else columns end end
def conditions
def conditions Arel::Nodes::Grouping.new( Arel::Nodes::InfixOperation.new("@@", arel_wrap(tsdocument), arel_wrap(tsquery)) ) end
def deprecated_headline_options
def deprecated_headline_options indifferent_options = options.with_indifferent_access %w[ start_sel stop_sel max_fragments max_words min_words short_word fragment_delimiter highlight_all ].reduce({}) do |hash, deprecated_key| hash.tap do value = indifferent_options[:highlight][deprecated_key] unless value.nil? key = deprecated_key.camelize ActiveSupport::Deprecation.warn( "pg_search 3.0 will no longer accept :#{deprecated_key} as an argument to :ts_headline, " \ "use :#{key} instead." ) hash[key] = ts_headline_option_value(value) end end end end
def dictionary
def dictionary Arel::Nodes.build_quoted(options[:dictionary] || :simple) end
def headline_options
def headline_options indifferent_options = options.with_indifferent_access %w[ StartSel StopSel MaxFragments MaxWords MinWords ShortWord FragmentDelimiter HighlightAll ].reduce({}) do |hash, key| hash.tap do value = indifferent_options[:highlight][key] hash[key] = ts_headline_option_value(value) end end end
def highlight
def highlight arel_wrap(ts_headline) end
def normalization
32 divides the rank by itself + 1
16 divides the rank by 1 + the logarithm of the number of unique words in document
8 divides the rank by the number of unique words in document
4 divides the rank by the mean harmonic distance between extents (this is implemented only by ts_rank_cd)
2 divides the rank by the document length
1 divides the rank by 1 + the logarithm of the document length
0 (the default) ignores the document length
From http://www.postgresql.org/docs/8.3/static/textsearch-controls.html
def normalization options[:normalization] || 0 end
def rank
def rank arel_wrap(tsearch_rank) end
def ts_headline
def ts_headline Arel::Nodes::NamedFunction.new("ts_headline", [ dictionary, arel_wrap(document), arel_wrap(tsquery), Arel::Nodes.build_quoted(ts_headline_options) ]).to_sql end
def ts_headline_option_value(value)
def ts_headline_option_value(value) case value when String %("#{value.gsub('"', '""')}") when true "TRUE" when false "FALSE" else value end end
def ts_headline_options
def ts_headline_options return '' unless options[:highlight].is_a?(Hash) headline_options .merge(deprecated_headline_options) .map { |key, value| "#{key} = #{value}" unless value.nil? } .compact .join(", ") end
def tsdocument
def tsdocument tsdocument_terms = (columns_to_use || []).map do |search_column| column_to_tsvector(search_column) end if options[:tsvector_column] tsvector_columns = Array.wrap(options[:tsvector_column]) tsdocument_terms << tsvector_columns.map do |tsvector_column| column_name = connection.quote_column_name(tsvector_column) "#{quoted_table_name}.#{column_name}" end end tsdocument_terms.join(' || ') end
def tsearch_rank
def tsearch_rank Arel::Nodes::NamedFunction.new("ts_rank", [ arel_wrap(tsdocument), arel_wrap(tsquery), normalization ]).to_sql end
def tsquery
def tsquery return "''" if query.blank? query_terms = query.split(" ").compact tsquery_terms = query_terms.map { |term| tsquery_for_term(term) } tsquery_terms.join(options[:any_word] ? ' || ' : ' && ') end
def tsquery_for_term(unsanitized_term) # rubocop:disable Metrics/AbcSize
def tsquery_for_term(unsanitized_term) # rubocop:disable Metrics/AbcSize if options[:negation] && unsanitized_term.start_with?("!") unsanitized_term[0] = '' negated = true end sanitized_term = unsanitized_term.gsub(DISALLOWED_TSQUERY_CHARACTERS, " ") term_sql = Arel.sql(normalize(connection.quote(sanitized_term))) # After this, the SQL expression evaluates to a string containing the term surrounded by single-quotes. # If :prefix is true, then the term will have :* appended to the end. # If :negated is true, then the term will have ! prepended to the front. terms = [ (Arel::Nodes.build_quoted('!') if negated), Arel::Nodes.build_quoted("' "), term_sql, Arel::Nodes.build_quoted(" '"), (Arel::Nodes.build_quoted(":*") if options[:prefix]) ].compact tsquery_sql = terms.inject do |memo, term| Arel::Nodes::InfixOperation.new("||", memo, Arel::Nodes.build_quoted(term)) end Arel::Nodes::NamedFunction.new( "to_tsquery", [dictionary, tsquery_sql] ).to_sql end