class ThinkingSphinx::Attribute

Attributes - eternally useful when it comes to filtering, sorting or grouping. This class isn't really useful to you unless you're hacking around with the internals of Thinking Sphinx - but hey, don't let that stop you.

One key thing to remember - if you're using the attribute manually to generate SQL statements, you'll need to set the base model, and all the associations. Which can get messy. Use Index.link!, it really helps.

Constants

SphinxTypeMappings

Attributes

query_source[RW]

Public Class Methods

new(source, columns, options = {}) click to toggle source

To create a new attribute, you'll need to pass in either a single Column or an array of them, and some (optional) options.

Valid options are:

  • :as => :alias_name

  • :type => :attribute_type

  • :source => :field, :query, :ranged_query

Alias is only required in three circumstances: when there's another attribute or field with the same name, when the column name is 'id', or when there's more than one column.

Type is not required, unless you want to force a column to be a certain type (but keep in mind the value will not be CASTed in the SQL statements). The only time you really need to use this is when the type can't be figured out by the column - ie: when not actually using a database column as your source.

Source is only used for multi-value attributes (MVA). By default this will use a left-join and a group_concat to obtain the values. For better performance during indexing it can be beneficial to let Sphinx use a separate query to retrieve all document,value-pairs. Either :query or :ranged_query will enable this feature, where :ranged_query will cause the query to be executed incremental.

Example usage:

Attribute.new(
  Column.new(:created_at)
)

Attribute.new(
  Column.new(:posts, :id),
  :as => :post_ids
)

Attribute.new(
  Column.new(:posts, :id),
  :as => :post_ids,
  :source => :ranged_query
)

Attribute.new(
  [Column.new(:pages, :id), Column.new(:articles, :id)],
  :as => :content_ids
)

Attribute.new(
  Column.new("NOW()"),
  :as   => :indexed_at,
  :type => :datetime
)

If you're creating attributes for latitude and longitude, don't forget that Sphinx expects these values to be in radians.

Calls superclass method ThinkingSphinx::Property.new
# File lib/thinking_sphinx/attribute.rb, line 81
def initialize(source, columns, options = {})
  super

  @type           = options[:type]
  @query_source   = options[:source]
  @crc            = options[:crc]
  @all_ints       = options[:all_ints]

  @type         ||= :multi    unless @query_source.nil?
  if @type == :string && @crc
    @type = is_many? ? :multi : :integer
  end

  source.attributes << self
end

Public Instance Methods

all_datetimes?() click to toggle source
# File lib/thinking_sphinx/attribute.rb, line 201
def all_datetimes?
  all_of_type?(:datetime, :date, :timestamp)
end
all_ints?() click to toggle source
# File lib/thinking_sphinx/attribute.rb, line 197
def all_ints?
  @all_ints || all_of_type?(:integer)
end
all_strings?() click to toggle source
# File lib/thinking_sphinx/attribute.rb, line 205
def all_strings?
  all_of_type?(:string, :text)
end
config_value(offset = nil, delta = false) click to toggle source

Returns the configuration value that should be used for the attribute. Special case is the multi-valued attribute that needs some extra configuration.

# File lib/thinking_sphinx/attribute.rb, line 144
def config_value(offset = nil, delta = false)
  if type == :multi
    multi_config = include_as_association? ? "field" :
      source_value(offset, delta).gsub(/\s+/, " ").strip
    "uint #{unique_name} from #{multi_config}"
  else
    unique_name
  end
end
include_as_association?() click to toggle source
# File lib/thinking_sphinx/attribute.rb, line 135
def include_as_association?
  ! (type == :multi && (query_source == :query || query_source == :ranged_query))
end
live_value(instance) click to toggle source
# File lib/thinking_sphinx/attribute.rb, line 186
def live_value(instance)
  object = instance
  column = @columns.first
  column.__stack.each { |method|
    object = object.send(method)
    return sphinx_value(nil) if object.nil?
  }

  sphinx_value object.send(column.__name)
end
to_select_sql() click to toggle source

Get the part of the SELECT clause related to this attribute. Don't forget to set your model and associations first though.

This will concatenate strings and arrays of integers, and convert datetimes to timestamps, as needed.

