jaikoo-thinking-sphinx 0.9.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. data/LICENCE +20 -0
  2. data/README +76 -0
  3. data/lib/thinking_sphinx.rb +112 -0
  4. data/lib/thinking_sphinx/active_record.rb +153 -0
  5. data/lib/thinking_sphinx/active_record/delta.rb +80 -0
  6. data/lib/thinking_sphinx/active_record/has_many_association.rb +29 -0
  7. data/lib/thinking_sphinx/active_record/search.rb +50 -0
  8. data/lib/thinking_sphinx/adapters/abstract_adapter.rb +27 -0
  9. data/lib/thinking_sphinx/adapters/mysql_adapter.rb +9 -0
  10. data/lib/thinking_sphinx/adapters/postgresql_adapter.rb +84 -0
  11. data/lib/thinking_sphinx/association.rb +144 -0
  12. data/lib/thinking_sphinx/attribute.rb +284 -0
  13. data/lib/thinking_sphinx/collection.rb +105 -0
  14. data/lib/thinking_sphinx/configuration.rb +314 -0
  15. data/lib/thinking_sphinx/field.rb +206 -0
  16. data/lib/thinking_sphinx/index.rb +432 -0
  17. data/lib/thinking_sphinx/index/builder.rb +220 -0
  18. data/lib/thinking_sphinx/index/faux_column.rb +110 -0
  19. data/lib/thinking_sphinx/rails_additions.rb +68 -0
  20. data/lib/thinking_sphinx/search.rb +436 -0
  21. data/spec/unit/thinking_sphinx/active_record/delta_spec.rb +132 -0
  22. data/spec/unit/thinking_sphinx/active_record/has_many_association_spec.rb +53 -0
  23. data/spec/unit/thinking_sphinx/active_record/search_spec.rb +107 -0
  24. data/spec/unit/thinking_sphinx/active_record_spec.rb +295 -0
  25. data/spec/unit/thinking_sphinx/association_spec.rb +247 -0
  26. data/spec/unit/thinking_sphinx/attribute_spec.rb +360 -0
  27. data/spec/unit/thinking_sphinx/collection_spec.rb +71 -0
  28. data/spec/unit/thinking_sphinx/configuration_spec.rb +512 -0
  29. data/spec/unit/thinking_sphinx/field_spec.rb +224 -0
  30. data/spec/unit/thinking_sphinx/index/builder_spec.rb +34 -0
  31. data/spec/unit/thinking_sphinx/index/faux_column_spec.rb +68 -0
  32. data/spec/unit/thinking_sphinx/index_spec.rb +317 -0
  33. data/spec/unit/thinking_sphinx/search_spec.rb +203 -0
  34. data/spec/unit/thinking_sphinx_spec.rb +129 -0
  35. data/tasks/thinking_sphinx_tasks.rake +1 -0
  36. data/tasks/thinking_sphinx_tasks.rb +100 -0
  37. metadata +103 -0
