class PgSearch::Features::TSearch

Constants

DISALLOWED_TSQUERY_CHARACTERS

Public Class Methods

valid_options() click to toggle source
# File lib/pg_search/features/tsearch.rb, line 9
def self.valid_options
  super + %i[dictionary prefix negation any_word normalization tsvector_column highlight]
end

Public Instance Methods

conditions() click to toggle source
# File lib/pg_search/features/tsearch.rb, line 13
def conditions
  Arel::Nodes::Grouping.new(
    Arel::Nodes::InfixOperation.new("@@", arel_wrap(tsdocument), arel_wrap(tsquery))
  )
end
highlight() click to toggle source
# File lib/pg_search/features/tsearch.rb, line 23
def highlight
  arel_wrap(ts_headline)
end
rank() click to toggle source
# File lib/pg_search/features/tsearch.rb, line 19
def rank
  arel_wrap(tsearch_rank)
end

Private Instance Methods

arel_wrap(sql_string) click to toggle source
# File lib/pg_search/features/tsearch.rb, line 184
def arel_wrap(sql_string)
  Arel::Nodes::Grouping.new(Arel.sql(sql_string))
end
column_to_tsvector(search_column) click to toggle source
# File lib/pg_search/features/tsearch.rb, line 196
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
columns_to_use() click to toggle source
# File lib/pg_search/features/tsearch.rb, line 188
def columns_to_use
  if options[:tsvector_column]
    columns.select { |c| c.is_a?(PgSearch::Configuration::ForeignColumn) }
  else
    columns
  end
end
deprecated_headline_options() click to toggle source
# File lib/pg_search/features/tsearch.rb, line 61
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

        warn(
          "pg_search 3.0 will no longer accept :#{deprecated_key} as an argument to :ts_headline, " \
          "use :#{key} instead.",
          category: :deprecated,
          uplevel: 1
        )

        hash[key] = ts_headline_option_value(value)
      end
    end
  end
end
dictionary() click to toggle source
# File lib/pg_search/features/tsearch.rb, line 180
def dictionary
  Arel::Nodes.build_quoted(options[:dictionary] || :simple)
end
headline_options() click to toggle source
# File lib/pg_search/features/tsearch.rb, line 47
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
normalization() click to toggle source

From www.postgresql.org/docs/8.3/static/textsearch-controls.html

0 (the default) ignores the document length
1 divides the rank by 1 + the logarithm of the document length
2 divides the rank by the document length
4 divides the rank by the mean harmonic distance between extents (this is implemented only by ts_rank_cd)
8 divides the rank by the number of unique words in document
16 divides the rank by 1 + the logarithm of the number of unique words in document
32 divides the rank by itself + 1

The integer option controls several behaviors, so it is a bit mask: you can specify one or more behaviors

# File lib/pg_search/features/tsearch.rb, line 168
def normalization
  options[:normalization] || 0
end
ts_headline() click to toggle source
# File lib/pg_search/features/tsearch.rb, line 29
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
ts_headline_option_value(value) click to toggle source
# File lib/pg_search/features/tsearch.rb, line 86
def ts_headline_option_value(value)
  case value
  when String
    %("#{value.gsub('"', '""')}")
  when true
    "TRUE"
  when false
    "FALSE"
  else
    value
  end
end
ts_headline_options() click to toggle source
# File lib/pg_search/features/tsearch.rb, line 38
def ts_headline_options
  return "" unless options[:highlight].is_a?(Hash)

  headline_options
    .merge(deprecated_headline_options)
    .filter_map { |key, value| "#{key} = #{value}" unless value.nil? }
    .join(", ")
end
tsdocument() click to toggle source
# File lib/pg_search/features/tsearch.rb, line 141
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
tsearch_rank() click to toggle source
# File lib/pg_search/features/tsearch.rb, line 172
def tsearch_rank
  Arel::Nodes::NamedFunction.new("ts_rank", [
    arel_wrap(tsdocument),
    arel_wrap(tsquery),
    normalization
  ]).to_sql
end
tsquery() click to toggle source
# File lib/pg_search/features/tsearch.rb, line 133
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
tsquery_expression(term_sql, negated:, prefix:) click to toggle source

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.

# File lib/pg_search/features/tsearch.rb, line 119
def tsquery_expression(term_sql, negated:, prefix:)
  terms = [
    (Arel::Nodes.build_quoted("!") if negated),
    Arel::Nodes.build_quoted("' "),
    term_sql,
    Arel::Nodes.build_quoted(" '"),
    (Arel::Nodes.build_quoted(":*") if prefix)
  ].compact

  terms.inject do |memo, term|
    Arel::Nodes::InfixOperation.new("||", memo, Arel::Nodes.build_quoted(term))
  end
end
tsquery_for_term(unsanitized_term) click to toggle source
# File lib/pg_search/features/tsearch.rb, line 101
def tsquery_for_term(unsanitized_term)
  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)))

  tsquery = tsquery_expression(term_sql, negated: negated, prefix: options[:prefix])

  Arel::Nodes::NamedFunction.new("to_tsquery", [dictionary, tsquery]).to_sql
end