jaikoo-thinking-sphinx 0.9.10

Sign up to get free protection for your applications and to get access to all the features.
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