# File lib/thinking_sphinx/attribute.rb, line 103
def to_select_sql
  return nil unless include_as_association? && available?

  separator = all_ints? || all_datetimes? || @crc ? ',' : ' '

  clause = columns_with_prefixes.collect { |column|
    case type
    when :string
      adapter.convert_nulls(column)
    when :datetime
      adapter.cast_to_datetime(column)
    when :multi
      column = adapter.cast_to_datetime(column)   if is_many_datetimes?
      column = adapter.convert_nulls(column, '0') if is_many_ints?
      column
    else
      column
    end
  }.join(', ')

  clause = adapter.crc(clause)                          if @crc
  clause = adapter.concatenate(clause, separator)       if concat_ws?
  clause = adapter.group_concatenate(clause, separator) if is_many?
  clause = adapter.downcase(clause)                     if insensitive?

  "#{clause} AS #{quote_column(unique_name)}"
end
type() click to toggle source

Returns the type of the column. If that's not already set, it returns :multi if there's the possibility of more than one value, :string if there's more than one association, otherwise it figures out what the actual column's datatype is and returns that.

# File lib/thinking_sphinx/attribute.rb, line 159
def type
  @type ||= begin
    base_type = case
    when is_many?, is_many_ints?
      :multi
    when @associations.values.flatten.length > 1
      :string
    else
      translated_type_from_database
    end

    if base_type == :string && @crc
      base_type = :integer
    else
      @crc = false unless base_type == :multi && is_many_strings? && @crc
    end

    base_type
  end
end
type_to_config() click to toggle source
# File lib/thinking_sphinx/attribute.rb, line 131
def type_to_config
  SphinxTypeMappings[type]
end
updatable?() click to toggle source
# File lib/thinking_sphinx/attribute.rb, line 180
def updatable?
  [:integer, :datetime, :boolean].include?(type) &&
  unique_name != :sphinx_internal_id &&
  !is_string?
end

Private Instance Methods

all_of_type?(*column_types) click to toggle source
# File lib/thinking_sphinx/attribute.rb, line 373
def all_of_type?(*column_types)
  @columns.all? { |col|
    klasses = @associations[col.__stack].empty? ? [@model] :
      @associations[col.__stack].collect { |assoc| assoc.reflection.klass }
    klasses.all? { |klass|
      column = klass.columns.detect { |column| column.name == col.__name.to_s }
      !column.nil? && column_types.include?(column.type)
    }
  }
end
association_joins() click to toggle source
# File lib/thinking_sphinx/attribute.rb, line 302
def association_joins
  joins = []
  assoc = end_association_for_mva
  while assoc != base_association_for_mva
    joins << assoc.join
    assoc = assoc.parent
  end

  joins
end
base_association_for_mva() click to toggle source
# File lib/thinking_sphinx/attribute.rb, line 291
def base_association_for_mva
  @first_association_for_mva ||= begin
    assoc = end_association_for_mva
    while !assoc.parent.nil?
      assoc = assoc.parent
    end

    assoc
  end
end
column_from_db() click to toggle source
# File lib/thinking_sphinx/attribute.rb, line 364
def column_from_db
  klass = @associations.values.flatten.first ?
    @associations.values.flatten.first.reflection.klass : @model

  klass.columns.detect { |col|
    @columns.collect { |c| c.__name.to_s }.include? col.name
  }
end
end_association_for_mva() click to toggle source
# File lib/thinking_sphinx/attribute.rb, line 285
def end_association_for_mva
  @association_for_mva ||= associations[columns.first.__stack].detect { |assoc|
    assoc.has_column?(columns.first.__name)
  }
end
foreign_key_for_mva(assoc) click to toggle source
# File lib/thinking_sphinx/attribute.rb, line 273
def foreign_key_for_mva(assoc)
  if ThinkingSphinx.rails_3_1?
    if assoc.reflection.through_reflection
      quote_with_table assoc.table, assoc.reflection.through_reflection.foreign_key
    else
      quote_with_table assoc.table, assoc.reflection.foreign_key
    end
  else
    quote_with_table assoc.table, assoc.reflection.primary_key_name
  end
end
insensitive?() click to toggle source
# File lib/thinking_sphinx/attribute.rb, line 401
def insensitive?
  @sortable == :insensitive
end
integer_type_from_db() click to toggle source
# File lib/thinking_sphinx/attribute.rb, line 352
def integer_type_from_db
  column = column_from_db
  return nil if column.nil?

  case column.sql_type
  when adapter.bigint_pattern
    :bigint
  else
    :integer
  end
