summaryrefslogtreecommitdiff
path: root/vendor/plugins/thinking-sphinx/lib/thinking_sphinx/index/builder.rb
blob: dbd2ba0cc0d51eb5546a606d22a3db9c4ac46a46 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
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)
    # - where
    # - set_property (aliased to 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
          }
        }
        
        attr_accessor :fields, :attributes, :properties, :conditions,
          :groupings
        
        # Set up all the collections. Consider this the equivalent of an
        # instance's initialize method.
        # 
        def setup
          @fields     = []
          @attributes = []
          @properties = {}
          @conditions = []
          @groupings  = []
        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(FauxColumn.coerce(columns), options)
            fields << field
            
            add_sort_attribute field, options   if field.sortable
            add_facet_attribute field, options  if field.faceted
          end
        end
        alias_method :field,    :indexes
        alias_method :includes, :indexes
                
        # 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(FauxColumn.coerce(columns), options)
            attributes << attribute
            
            add_facet_attribute attribute, options if attribute.faceted
          end
        end
        alias_method :attribute, :has
        
        def facet(*args)
          options = args.extract_options!
          options[:facet] = true
          
          args.each do |columns|
            attribute = Attribute.new(FauxColumn.coerce(columns), options)
            attributes << attribute
            
            add_facet_attribute attribute, 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)
          @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)
          @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!
          if options.empty?
            @properties[args[0]] = args[1]
          else
            @properties.merge!(options)
          end
        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 add_sort_attribute(field, options)
          add_internal_attribute field, options, "_sort"
        end
        
        def add_facet_attribute(resource, options)
          add_internal_attribute resource, options, "_facet", true
        end
        
        def add_internal_attribute(resource, options, suffix, crc = false)
          @attributes << Attribute.new(
            resource.columns.collect { |col| col.clone },
            options.merge(
              :type => resource.is_a?(Field) ? :string : nil,
              :as   => resource.unique_name.to_s.concat(suffix).to_sym,
              :crc  => crc
            ).except(:facet)
          )
        end
      end
    end
  end
end