lib/thinking_sphinx/index/builder.rb in pixeltrix-thinking-sphinx-1.1.5 vs lib/thinking_sphinx/index/builder.rb in pixeltrix-thinking-sphinx-1.2.1
- old
+ new
@@ -1,233 +1,287 @@
module ThinkingSphinx
class Index
# The Builder class is the core for the index definition block processing.
# There are four methods you really need to pay attention to:
- # - indexes (aliased to includes and attribute)
- # - has (aliased to attribute)
+ # - indexes
+ # - has
# - where
- # - set_property (aliased to set_properties)
+ # - set_property/set_properties
#
# The first two of these methods allow you to define what data makes up
# your indexes. #where provides a method to add manual SQL conditions, and
# set_property allows you to set some settings on a per-index basis. Check
# out each method's documentation for better ideas of usage.
#
class Builder
- class << self
- # No idea where this is coming from - haven't found it in any ruby or
- # rails documentation. It's not needed though, so it gets undef'd.
- # Hopefully the list of methods that get in the way doesn't get too
- # long.
- HiddenMethods = [:parent, :name, :id, :type].each { |method|
- define_method(method) {
- caller.grep(/irb.completion/).empty? ? method_missing(method) : super
- }
+ instance_methods.grep(/^[^_]/).each { |method|
+ next if method.to_s == "instance_eval"
+ define_method(method) {
+ caller.grep(/irb.completion/).empty? ? method_missing(method) : super
}
+ }
+
+ def self.generate(model, &block)
+ index = ThinkingSphinx::Index.new(model)
+ model.sphinx_facets ||= []
- attr_accessor :fields, :attributes, :properties, :conditions,
- :groupings
+ Builder.new(index, &block) if block_given?
- # Set up all the collections. Consider this the equivalent of an
- # instance's initialize method.
- #
- def setup
- @fields = []
- @attributes = []
- @properties = {}
- @conditions = []
- @groupings = []
- end
+ index.delta_object = ThinkingSphinx::Deltas.parse index
+ index
+ end
+
+ def initialize(index, &block)
+ @index = index
+ @source = ThinkingSphinx::Source.new(@index)
+ @index.sources << @source
+ @explicit_source = false
- # This is how you add fields - the strings Sphinx looks at - to your
- # index. Technically, to use this method, you need to pass in some
- # columns and options - but there's some neat method_missing stuff
- # happening, so lets stick to the expected syntax within a define_index
- # block.
- #
- # Expected options are :as, which points to a column alias in symbol
- # form, and :sortable, which indicates whether you want to sort by this
- # field.
- #
- # Adding Single-Column Fields:
- #
- # You can use symbols or methods - and can chain methods together to
- # get access down the associations tree.
- #
- # indexes :id, :as => :my_id
- # indexes :name, :sortable => true
- # indexes first_name, last_name, :sortable => true
- # indexes users.posts.content, :as => :post_content
- # indexes users(:id), :as => :user_ids
- #
- # Keep in mind that if any keywords for Ruby methods - such as id or
- # name - clash with your column names, you need to use the symbol
- # version (see the first, second and last examples above).
- #
- # If you specify multiple columns (example #2), a field will be created
- # for each. Don't use the :as option in this case. If you want to merge
- # those columns together, continue reading.
- #
- # Adding Multi-Column Fields:
- #
- # indexes [first_name, last_name], :as => :name
- # indexes [location, parent.location], :as => :location
- #
- # To combine multiple columns into a single field, you need to wrap
- # them in an Array, as shown by the above examples. There's no
- # limitations on whether they're symbols or methods or what level of
- # associations they come from.
- #
- # Adding SQL Fragment Fields
- #
- # You can also define a field using an SQL fragment, useful for when
- # you would like to index a calculated value.
- #
- # indexes "age < 18", :as => :minor
- #
- def indexes(*args)
- options = args.extract_options!
- args.each do |columns|
- fields << Field.new(FauxColumn.coerce(columns), options)
-
- if fields.last.sortable || fields.last.faceted
- attributes << Attribute.new(
- fields.last.columns.collect { |col| col.clone },
- options.merge(
- :type => :string,
- :as => fields.last.unique_name.to_s.concat("_sort").to_sym
- ).except(:facet)
- )
- end
- end
- end
- alias_method :field, :indexes
- alias_method :includes, :indexes
+ self.instance_eval &block
- # This is the method to add attributes to your index (hence why it is
- # aliased as 'attribute'). The syntax is the same as #indexes, so use
- # that as starting point, but keep in mind the following points.
- #
- # An attribute can have an alias (the :as option), but it is always
- # sortable - so you don't need to explicitly request that. You _can_
- # specify the data type of the attribute (the :type option), but the
- # code's pretty good at figuring that out itself from peering into the
- # database.
- #
- # Attributes are limited to the following types: integers, floats,
- # datetimes (converted to timestamps), booleans and strings. Don't
- # forget that Sphinx converts string attributes to integers, which are
- # useful for sorting, but that's about it.
- #
- # You can also have a collection of integers for multi-value attributes
- # (MVAs). Generally these would be through a has_many relationship,
- # like in this example:
- #
- # has posts(:id), :as => :post_ids
- #
- # This allows you to filter on any of the values tied to a specific
- # record. Might be best to read through the Sphinx documentation to get
- # a better idea of that though.
- #
- # Adding SQL Fragment Attributes
- #
- # You can also define an attribute using an SQL fragment, useful for
- # when you would like to index a calculated value. Don't forget to set
- # the type of the attribute though:
- #
- # has "age < 18", :as => :minor, :type => :boolean
- #
- # If you're creating attributes for latitude and longitude, don't
- # forget that Sphinx expects these values to be in radians.
- #
- def has(*args)
- options = args.extract_options!
- args.each do |columns|
- attributes << Attribute.new(FauxColumn.coerce(columns), options)
- end
+ if @index.sources.any? { |source|
+ source.fields.length == 0
+ }
+ raise "At least one field is necessary for an index"
end
- alias_method :attribute, :has
+ end
+
+ def define_source(&block)
+ if @explicit_source
+ @source = ThinkingSphinx::Source.new(@index)
+ @index.sources << @source
+ else
+ @explicit_source = true
+ end
- def facet(*args)
- options = args.extract_options!
- options[:facet] = true
+ self.instance_eval &block
+ end
+
+ # This is how you add fields - the strings Sphinx looks at - to your
+ # index. Technically, to use this method, you need to pass in some
+ # columns and options - but there's some neat method_missing stuff
+ # happening, so lets stick to the expected syntax within a define_index
+ # block.
+ #
+ # Expected options are :as, which points to a column alias in symbol
+ # form, and :sortable, which indicates whether you want to sort by this
+ # field.
+ #
+ # Adding Single-Column Fields:
+ #
+ # You can use symbols or methods - and can chain methods together to
+ # get access down the associations tree.
+ #
+ # indexes :id, :as => :my_id
+ # indexes :name, :sortable => true
+ # indexes first_name, last_name, :sortable => true
+ # indexes users.posts.content, :as => :post_content
+ # indexes users(:id), :as => :user_ids
+ #
+ # Keep in mind that if any keywords for Ruby methods - such as id or
+ # name - clash with your column names, you need to use the symbol
+ # version (see the first, second and last examples above).
+ #
+ # If you specify multiple columns (example #2), a field will be created
+ # for each. Don't use the :as option in this case. If you want to merge
+ # those columns together, continue reading.
+ #
+ # Adding Multi-Column Fields:
+ #
+ # indexes [first_name, last_name], :as => :name
+ # indexes [location, parent.location], :as => :location
+ #
+ # To combine multiple columns into a single field, you need to wrap
+ # them in an Array, as shown by the above examples. There's no
+ # limitations on whether they're symbols or methods or what level of
+ # associations they come from.
+ #
+ # Adding SQL Fragment Fields
+ #
+ # You can also define a field using an SQL fragment, useful for when
+ # you would like to index a calculated value.
+ #
+ # indexes "age < 18", :as => :minor
+ #
+ def indexes(*args)
+ options = args.extract_options!
+ args.each do |columns|
+ field = Field.new(@source, FauxColumn.coerce(columns), options)
- args.each do |columns|
- attributes << Attribute.new(FauxColumn.coerce(columns), options)
- end
+ add_sort_attribute field, options if field.sortable
+ add_facet_attribute field, options if field.faceted
end
-
- # Use this method to add some manual SQL conditions for your index
- # request. You can pass in as many strings as you like, they'll get
- # joined together with ANDs later on.
- #
- # where "user_id = 10"
- # where "parent_type = 'Article'", "created_at < NOW()"
- #
- def where(*args)
- @conditions += args
+ end
+
+ # This is the method to add attributes to your index (hence why it is
+ # aliased as 'attribute'). The syntax is the same as #indexes, so use
+ # that as starting point, but keep in mind the following points.
+ #
+ # An attribute can have an alias (the :as option), but it is always
+ # sortable - so you don't need to explicitly request that. You _can_
+ # specify the data type of the attribute (the :type option), but the
+ # code's pretty good at figuring that out itself from peering into the
+ # database.
+ #
+ # Attributes are limited to the following types: integers, floats,
+ # datetimes (converted to timestamps), booleans and strings. Don't
+ # forget that Sphinx converts string attributes to integers, which are
+ # useful for sorting, but that's about it.
+ #
+ # You can also have a collection of integers for multi-value attributes
+ # (MVAs). Generally these would be through a has_many relationship,
+ # like in this example:
+ #
+ # has posts(:id), :as => :post_ids
+ #
+ # This allows you to filter on any of the values tied to a specific
+ # record. Might be best to read through the Sphinx documentation to get
+ # a better idea of that though.
+ #
+ # Adding SQL Fragment Attributes
+ #
+ # You can also define an attribute using an SQL fragment, useful for
+ # when you would like to index a calculated value. Don't forget to set
+ # the type of the attribute though:
+ #
+ # has "age < 18", :as => :minor, :type => :boolean
+ #
+ # If you're creating attributes for latitude and longitude, don't
+ # forget that Sphinx expects these values to be in radians.
+ #
+ def has(*args)
+ options = args.extract_options!
+ args.each do |columns|
+ attribute = Attribute.new(@source, FauxColumn.coerce(columns), options)
+
+ add_facet_attribute attribute, options if attribute.faceted
end
+ end
+
+ def facet(*args)
+ options = args.extract_options!
+ options[:facet] = true
- # Use this method to add some manual SQL strings to the GROUP BY
- # clause. You can pass in as many strings as you'd like, they'll get
- # joined together with commas later on.
- #
- # group_by "lat", "lng"
- #
- def group_by(*args)
- @groupings += args
+ args.each do |columns|
+ attribute = Attribute.new(@source, FauxColumn.coerce(columns), options)
+
+ add_facet_attribute attribute, options
end
-
- # This is what to use to set properties on the index. Chief amongst
- # those is the delta property - to allow automatic updates to your
- # indexes as new models are added and edited - but also you can
- # define search-related properties which will be the defaults for all
- # searches on the model.
- #
- # set_property :delta => true
- # set_property :field_weights => {"name" => 100}
- # set_property :order => "name ASC"
- # set_property :include => :picture
- # set_property :select => 'name'
- #
- # Also, the following two properties are particularly relevant for
- # geo-location searching - latitude_attr and longitude_attr. If your
- # attributes for these two values are named something other than
- # lat/latitude or lon/long/longitude, you can dictate what they are
- # when defining the index, so you don't need to specify them for every
- # geo-related search.
- #
- # set_property :latitude_attr => "lt", :longitude_attr => "lg"
- #
- # Please don't forget to add a boolean field named 'delta' to your
- # model's database table if enabling the delta index for it.
- #
- def set_property(*args)
- options = args.extract_options!
- if options.empty?
- @properties[args[0]] = args[1]
- else
- @properties.merge!(options)
- end
+ end
+
+ # Use this method to add some manual SQL conditions for your index
+ # request. You can pass in as many strings as you like, they'll get
+ # joined together with ANDs later on.
+ #
+ # where "user_id = 10"
+ # where "parent_type = 'Article'", "created_at < NOW()"
+ #
+ def where(*args)
+ @source.conditions += args
+ end
+
+ # Use this method to add some manual SQL strings to the GROUP BY
+ # clause. You can pass in as many strings as you'd like, they'll get
+ # joined together with commas later on.
+ #
+ # group_by "lat", "lng"
+ #
+ def group_by(*args)
+ @source.groupings += args
+ end
+
+ # This is what to use to set properties on the index. Chief amongst
+ # those is the delta property - to allow automatic updates to your
+ # indexes as new models are added and edited - but also you can
+ # define search-related properties which will be the defaults for all
+ # searches on the model.
+ #
+ # set_property :delta => true
+ # set_property :field_weights => {"name" => 100}
+ # set_property :order => "name ASC"
+ # set_property :include => :picture
+ # set_property :select => 'name'
+ #
+ # Also, the following two properties are particularly relevant for
+ # geo-location searching - latitude_attr and longitude_attr. If your
+ # attributes for these two values are named something other than
+ # lat/latitude or lon/long/longitude, you can dictate what they are
+ # when defining the index, so you don't need to specify them for every
+ # geo-related search.
+ #
+ # set_property :latitude_attr => "lt", :longitude_attr => "lg"
+ #
+ # Please don't forget to add a boolean field named 'delta' to your
+ # model's database table if enabling the delta index for it.
+ # Valid options for the delta property are:
+ #
+ # true
+ # false
+ # :default
+ # :delayed
+ # :datetime
+ #
+ # You can also extend ThinkingSphinx::Deltas::DefaultDelta to implement
+ # your own handling for delta indexing.
+ #
+ def set_property(*args)
+ options = args.extract_options!
+ options.each do |key, value|
+ set_single_property key, value
end
- alias_method :set_properties, :set_property
- # Handles the generation of new columns for the field and attribute
- # definitions.
- #
- def method_missing(method, *args)
- FauxColumn.new(method, *args)
+ set_single_property args[0], args[1] if args.length == 2
+ end
+ alias_method :set_properties, :set_property
+
+ # Handles the generation of new columns for the field and attribute
+ # definitions.
+ #
+ def method_missing(method, *args)
+ FauxColumn.new(method, *args)
+ end
+
+ # A method to allow adding fields from associations which have names
+ # that clash with method names in the Builder class (ie: properties,
+ # fields, attributes).
+ #
+ # Example: indexes assoc(:properties).column
+ #
+ def assoc(assoc, *args)
+ FauxColumn.new(assoc, *args)
+ end
+
+ private
+
+ def set_single_property(key, value)
+ source_options = ThinkingSphinx::Configuration::SourceOptions
+ if source_options.include?(key.to_s)
+ @source.options.merge! key => value
+ else
+ @index.local_options.merge! key => value
end
+ end
+
+ def add_sort_attribute(field, options)
+ add_internal_attribute field, options, "_sort"
+ end
+
+ def add_facet_attribute(property, options)
+ add_internal_attribute property, options, "_facet", true
+ @index.model.sphinx_facets << property.to_facet
+ end
+
+ def add_internal_attribute(property, options, suffix, crc = false)
+ return unless ThinkingSphinx::Facet.translate?(property)
- # A method to allow adding fields from associations which have names
- # that clash with method names in the Builder class (ie: properties,
- # fields, attributes).
- #
- # Example: indexes assoc(:properties).column
- #
- def assoc(assoc)
- FauxColumn.new(method)
- end
+ Attribute.new(@source,
+ property.columns.collect { |col| col.clone },
+ options.merge(
+ :type => property.is_a?(Field) ? :string : options[:type],
+ :as => property.unique_name.to_s.concat(suffix).to_sym,
+ :crc => crc
+ ).except(:facet)
+ )
end
end
end
end