end
is_many_datetimes?() click to toggle source
# File lib/thinking_sphinx/attribute.rb, line 317
def is_many_datetimes?
  is_many? && all_datetimes?
end
is_many_ints?() click to toggle source
# File lib/thinking_sphinx/attribute.rb, line 313
def is_many_ints?
  concat_ws? && all_ints?
end
is_many_strings?() click to toggle source
# File lib/thinking_sphinx/attribute.rb, line 321
def is_many_strings?
  is_many? && all_strings?
end
primary_key_for_mva(assoc) click to toggle source
# File lib/thinking_sphinx/attribute.rb, line 267
def primary_key_for_mva(assoc)
  quote_with_table(
    assoc.table, assoc.primary_key_from_reflection || columns.first.__name
  )
end
query(offset) click to toggle source
# File lib/thinking_sphinx/attribute.rb, line 228
def query(offset)
  base_assoc = base_association_for_mva
  end_assoc  = end_association_for_mva
  raise "Could not determine SQL for MVA" if base_assoc.nil?

  relation = ::ActiveRecord::Relation.new(
    base_assoc.reflection.klass, Arel::Table.new(base_assoc.table)
  )

  association_joins.each do |join|
    join.join_type = Arel::OuterJoin
    relation = relation.joins(join)
  end

  relation = relation.select "#{foreign_key_for_mva base_assoc} #{ThinkingSphinx.unique_id_expression(adapter, offset)} AS #{quote_column('id')}, #{primary_key_for_mva(end_assoc)} AS #{quote_column(unique_name)}"

  relation.to_sql
end
query_clause() click to toggle source
# File lib/thinking_sphinx/attribute.rb, line 247
def query_clause
  foreign_key = foreign_key_for_mva base_association_for_mva
  " WHERE #{foreign_key} >= $start AND #{foreign_key} <= $end"
end
query_delta() click to toggle source
# File lib/thinking_sphinx/attribute.rb, line 252
    def query_delta
      foreign_key = foreign_key_for_mva base_association_for_mva
      <<-SQL
#{foreign_key} IN (SELECT #{quote_column model.primary_key}
FROM #{model.quoted_table_name}
WHERE #{@source.index.delta_object.clause(model, true)})
      SQL
    end
range_query() click to toggle source
# File lib/thinking_sphinx/attribute.rb, line 261
def range_query
  assoc       = base_association_for_mva
  foreign_key = foreign_key_for_mva assoc
  "SELECT MIN(#{foreign_key}), MAX(#{foreign_key}) FROM #{quote_table_name assoc.table}"
end
source_value(offset, delta) click to toggle source
# File lib/thinking_sphinx/attribute.rb, line 211
def source_value(offset, delta)
  if is_string?
    return "#{query_source.to_s.dasherize}; #{columns.first.__name}"
  end

  query = query(offset)

  if query_source == :ranged_query
    query += query_clause
    query += " AND #{query_delta.strip}" if delta
    "ranged-query; #{query}; #{range_query}"
  else
    query += " WHERE #{query_delta.strip}" if delta
    "query; #{query}"
  end
end
sphinx_value(value) click to toggle source
# File lib/thinking_sphinx/attribute.rb, line 384
def sphinx_value(value)
  case value
  when TrueClass
    1
  when FalseClass, NilClass
    0
  when Time
    value.to_i
  when Date
    value.to_time.to_i
  when String
    value.to_crc32
  else
    value
  end
end
translated_type_from_database() click to toggle source
# File lib/thinking_sphinx/attribute.rb, line 325
    def translated_type_from_database
      case type_from_db = type_from_database
      when :integer
        integer_type_from_db
      when :datetime, :string, :float, :boolean
        type_from_db
      when :decimal
        :float
      when :timestamp, :date
        :datetime
      else
        raise <<-MESSAGE

Cannot automatically map attribute #{unique_name} in #{@model.name} to an
equivalent Sphinx type (integer, float, boolean, datetime, string as ordinal).
You could try to explicitly convert the column's value in your define_index
block:
  has "CAST(column AS INT)", :type => :integer, :as => :column
        MESSAGE
      end
    end
type_from_database() click to toggle source
# File lib/thinking_sphinx/attribute.rb, line 347
def type_from_database
  column = column_from_db
  column.nil? ? nil : column.type
end