thinking-sphinx 2.0.6 → 2.0.7

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 (50) hide show
  1. data/HISTORY +157 -0
  2. data/lib/cucumber/thinking_sphinx/external_world.rb +12 -0
  3. data/lib/cucumber/thinking_sphinx/internal_world.rb +127 -0
  4. data/lib/cucumber/thinking_sphinx/sql_logger.rb +20 -0
  5. data/lib/thinking-sphinx.rb +1 -0
  6. data/lib/thinking_sphinx/action_controller.rb +31 -0
  7. data/lib/thinking_sphinx/active_record/attribute_updates.rb +53 -0
  8. data/lib/thinking_sphinx/active_record/collection_proxy.rb +40 -0
  9. data/lib/thinking_sphinx/active_record/collection_proxy_with_scopes.rb +27 -0
  10. data/lib/thinking_sphinx/active_record/delta.rb +65 -0
  11. data/lib/thinking_sphinx/active_record/has_many_association.rb +37 -0
  12. data/lib/thinking_sphinx/active_record/has_many_association_with_scopes.rb +21 -0
  13. data/lib/thinking_sphinx/active_record/log_subscriber.rb +61 -0
  14. data/lib/thinking_sphinx/active_record/scopes.rb +110 -0
  15. data/lib/thinking_sphinx/active_record.rb +383 -0
  16. data/lib/thinking_sphinx/adapters/abstract_adapter.rb +87 -0
  17. data/lib/thinking_sphinx/adapters/mysql_adapter.rb +62 -0
  18. data/lib/thinking_sphinx/adapters/postgresql_adapter.rb +171 -0
  19. data/lib/thinking_sphinx/association.rb +229 -0
  20. data/lib/thinking_sphinx/attribute.rb +407 -0
  21. data/lib/thinking_sphinx/auto_version.rb +38 -0
  22. data/lib/thinking_sphinx/bundled_search.rb +44 -0
  23. data/lib/thinking_sphinx/class_facet.rb +20 -0
  24. data/lib/thinking_sphinx/configuration.rb +335 -0
  25. data/lib/thinking_sphinx/context.rb +77 -0
  26. data/lib/thinking_sphinx/core/string.rb +15 -0
  27. data/lib/thinking_sphinx/deltas/default_delta.rb +62 -0
  28. data/lib/thinking_sphinx/deltas.rb +28 -0
  29. data/lib/thinking_sphinx/deploy/capistrano.rb +99 -0
  30. data/lib/thinking_sphinx/excerpter.rb +23 -0
  31. data/lib/thinking_sphinx/facet.rb +128 -0
  32. data/lib/thinking_sphinx/facet_search.rb +170 -0
  33. data/lib/thinking_sphinx/field.rb +98 -0
  34. data/lib/thinking_sphinx/index/builder.rb +312 -0
  35. data/lib/thinking_sphinx/index/faux_column.rb +118 -0
  36. data/lib/thinking_sphinx/index.rb +157 -0
  37. data/lib/thinking_sphinx/join.rb +37 -0
  38. data/lib/thinking_sphinx/property.rb +185 -0
  39. data/lib/thinking_sphinx/railtie.rb +46 -0
  40. data/lib/thinking_sphinx/search.rb +995 -0
  41. data/lib/thinking_sphinx/search_methods.rb +439 -0
  42. data/lib/thinking_sphinx/sinatra.rb +7 -0
  43. data/lib/thinking_sphinx/source/internal_properties.rb +51 -0
  44. data/lib/thinking_sphinx/source/sql.rb +157 -0
  45. data/lib/thinking_sphinx/source.rb +194 -0
  46. data/lib/thinking_sphinx/tasks.rb +132 -0
  47. data/lib/thinking_sphinx/test.rb +55 -0
  48. data/lib/thinking_sphinx/version.rb +3 -0
  49. data/lib/thinking_sphinx.rb +296 -0
  50. metadata +53 -4
