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