@@ -0,0 +1,432 @@
1
+ require 'thinking_sphinx/index/builder'
2
+ require 'thinking_sphinx/index/faux_column'
3
+
4
+ module ThinkingSphinx
5
+ # The Index class is a ruby representation of a Sphinx source (not a Sphinx
6
+ # index - yes, I know it's a little confusing. You'll manage). This is
7
+ # another 'internal' Thinking Sphinx class - if you're using it directly,
8
+ # you either know what you're doing, or messing with things beyond your ken.
9
+ # Enjoy.
10
+ #
11
+ class Index
12
+ attr_accessor :model, :fields, :attributes, :conditions, :groupings,
13
+ :delta, :options
14
+
15
+ # Create a new index instance by passing in the model it is tied to, and
16
+ # a block to build it with (optional but recommended). For documentation
17
+ # on the syntax for inside the block, the Builder class is what you want.
18
+ #
19
+ # Quick Example:
20
+ #
21
+ # Index.new(User) do
22
+ # indexes login, email
23
+ #
24
+ # has created_at
25
+ #
26
+ # set_property :delta => true
27
+ # end
28
+ #
29
+ def initialize(model, &block)
30
+ @model = model
31
+ @associations = {}
32
+ @fields = []
33
+ @attributes = []
34
+ @conditions = []
35
+ @groupings = []
36
+ @options = {}
37
+ @delta = false
38
+
39
+ initialize_from_builder(&block) if block_given?
40
+ end
41
+
42
+ def name
43
+ self.class.name(@model)
44
+ end
45
+
46
+ def self.name(model)
47
+ model.name.underscore.tr(':/\\', '_')
48
+ end
49
+
50
+ def empty?(part = :core)
51
+ config = ThinkingSphinx::Configuration.instance
52
+ File.size?("#{config.searchd_file_path}/#{self.name}_#{part}.spa").nil?
53
+ end
54
+
55
+ def to_config(model, index, database_conf, offset)
56
+ # Set up associations and joins
57
+ add_internal_attributes
58
+ link!
59
+
60
+ attr_sources = attributes.collect { |attrib|
61
+ attrib.to_sphinx_clause
62
+ }.join("\n ")
63
+
64
+ db_adapter = case adapter
65
+ when :postgres
66
+ "pgsql"
67
+ when :mysql
68
+ "mysql"
69
+ else
70
+ raise "Unsupported Database Adapter: Sphinx only supports MySQL and PosgreSQL"
71
+ end
72
+
73
+ config = <<-SOURCE
74
+
75
+ source #{self.class.name(model)}_#{index}_core
76
+ {
77
+ type = #{db_adapter}
78
+ sql_host = #{database_conf[:host] || "localhost"}
79
+ sql_user = #{database_conf[:username] || database_conf[:user]}
80
+ sql_pass = #{(database_conf[:password] || "").gsub('#', '\#')}
81
+ sql_db = #{database_conf[:database]}
82
+ #{"sql_port = #{database_conf[:port]}" unless database_conf[:port].blank? }
83
+ #{"sql_sock = #{database_conf[:socket]}" unless database_conf[:socket].blank? }
84
+
85
+ sql_query_pre = #{utf8? && adapter == :mysql ? "SET NAMES utf8" : ""}
86
+ #{"sql_query_pre = SET SESSION group_concat_max_len = #{@options[:group_concat_max_len]}" if @options[:group_concat_max_len]}
87
+ sql_query_pre = #{to_sql_query_pre}
88
+ sql_query = #{to_sql(:offset => offset).gsub(/\n/, ' ')}
89
+ sql_query_range = #{to_sql_query_range}
90
+ sql_query_info = #{to_sql_query_info(offset)}
91
+ #{attr_sources}
92
+ #{ThinkingSphinx::Configuration.instance.hash_to_config(self.source_options)}
93
+ }
94
+ SOURCE
95
+
96
+ if delta?
97
+ config += <<-SOURCE
98
+
99
+ source #{self.class.name(model)}_#{index}_delta : #{self.class.name(model)}_#{index}_core
100
+ {
101
+ sql_query_pre =
102
+ sql_query_pre = #{utf8? && adapter == :mysql ? "SET NAMES utf8" : ""}
103
+ #{"sql_query_pre = SET SESSION group_concat_max_len = #{@options[:group_concat_max_len]}" if @options[:group_concat_max_len]}
104
+ sql_query = #{to_sql(:delta => true, :offset => offset).gsub(/\n/, ' ')}
105
+ sql_query_range = #{to_sql_query_range :delta => true}
106
+ }
107
+ SOURCE
108
+ end
109
+
110
+ config
111
+ end
112
+
113
+ # Link all the fields and associations to their corresponding
114
+ # associations and joins. This _must_ be called before interrogating
115
+ # the index's fields and associations for anything that may reference
116
+ # their SQL structure.
117
+ #
118
+ def link!
119
+ base = ::ActiveRecord::Associations::ClassMethods::JoinDependency.new(
120
+ @model, [], nil
121
+ )
122
+
123
+ @fields.each { |field|
124
+ field.model ||= @model
125
+ field.columns.each { |col|
126
+ field.associations[col] = associations(col.__stack.clone)
127
+ field.associations[col].each { |assoc| assoc.join_to(base) }
128
+ }
129
+ }
130
+
131
+ @attributes.each { |attribute|
132
+ attribute.model ||= @model
133
+ attribute.columns.each { |col|
134
+ attribute.associations[col] = associations(col.__stack.clone)
135
+ attribute.associations[col].each { |assoc| assoc.join_to(base) }
136
+ }
137
+ }
138
+ end
139
+
140
+ # Generates the big SQL statement to get the data back for all the fields
141
+ # and attributes, using all the relevant association joins. If you want
142
+ # the version filtered for delta values, send through :delta => true in the
143
+ # options. Won't do much though if the index isn't set up to support a
144
+ # delta sibling.
145
+ #
146
+ # Examples:
147
+ #
148
+ # index.to_sql
149
+ # index.to_sql(:delta => true)
150
+ #
151
+ def to_sql(options={})
152
+ assocs = all_associations
153
+
154
+ where_clause = ""
155
+ if self.delta?
156
+ where_clause << " AND #{@model.quoted_table_name}.#{quote_column('delta')}" +" = #{options[:delta] ? db_boolean(true) : db_boolean(false)}"
157
+ end
158
+ unless @conditions.empty?
159
+ where_clause << " AND " << @conditions.join(" AND ")
160
+ end
161
+
162
+ internal_groupings = []
163
+ if @model.column_names.include?(@model.inheritance_column)
164
+ internal_groupings << "#{@model.quoted_table_name}.#{quote_column(@model.inheritance_column)}"
165
+ end
166
+
167
+ unique_id_expr = "* #{ThinkingSphinx.indexed_models.size} + #{options[:offset] || 0}"
168
+
169
+ sql = <<-SQL
170
+ SELECT #{ (
171
+ ["#{@model.quoted_table_name}.#{quote_column(@model.primary_key)} #{unique_id_expr} AS #{quote_column(@model.primary_key)} "] +
172
+ @fields.collect { |field| field.to_select_sql } +
173
+ @attributes.collect { |attribute| attribute.to_select_sql }
174
+ ).join(", ") }
175
+ FROM #{ @model.table_name }
176
+ #{ assocs.collect { |assoc| assoc.to_sql }.join(' ') }
177
+ WHERE #{@model.quoted_table_name}.#{quote_column(@model.primary_key)} >= $start
178
+ AND #{@model.quoted_table_name}.#{quote_column(@model.primary_key)} <= $end
179
+ #{ where_clause }
180
+ GROUP BY #{ (
181
+ ["#{@model.quoted_table_name}.#{quote_column(@model.primary_key)}"] +
182
+ @fields.collect { |field| field.to_group_sql }.compact +
183
+ @attributes.collect { |attribute| attribute.to_group_sql }.compact +
184
+ @groupings + internal_groupings
185
+ ).join(", ") }
186
+ SQL
187
+
188
+ if @model.connection.class.name == "ActiveRecord::ConnectionAdapters::MysqlAdapter"
189
+ sql += " ORDER BY NULL"
190
+ end
191
+
192
+ sql
193
+ end
194
+
195
+ # Simple helper method for the query info SQL - which is a statement that
196
+ # returns the single row for a corresponding id.
197
+ #
198
+ def to_sql_query_info(offset)
199
+ "SELECT * FROM #{@model.quoted_table_name} WHERE " +
200
+ " #{quote_column(@model.primary_key)} = (($id - #{offset}) / #{ThinkingSphinx.indexed_models.size})"
201
+ end
202
+
203
+ # Simple helper method for the query range SQL - which is a statement that
204
+ # returns minimum and maximum id values. These can be filtered by delta -
205
+ # so pass in :delta => true to get the delta version of the SQL.
206
+ #
207
+ def to_sql_query_range(options={})
208
+ min_statement = "MIN(#{quote_column(@model.primary_key)})"
209
+ max_statement = "MAX(#{quote_column(@model.primary_key)})"
210
+
211
+ # Fix to handle Sphinx PostgreSQL bug (it doesn't like NULLs or 0's)
212
+ if adapter == :postgres
213
+ min_statement = "COALESCE(#{min_statement}, 1)"
214
+ max_statement = "COALESCE(#{max_statement}, 1)"
215
+ end
216
+
217
+ sql = "SELECT #{min_statement}, #{max_statement} " +
218
+ "FROM #{@model.quoted_table_name} "
219
+ sql << "WHERE #{@model.quoted_table_name}.#{quote_column('delta')} " +
220
+ "= #{options[:delta] ? db_boolean(true) : db_boolean(false)}" if self.delta?
221
+ sql
222
+ end
223
+
224
+ # Returns the SQL query to run before a full index - ie: nothing unless the
225
+ # index has a delta, and then it's an update statement to set delta values
226
+ # back to 0.
227
+ #
228
+ def to_sql_query_pre
229
+ self.delta? ? "UPDATE #{@model.quoted_table_name} SET #{quote_column('delta')} = #{db_boolean(false)}" : ""
230
+ end
231
+
232
+ # Flag to indicate whether this index has a corresponding delta index.
233
+ #
234
+ def delta?
235
+ @delta
236
+ end
237
+
238
+ def adapter
239
+ @adapter ||= case @model.connection.class.name
240
+ when "ActiveRecord::ConnectionAdapters::MysqlAdapter"
241
+ :mysql
242
+ when "ActiveRecord::ConnectionAdapters::PostgreSQLAdapter"
243
+ :postgres
244
+ else
245
+ raise "Invalid Database Adapter: Sphinx only supports MySQL and PostgreSQL"
246
+ end
247
+ end
248
+
249
+ def adapter_object
250
+ @adapter_object ||= ThinkingSphinx::AbstractAdapter.detect(@model)
251
+ end
252
+
253
+ def prefix_fields
254
+ @fields.select { |field| field.prefixes }
255
+ end
256
+
257
+ def infix_fields
258
+ @fields.select { |field| field.infixes }
259
+ end
260
+
261
+ def local_index_options
262
+ @options.keys.inject({}) do |local_options, key|
263
+ if ThinkingSphinx::Configuration::IndexOptions.include?(key.to_s)
264
+ local_options[key.to_sym] = @options[key]
265
+ end
266
+ local_options
267
+ end
268
+ end
269
+
270
+ def index_options
271
+ all_index_options = ThinkingSphinx::Configuration.instance.index_options.clone
272
+ @options.keys.select { |key|
273
+ ThinkingSphinx::Configuration::IndexOptions.include?(key.to_s)
274
+ }.each { |key| all_index_options[key.to_sym] = @options[key] }
275
+ all_index_options
276
+ end
277
+
278
+ def source_options
279
+ all_source_options = ThinkingSphinx::Configuration.instance.source_options.clone
280
+ @options.keys.select { |key|
281
+ ThinkingSphinx::Configuration::SourceOptions.include?(key.to_s)
282
+ }.each { |key| all_source_options[key.to_sym] = @options[key] }
283
+ all_source_options
284
+ end
285
+
286
+ private
287
+
288
+ def utf8?
289
+ self.index_options[:charset_type] == "utf-8"
290
+ end
291
+
292
+ def quote_column(column)
293
+ @model.connection.quote_column_name(column)
294
+ end
295
+
296
+ # Does all the magic with the block provided to the base #initialize.
297
+ # Creates a new class subclassed from Builder, and evaluates the block
298
+ # on it, then pulls all relevant settings - fields, attributes, conditions,
299
+ # properties - into the new index.
300
+ #
301
+ # Also creates a CRC attribute for the model.
302
+ #
303
+ def initialize_from_builder(&block)
304
+ builder = Class.new(Builder)
305
+ builder.setup
306
+
307
+ builder.instance_eval &block
308
+
309
+ unless @model.descends_from_active_record?
310
+ stored_class = @model.store_full_sti_class ? @model.name : @model.name.demodulize
311
+ builder.where("#{@model.quoted_table_name}.#{quote_column(@model.inheritance_column)} = '#{stored_class}'")
312
+ end
313
+
314
+ @fields = builder.fields
315
+ @attributes = builder.attributes
316
+ @conditions = builder.conditions
317
+ @groupings = builder.groupings
318
+ @delta = builder.properties[:delta]
319
+ @options = builder.properties.except(:delta)
320
+
321
+ # We want to make sure that if the database doesn't exist, then Thinking
322
+ # Sphinx doesn't mind when running non-TS tasks (like db:create, db:drop
323
+ # and db:migrate). It's a bit hacky, but I can't think of a better way.
324
+ rescue StandardError => err
325
+ case err.class.name
326
+ when "Mysql::Error", "ActiveRecord::StatementInvalid"
327
+ return
328
+ else
329
+ raise err
330
+ end
331
+ end
332
+
333
+ # Returns all associations used amongst all the fields and attributes.
334
+ # This includes all associations between the model and what the actual
335
+ # columns are from.
336
+ #
337
+ def all_associations
338
+ @all_associations ||= (
339
+ # field associations
340
+ @fields.collect { |field|
341
+ field.associations.values
342
+ }.flatten +
343
+ # attribute associations
344
+ @attributes.collect { |attrib|
345
+ attrib.associations.values
346
+ }.flatten
347
+ ).uniq.collect { |assoc|
348
+ # get ancestors as well as column-level associations
349
+ assoc.ancestors
350
+ }.flatten.uniq
351
+ end
352
+
353
+ # Gets a stack of associations for a specific path.
354
+ #
355
+ def associations(path, parent = nil)
356
+ assocs = []
357
+
358
+ if parent.nil?
359
+ assocs = association(path.shift)
360
+ else
361
+ assocs = parent.children(path.shift)
362
+ end
363
+
364
+ until path.empty?
365
+ point = path.shift
366
+ assocs = assocs.collect { |assoc|
367
+ assoc.children(point)
368
+ }.flatten
369
+ end
370
+
371
+ assocs
372
+ end
373
+
374
+ # Gets the association stack for a specific key.
375
+ #
376
+ def association(key)
377
+ @associations[key] ||= Association.children(@model, key)
378
+ end
379
+
380
+ # Returns the proper boolean value string literal for the
381
+ # current database adapter.
382
+ #
383
+ def db_boolean(val)
384
+ if adapter == :postgres
385
+ val ? 'TRUE' : 'FALSE'
386
+ else
387
+ val ? '1' : '0'
388
+ end
389
+ end
390
+
391
+ def crc_column
392
+ if @model.column_names.include?(@model.inheritance_column)
393
+ case adapter
394
+ when :postgres
395
+ "COALESCE(crc32(#{@model.quoted_table_name}.#{quote_column(@model.inheritance_column)}), #{@model.to_crc32.to_s})"
396
+ when :mysql
397
+ "IFNULL(CRC32(#{@model.quoted_table_name}.#{quote_column(@model.inheritance_column)}), #{@model.to_crc32.to_s})"
398
+ end
399
+ else
400
+ @model.to_crc32.to_s
401
+ end
402
+ end
403
+
404
+ def add_internal_attributes
405
+ @attributes << Attribute.new(
406
+ FauxColumn.new(@model.primary_key.to_sym),
407
+ :type => :integer,
408
+ :as => :sphinx_internal_id
409
+ ) unless @attributes.detect { |attr| attr.alias == :sphinx_internal_id }
410
+
411
+ @attributes << Attribute.new(
412
+ FauxColumn.new(crc_column),
413
+ :type => :integer,
414
+ :as => :class_crc
415
+ ) unless @attributes.detect { |attr| attr.alias == :class_crc }
416
+
417
+ @attributes << Attribute.new(
418
+ FauxColumn.new("'" + (@model.send(:subclasses).collect { |klass|
419
+ klass.to_crc32.to_s
420
+ } << @model.to_crc32.to_s).join(",") + "'"),
421
+ :type => :multi,
422
+ :as => :subclass_crcs
423
+ ) unless @attributes.detect { |attr| attr.alias == :subclass_crcs }
424
+
425
+ @attributes << Attribute.new(
426
+ FauxColumn.new("0"),
427
+ :type => :integer,
428
+ :as => :sphinx_deleted
429
+ ) unless @attributes.detect { |attr| attr.alias == :sphinx_deleted }
430
+ end
431
+ end
432
+ end
@@ -0,0 +1,220 @@
1
+ module ThinkingSphinx
2
+ class Index
3
+ # The Builder class is the core for the index definition block processing.
4
+ # There are four methods you really need to pay attention to:
5
+ # - indexes (aliased to includes and attribute)
6
+ # - has (aliased to attribute)
7
+ # - where
8
+ # - set_property (aliased to set_properties)
9
+ #
10
+ # The first two of these methods allow you to define what data makes up
11
+ # your indexes. #where provides a method to add manual SQL conditions, and
12
+ # set_property allows you to set some settings on a per-index basis. Check
13
+ # out each method's documentation for better ideas of usage.
14
+ #
15
+ class Builder
16
+ class << self
17
+ # No idea where this is coming from - haven't found it in any ruby or
18
+ # rails documentation. It's not needed though, so it gets undef'd.
19
+ # Hopefully the list of methods that get in the way doesn't get too
20
+ # long.
21
+ undef_method :parent if respond_to?(:parent)
22
+ undef_method :name if respond_to?(:name)
23
+ undef_method :id if respond_to?(:id)
24
+ undef_method :type if respond_to?(:type)
25
+
26
+ attr_accessor :fields, :attributes, :properties, :conditions,
27
+ :groupings
28
+
29
+ # Set up all the collections. Consider this the equivalent of an
30
+ # instance's initialize method.
31
+ #
32
+ def setup
33
+ @fields = []
34
+ @attributes = []
35
+ @properties = {}
36
+ @conditions = []
37
+ @groupings = []
38
+ end
39
+
40
+ # This is how you add fields - the strings Sphinx looks at - to your
41
+ # index. Technically, to use this method, you need to pass in some
42
+ # columns and options - but there's some neat method_missing stuff
43
+ # happening, so lets stick to the expected syntax within a define_index
44
+ # block.
45
+ #
46
+ # Expected options are :as, which points to a column alias in symbol
47
+ # form, and :sortable, which indicates whether you want to sort by this
48
+ # field.
49
+ #
50
+ # Adding Single-Column Fields:
51
+ #
52
+ # You can use symbols or methods - and can chain methods together to
53
+ # get access down the associations tree.
54
+ #
55
+ # indexes :id, :as => :my_id
56
+ # indexes :name, :sortable => true
57
+ # indexes first_name, last_name, :sortable => true
58
+ # indexes users.posts.content, :as => :post_content
59
+ # indexes users(:id), :as => :user_ids
60
+ #
61
+ # Keep in mind that if any keywords for Ruby methods - such as id or
62
+ # name - clash with your column names, you need to use the symbol
63
+ # version (see the first, second and last examples above).
64
+ #
65
+ # If you specify multiple columns (example #2), a field will be created
66
+ # for each. Don't use the :as option in this case. If you want to merge
67
+ # those columns together, continue reading.
68
+ #
69
+ # Adding Multi-Column Fields:
70
+ #
71
+ # indexes [first_name, last_name], :as => :name
72
+ # indexes [location, parent.location], :as => :location
73
+ #
74
+ # To combine multiple columns into a single field, you need to wrap
75
+ # them in an Array, as shown by the above examples. There's no
76
+ # limitations on whether they're symbols or methods or what level of
77
+ # associations they come from.
78
+ #
79
+ # Adding SQL Fragment Fields
80
+ #
81
+ # You can also define a field using an SQL fragment, useful for when
82
+ # you would like to index a calculated value.
83
+ #
84
+ # indexes "age < 18", :as => :minor
85
+ #
86
+ def indexes(*args)
87
+ options = args.extract_options!
88
+ args.each do |columns|
89
+ fields << Field.new(FauxColumn.coerce(columns), options)
90
+
91
+ if fields.last.sortable
92
+ attributes << Attribute.new(
93
+ fields.last.columns.collect { |col| col.clone },
94
+ options.merge(
95
+ :type => :string,
96
+ :as => fields.last.unique_name.to_s.concat("_sort").to_sym
97
+ )
98
+ )
99
+ end
100
+ end
101
+ end
102
+ alias_method :field, :indexes
103
+ alias_method :includes, :indexes
104
+
105
+ # This is the method to add attributes to your index (hence why it is
106
+ # aliased as 'attribute'). The syntax is the same as #indexes, so use
107
+ # that as starting point, but keep in mind the following points.
108
+ #
109
+ # An attribute can have an alias (the :as option), but it is always
110
+ # sortable - so you don't need to explicitly request that. You _can_
111
+ # specify the data type of the attribute (the :type option), but the
112
+ # code's pretty good at figuring that out itself from peering into the
113
+ # database.
114
+ #
115
+ # Attributes are limited to the following types: integers, floats,
116
+ # datetimes (converted to timestamps), booleans and strings. Don't
117
+ # forget that Sphinx converts string attributes to integers, which are
118
+ # useful for sorting, but that's about it.
119
+ #
120
+ # You can also have a collection of integers for multi-value attributes
121
+ # (MVAs). Generally these would be through a has_many relationship,
122
+ # like in this example:
123
+ #
124
+ # has posts(:id), :as => :post_ids
125
+ #
126
+ # This allows you to filter on any of the values tied to a specific
127
+ # record. Might be best to read through the Sphinx documentation to get
128
+ # a better idea of that though.
129
+ #
130
+ # Adding SQL Fragment Attributes
131
+ #
132
+ # You can also define an attribute using an SQL fragment, useful for
133
+ # when you would like to index a calculated value. Don't forget to set
134
+ # the type of the attribute though:
135
+ #
136
+ # indexes "age < 18", :as => :minor, :type => :boolean
137
+ #
138
+ # If you're creating attributes for latitude and longitude, don't
139
+ # forget that Sphinx expects these values to be in radians.
140
+ #
141
+ def has(*args)
142
+ options = args.extract_options!
143
+ args.each do |columns|
144
+ attributes << Attribute.new(FauxColumn.coerce(columns), options)
145
+ end
146
+ end
147
+ alias_method :attribute, :has
148
+
149
+ # Use this method to add some manual SQL conditions for your index
150
+ # request. You can pass in as many strings as you like, they'll get
151
+ # joined together with ANDs later on.
152
+ #
153
+ # where "user_id = 10"
154
+ # where "parent_type = 'Article'", "created_at < NOW()"
155
+ #
156
+ def where(*args)
157
+ @conditions += args
158
+ end
159
+
160
+ # Use this method to add some manual SQL strings to the GROUP BY
161
+ # clause. You can pass in as many strings as you'd like, they'll get
162
+ # joined together with commas later on.
163
+ #
164
+ # group_by "lat", "lng"
165
+ #
166
+ def group_by(*args)
167
+ @groupings += args
168
+ end
169
+
170
+ # This is what to use to set properties on the index. Chief amongst
171
+ # those is the delta property - to allow automatic updates to your
172
+ # indexes as new models are added and edited - but also you can
173
+ # define search-related properties which will be the defaults for all
174
+ # searches on the model.
175
+ #
176
+ # set_property :delta => true
177
+ # set_property :field_weights => {"name" => 100}
178
+ #
179
+ # Also, the following two properties are particularly relevant for
180
+ # geo-location searching - latitude_attr and longitude_attr. If your
181
+ # attributes for these two values are named something other than
182
+ # lat/latitude or lon/long/longitude, you can dictate what they are
183
+ # when defining the index, so you don't need to specify them for every
184
+ # geo-related search.
185
+ #
186
+ # set_property :latitude_attr => "lt", :longitude_attr => "lg"
187
+ #
188
+ # Please don't forget to add a boolean field named 'delta' to your
189
+ # model's database table if enabling the delta index for it.
190
+ #
191
+ def set_property(*args)
192
+ options = args.extract_options!
193
+ if options.empty?
194
+ @properties[args[0]] = args[1]
195
+ else
196
+ @properties.merge!(options)
197
+ end
198
+ end
199
+ alias_method :set_properties, :set_property
200
+
201
+ # Handles the generation of new columns for the field and attribute
202
+ # definitions.
203
+ #
204
+ def method_missing(method, *args)
205
+ FauxColumn.new(method, *args)
206
+ end
207
+
208
+ # A method to allow adding fields from associations which have names
209
+ # that clash with method names in the Builder class (ie: properties,
210
+ # fields, attributes).
211
+ #
212
+ # Example: indexes assoc(:properties).column
213
+ #
214
+ def assoc(assoc)
215
+ FauxColumn.new(method)
216
+ end
217
+ end
218
+ end
219
+ end
220
+ end