@@ -0,0 +1,128 @@
1
+ module ThinkingSphinx
2
+ class Facet
3
+ attr_reader :property, :value_source
4
+
5
+ def initialize(property, value_source = nil)
6
+ @property = property
7
+ @value_source = value_source
8
+
9
+ if property.columns.length != 1
10
+ raise "Can't translate Facets on multiple-column field or attribute"
11
+ end
12
+ end
13
+
14
+ def self.name_for(facet)
15
+ case facet
16
+ when Facet
17
+ facet.name
18
+ when String, Symbol
19
+ return :class if facet.to_s == 'sphinx_internal_class'
20
+ facet.to_s.gsub(/(_facet|_crc)$/,'').to_sym
21
+ end
22
+ end
23
+
24
+ def self.attribute_name_for(name)
25
+ name.to_s == 'class' ? 'class_crc' : "#{name}_facet"
26
+ end
27
+
28
+ def self.attribute_name_from_value(name, value)
29
+ case value
30
+ when String
31
+ attribute_name_for(name)
32
+ when Array
33
+ if value.all? { |val| val.is_a?(Integer) }
34
+ name
35
+ else
36
+ attribute_name_for(name)
37
+ end
38
+ else
39
+ name
40
+ end
41
+ end
42
+
43
+ def self.translate?(property)
44
+ return true if property.is_a?(Field)
45
+
46
+ case property.type
47
+ when :string
48
+ true
49
+ when :integer, :boolean, :datetime, :float
50
+ false
51
+ when :multi
52
+ !property.all_ints?
53
+ end
54
+ end
55
+
56
+ def name
57
+ property.unique_name
58
+ end
59
+
60
+ def attribute_name
61
+ if translate?
62
+ Facet.attribute_name_for(@property.unique_name)
63
+ else
64
+ @property.unique_name.to_s
65
+ end
66
+ end
67
+
68
+ def translate?
69
+ Facet.translate?(@property)
70
+ end
71
+
72
+ def type
73
+ @property.is_a?(Field) ? :string : @property.type
74
+ end
75
+
76
+ def float?
77
+ @property.type == :float
78
+ end
79
+
80
+ def value(object, attribute_hash)
81
+ attribute_value = attribute_hash['@groupby']
82
+ return translate(object, attribute_value) if translate? || float?
83
+
84
+ case @property.type
85
+ when :datetime
86
+ Time.at(attribute_value)
87
+ when :boolean
88
+ attribute_value > 0
89
+ else
90
+ attribute_value
91
+ end
92
+ end
93
+
94
+ def to_s
95
+ name
96
+ end
97
+
98
+ private
99
+
100
+ def translate(object, attribute_value)
101
+ objects = source_objects(object)
102
+ return if objects.blank?
103
+
104
+ method = value_source || column.__name
105
+ object = objects.one? ? objects.first : objects.detect { |item|
106
+ result = item.send(method)
107
+ result && result.to_crc32 == attribute_value
108
+ }
109
+
110
+ object.try(method)
111
+ end
112
+
113
+ def source_objects(object)
114
+ column.__stack.each { |method|
115
+ object = Array(object).collect { |item|
116
+ item.send(method)
117
+ }.flatten.compact
118
+
119
+ return nil if object.empty?
120
+ }
121
+ Array(object)
122
+ end
123
+
124
+ def column
125
+ @property.columns.first
126
+ end
127
+ end
128
+ end
@@ -0,0 +1,170 @@
1
+ module ThinkingSphinx
2
+ class FacetSearch < Hash
3
+ attr_accessor :args, :options
4
+
5
+ def initialize(*args)
6
+ ThinkingSphinx.context.define_indexes
7
+
8
+ @options = args.extract_options!
9
+ @args = args
10
+
11
+ set_default_options
12
+
13
+ populate
14
+ end
15
+
16
+ def for(hash = {})
17
+ for_options = {:with => {}}.merge(options)
18
+
19
+ hash.each do |key, value|
20
+ attrib = ThinkingSphinx::Facet.attribute_name_from_value(key, value)
21
+ for_options[:with][attrib] = underlying_value key, value
22
+ end
23
+
24
+ ThinkingSphinx.search *(args + [for_options])
25
+ end
26
+
27
+ def facet_names
28
+ @facet_names ||= begin
29
+ names = options[:all_facets] ?
30
+ facet_names_for_all_classes : facet_names_common_to_all_classes
31
+
32
+ names.delete class_facet unless options[:class_facet]
33
+ names
34
+ end
35
+ end
36
+
37
+ private
38
+
39
+ def set_default_options
40
+ options[:all_facets] ||= false
41
+ if options[:class_facet].nil?
42
+ options[:class_facet] = ((options[:classes] || []).length != 1)
43
+ end
44
+ end
45
+
46
+ def populate
47
+ return if facet_names.empty?
48
+
49
+ ThinkingSphinx::Search.bundle_searches(facet_names) { |sphinx, name|
50
+ sphinx.search *(args + [facet_search_options(name)])
51
+ }.each_with_index { |search, index|
52
+ add_from_results facet_names[index], search
53
+ }
54
+ end
55
+
56
+ def facet_search_options(facet_name)
57
+ options.merge(
58
+ :group_function => :attr,
59
+ :limit => max_matches,
60
+ :max_matches => max_matches,
61
+ :page => 1,
62
+ :group_by => facet_name,
63
+ :ids_only => !translate?(facet_name)
64
+ )
65
+ end
66
+
67
+ def facet_classes
68
+ (
69
+ options[:classes] || ThinkingSphinx.context.indexed_models.collect { |model|
70
+ model.constantize
71
+ }
72
+ ).select { |klass| klass.sphinx_facets.any? }
73
+ end
74
+
75
+ def all_facets
76
+ facet_classes.collect { |klass|
77
+ klass.sphinx_facets
78
+ }.flatten.select { |facet|
79
+ options[:facets].blank? || Array(options[:facets]).include?(facet.name)
80
+ }
81
+ end
82
+
83
+ def facet_names_for_all_classes
84
+ all_facets.group_by { |facet|
85
+ facet.name
86
+ }.collect { |name, facets|
87
+ if facets.collect { |facet| facet.type }.uniq.length > 1
88
+ raise "Facet #{name} exists in more than one model with different types"
89
+ end
90
+ facets.first.attribute_name
91
+ }
92
+ end
93
+
94
+ def facet_names_common_to_all_classes
95
+ facet_names_for_all_classes.select { |name|
96
+ facet_classes.all? { |klass|
97
+ klass.sphinx_facets.detect { |facet|
98
+ facet.attribute_name == name
99
+ }
100
+ }
101
+ }
102
+ end
103
+
104
+ def translate?(name)
105
+ facet = facet_from_name(name)
106
+ facet.translate? || facet.float?
107
+ end
108
+
109
+ def config
110
+ ThinkingSphinx::Configuration.instance
111
+ end
112
+
113
+ def max_matches
114
+ @max_matches ||= config.configuration.searchd.max_matches || 1000
115
+ end
116
+
117
+ # example: facet = country_facet; name = :country
118
+ def add_from_results(facet, search)
119
+ name = ThinkingSphinx::Facet.name_for(facet)
120
+ facet = facet_from_name(facet)
121
+
122
+ self[name] ||= {}
123
+
124
+ return if search.empty?
125
+
126
+ search.each_with_match do |result, match|
127
+ facet_value = facet.value(result, match[:attributes])
128
+
129
+ self[name][facet_value] ||= 0
130
+ self[name][facet_value] += match[:attributes]["@count"]
131
+ end
132
+ end
133
+
134
+ def underlying_value(key, value)
135
+ case value
136
+ when Array
137
+ value.collect { |item| underlying_value(key, item) }
138
+ when String
139
+ value.to_crc32
140
+ else
141
+ value
142
+ end
143
+ end
144
+
145
+ def facet_from_object(object, name)
146
+ facet = nil
147
+ klass = object.class
148
+
149
+ while klass != ::ActiveRecord::Base && facet.nil?
150
+ facet = klass.sphinx_facets.detect { |facet|
151
+ facet.attribute_name == name
152
+ }
153
+ klass = klass.superclass
154
+ end
155
+
156
+ facet
157
+ end
158
+
159
+ def facet_from_name(name)
160
+ name = ThinkingSphinx::Facet.name_for(name)
161
+ all_facets.detect { |facet|
162
+ facet.name == name
163
+ }
164
+ end
165
+
166
+ def class_facet
167
+ Riddle.loaded_version.to_i < 2 ? 'class_crc' : 'sphinx_internal_class'
168
+ end
169
+ end
170
+ end
@@ -0,0 +1,98 @@
1
+ module ThinkingSphinx
2
+ # Fields - holding the string data which Sphinx indexes for your searches.
3
+ # This class isn't really useful to you unless you're hacking around with the
4
+ # internals of Thinking Sphinx - but hey, don't let that stop you.
5
+ #
6
+ # One key thing to remember - if you're using the field manually to
7
+ # generate SQL statements, you'll need to set the base model, and all the
8
+ # associations. Which can get messy. Use Index.link!, it really helps.
9
+ #
10
+ class Field < ThinkingSphinx::Property
11
+ attr_accessor :sortable, :infixes, :prefixes
12
+
13
+ # To create a new field, you'll need to pass in either a single Column
14
+ # or an array of them, and some (optional) options. The columns are
15
+ # references to the data that will make up the field.
16
+ #
17
+ # Valid options are:
18
+ # - :as => :alias_name
19
+ # - :sortable => true
20
+ # - :infixes => true
21
+ # - :prefixes => true
22
+ # - :file => true
23
+ # - :with => :attribute # or :wordcount
24
+ #
25
+ # Alias is only required in three circumstances: when there's
26
+ # another attribute or field with the same name, when the column name is
27
+ # 'id', or when there's more than one column.
28
+ #
29
+ # Sortable defaults to false - but is quite useful when set to true, as
30
+ # it creates an attribute with the same string value (which Sphinx converts
31
+ # to an integer value), which can be sorted by. Thinking Sphinx is smart
32
+ # enough to realise that when you specify fields in sort statements, you
33
+ # mean their respective attributes.
34
+ #
35
+ # If you have partial matching enabled (ie: enable_star), then you can
36
+ # specify certain fields to have their prefixes and infixes indexed. Keep
37
+ # in mind, though, that Sphinx's default is _all_ fields - so once you
38
+ # highlight a particular field, no other fields in the index will have
39
+ # these partial indexes.
40
+ #
41
+ # Here's some examples:
42
+ #
43
+ # Field.new(
44
+ # Column.new(:name)
45
+ # )
46
+ #
47
+ # Field.new(
48
+ # [Column.new(:first_name), Column.new(:last_name)],
49
+ # :as => :name, :sortable => true
50
+ # )
51
+ #
52
+ # Field.new(
53
+ # [Column.new(:posts, :subject), Column.new(:posts, :content)],
54
+ # :as => :posts, :prefixes => true
55
+ # )
56
+ #
57
+ def initialize(source, columns, options = {})
58
+ super
59
+
60
+ @sortable = options[:sortable] || false
61
+ @infixes = options[:infixes] || false
62
+ @prefixes = options[:prefixes] || false
63
+ @file = options[:file] || false
64
+ @with = options[:with]
65
+
66
+ source.fields << self
67
+ end
68
+
69
+ # Get the part of the SELECT clause related to this field. Don't forget
70
+ # to set your model and associations first though.
71
+ #
72
+ # This will concatenate strings if there's more than one data source or
73
+ # multiple data values (has_many or has_and_belongs_to_many associations).
74
+ #
75
+ def to_select_sql
76
+ return nil unless available?
77
+
78
+ clause = columns_with_prefixes.join(', ')
79
+
80
+ clause = adapter.concatenate(clause) if concat_ws?
81
+ clause = adapter.group_concatenate(clause) if is_many?
82
+
83
+ "#{clause} AS #{quote_column(unique_name)}"
84
+ end
85
+
86
+ def file?
87
+ @file
88
+ end
89
+
90
+ def with_attribute?
91
+ @with == :attribute
92
+ end
93
+
94
+ def with_wordcount?
95
+ @with == :wordcount
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,312 @@
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
6
+ # - has
7
+ # - where
8
+ # - set_property/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
+ instance_methods.grep(/^[^_]/).each { |method|
17
+ next if method.to_s == "instance_eval"
18
+ define_method(method) {
19
+ caller.grep(/irb.completion/).empty? ? method_missing(method) : super
20
+ }
21
+ }
22
+
23
+ def self.generate(model, name = nil, &block)
24
+ index = ThinkingSphinx::Index.new(model)
25
+ index.name = name unless name.nil?
26
+
27
+ Builder.new(index, &block) if block_given?
28
+
29
+ index.delta_object = ThinkingSphinx::Deltas.parse index
30
+ index
31
+ end
32
+
33
+ def initialize(index, &block)
34
+ @index = index
35
+ @explicit_source = false
36
+
37
+ self.instance_eval &block
38
+
39
+ if no_fields?
40
+ raise "At least one field is necessary for an index"
41
+ end
42
+ end
43
+
44
+ def define_source(&block)
45
+ if @explicit_source
46
+ @source = ThinkingSphinx::Source.new(@index)
47
+ @index.sources << @source
48
+ else
49
+ @explicit_source = true
50
+ end
51
+
52
+ self.instance_eval &block
53
+ end
54
+
55
+ # This is how you add fields - the strings Sphinx looks at - to your
56
+ # index. Technically, to use this method, you need to pass in some
57
+ # columns and options - but there's some neat method_missing stuff
58
+ # happening, so lets stick to the expected syntax within a define_index
59
+ # block.
60
+ #
61
+ # Expected options are :as, which points to a column alias in symbol
62
+ # form, and :sortable, which indicates whether you want to sort by this
63
+ # field.
64
+ #
65
+ # Adding Single-Column Fields:
66
+ #
67
+ # You can use symbols or methods - and can chain methods together to
68
+ # get access down the associations tree.
69
+ #
70
+ # indexes :id, :as => :my_id
71
+ # indexes :name, :sortable => true
72
+ # indexes first_name, last_name, :sortable => true
73
+ # indexes users.posts.content, :as => :post_content
74
+ # indexes users(:id), :as => :user_ids
75
+ #
76
+ # Keep in mind that if any keywords for Ruby methods - such as id or
77
+ # name - clash with your column names, you need to use the symbol
78
+ # version (see the first, second and last examples above).
79
+ #
80
+ # If you specify multiple columns (example #2), a field will be created
81
+ # for each. Don't use the :as option in this case. If you want to merge
82
+ # those columns together, continue reading.
83
+ #
84
+ # Adding Multi-Column Fields:
85
+ #
86
+ # indexes [first_name, last_name], :as => :name
87
+ # indexes [location, parent.location], :as => :location
88
+ #
89
+ # To combine multiple columns into a single field, you need to wrap
90
+ # them in an Array, as shown by the above examples. There's no
91
+ # limitations on whether they're symbols or methods or what level of
92
+ # associations they come from.
93
+ #
94
+ # Adding SQL Fragment Fields
95
+ #
96
+ # You can also define a field using an SQL fragment, useful for when
97
+ # you would like to index a calculated value.
98
+ #
99
+ # indexes "age < 18", :as => :minor
100
+ #
101
+ def indexes(*args)
102
+ options = args.extract_options!
103
+ args.each do |columns|
104
+ field = Field.new(source, FauxColumn.coerce(columns), options)
105
+
106
+ add_sort_attribute field, options if field.sortable
107
+ add_facet_attribute field, options if field.faceted
108
+ end
109
+ end
110
+
111
+ # This is the method to add attributes to your index (hence why it is
112
+ # aliased as 'attribute'). The syntax is the same as #indexes, so use
113
+ # that as starting point, but keep in mind the following points.
114
+ #
115
+ # An attribute can have an alias (the :as option), but it is always
116
+ # sortable - so you don't need to explicitly request that. You _can_
117
+ # specify the data type of the attribute (the :type option), but the
118
+ # code's pretty good at figuring that out itself from peering into the
119
+ # database.
120
+ #
121
+ # Attributes are limited to the following types: integers, floats,
122
+ # datetimes (converted to timestamps), booleans, strings and MVAs
123
+ # (:multi). Don't forget that Sphinx converts string attributes to
124
+ # integers, which are useful for sorting, but that's about it.
125
+ #
126
+ # Collection of integers are known as multi-value attributes (MVAs).
127
+ # Generally these would be through a has_many relationship, like in this
128
+ # example:
129
+ #
130
+ # has posts(:id), :as => :post_ids
131
+ #
132
+ # This allows you to filter on any of the values tied to a specific
133
+ # record. Might be best to read through the Sphinx documentation to get
134
+ # a better idea of that though.
135
+ #
136
+ # Adding SQL Fragment Attributes
137
+ #
138
+ # You can also define an attribute using an SQL fragment, useful for
139
+ # when you would like to index a calculated value. Don't forget to set
140
+ # the type of the attribute though:
141
+ #
142
+ # has "age < 18", :as => :minor, :type => :boolean
143
+ #
144
+ # If you're creating attributes for latitude and longitude, don't
145
+ # forget that Sphinx expects these values to be in radians.
146
+ #
147
+ def has(*args)
148
+ options = args.extract_options!
149
+ args.each do |columns|
150
+ attribute = Attribute.new(source, FauxColumn.coerce(columns), options)
151
+
152
+ add_facet_attribute attribute, options if attribute.faceted
153
+ end
154
+ end
155
+
156
+ def facet(*args)
157
+ options = args.extract_options!
158
+ options[:facet] = true
159
+
160
+ args.each do |columns|
161
+ attribute = Attribute.new(source, FauxColumn.coerce(columns), options)
162
+
163
+ add_facet_attribute attribute, options
164
+ end
165
+ end
166
+
167
+ def join(*args)
168
+ args.each do |association|
169
+ Join.new(source, association)
170
+ end
171
+ end
172
+
173
+ # Use this method to add some manual SQL conditions for your index
174
+ # request. You can pass in as many strings as you like, they'll get
175
+ # joined together with ANDs later on.
176
+ #
177
+ # where "user_id = 10"
178
+ # where "parent_type = 'Article'", "created_at < NOW()"
179
+ #
180
+ def where(*args)
181
+ source.conditions += args
182
+ end
183
+
184
+ # Use this method to add some manual SQL strings to the GROUP BY
185
+ # clause. You can pass in as many strings as you'd like, they'll get
186
+ # joined together with commas later on.
187
+ #
188
+ # group_by "lat", "lng"
189
+ #
190
+ def group_by(*args)
191
+ source.groupings += args
192
+ end
193
+
194
+ # This is what to use to set properties on the index. Chief amongst
195
+ # those is the delta property - to allow automatic updates to your
196
+ # indexes as new models are added and edited - but also you can
197
+ # define search-related properties which will be the defaults for all
198
+ # searches on the model.
199
+ #
200
+ # set_property :delta => true
201
+ # set_property :field_weights => {"name" => 100}
202
+ # set_property :order => "name ASC"
203
+ # set_property :select => 'name'
204
+ #
205
+ # Also, the following two properties are particularly relevant for
206
+ # geo-location searching - latitude_attr and longitude_attr. If your
207
+ # attributes for these two values are named something other than
208
+ # lat/latitude or lon/long/longitude, you can dictate what they are
209
+ # when defining the index, so you don't need to specify them for every
210
+ # geo-related search.
211
+ #
212
+ # set_property :latitude_attr => "lt", :longitude_attr => "lg"
213
+ #
214
+ # Please don't forget to add a boolean field named 'delta' to your
215
+ # model's database table if enabling the delta index for it.
216
+ # Valid options for the delta property are:
217
+ #
218
+ # true
219
+ # false
220
+ # :default
221
+ # :delayed
222
+ # :datetime
223
+ #
224
+ # You can also extend ThinkingSphinx::Deltas::DefaultDelta to implement
225
+ # your own handling for delta indexing.
226
+ #
227
+ def set_property(*args)
228
+ options = args.extract_options!
229
+ options.each do |key, value|
230
+ set_single_property key, value
231
+ end
232
+
233
+ set_single_property args[0], args[1] if args.length == 2
234
+ end
235
+ alias_method :set_properties, :set_property
236
+
237
+ # Handles the generation of new columns for the field and attribute
238
+ # definitions.
239
+ #
240
+ def method_missing(method, *args)
241
+ FauxColumn.new(method, *args)
242
+ end
243
+
244
+ # A method to allow adding fields from associations which have names
245
+ # that clash with method names in the Builder class (ie: properties,
246
+ # fields, attributes).
247
+ #
248
+ # Example: indexes assoc(:properties).column
249
+ #
250
+ def assoc(assoc, *args)
251
+ FauxColumn.new(assoc, *args)
252
+ end
253
+
254
+ # Use this method to generate SQL for your attributes, conditions, etc.
255
+ # You can pass in as whatever ActiveRecord::Base.sanitize_sql accepts.
256
+ #
257
+ # where sanitize_sql(["active = ?", true])
258
+ # #=> WHERE active = 1
259
+ #
260
+ def sanitize_sql(*args)
261
+ @index.model.send(:sanitize_sql, *args)
262
+ end
263
+
264
+ private
265
+
266
+ def source
267
+ @source ||= begin
268
+ source = ThinkingSphinx::Source.new(@index)
269
+ @index.sources << source
270
+ source
271
+ end
272
+ end
273
+
274
+ def set_single_property(key, value)
275
+ source_options = ThinkingSphinx::Configuration::SourceOptions
276
+ if source_options.include?(key.to_s)
277
+ source.options.merge! key => value
278
+ else
279
+ @index.local_options.merge! key => value
280
+ end
281
+ end
282
+
283
+ def add_sort_attribute(field, options)
284
+ add_internal_attribute field, options, "_sort"
285
+ end
286
+
287
+ def add_facet_attribute(property, options)
288
+ add_internal_attribute property, options, "_facet", true
289
+ @index.model.sphinx_facets << property.to_facet
290
+ end
291
+
292
+ def add_internal_attribute(property, options, suffix, crc = false)
293
+ return unless ThinkingSphinx::Facet.translate?(property)
294
+
295
+ Attribute.new(source,
296
+ property.columns.collect { |col| col.clone },
297
+ options.merge(
298
+ :type => property.is_a?(Field) ? :string : options[:type],
299
+ :as => property.unique_name.to_s.concat(suffix).to_sym,
300
+ :crc => crc
301
+ ).except(:facet)
302
+ )
303
+ end
304
+
305
+ def no_fields?
306
+ @index.sources.empty? || @index.sources.any? { |source|
307
+ source.fields.length == 0
308
+ }
309
+ end
310
+ end
311
+ end
312
+ end