DrMark-thinking-sphinx 1.1.6 → 1.1.14

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 (40) hide show
  1. data/{README → README.textile} +84 -84
  2. data/lib/thinking_sphinx/active_record/attribute_updates.rb +48 -0
  3. data/lib/thinking_sphinx/active_record/delta.rb +10 -1
  4. data/lib/thinking_sphinx/active_record.rb +10 -3
  5. data/lib/thinking_sphinx/adapters/postgresql_adapter.rb +1 -1
  6. data/lib/thinking_sphinx/attribute.rb +44 -134
  7. data/lib/thinking_sphinx/class_facet.rb +15 -0
  8. data/lib/thinking_sphinx/collection.rb +1 -0
  9. data/lib/thinking_sphinx/configuration.rb +7 -3
  10. data/lib/thinking_sphinx/deltas/datetime_delta.rb +1 -1
  11. data/lib/thinking_sphinx/deltas/default_delta.rb +3 -2
  12. data/lib/thinking_sphinx/deltas/delayed_delta.rb +4 -2
  13. data/lib/thinking_sphinx/deltas.rb +9 -6
  14. data/lib/thinking_sphinx/deploy/capistrano.rb +82 -0
  15. data/lib/thinking_sphinx/facet.rb +68 -18
  16. data/lib/thinking_sphinx/facet_collection.rb +16 -17
  17. data/lib/thinking_sphinx/field.rb +7 -97
  18. data/lib/thinking_sphinx/index/builder.rb +255 -232
  19. data/lib/thinking_sphinx/index.rb +37 -349
  20. data/lib/thinking_sphinx/property.rb +160 -0
  21. data/lib/thinking_sphinx/search/facets.rb +98 -0
  22. data/lib/thinking_sphinx/search.rb +4 -73
  23. data/lib/thinking_sphinx/source/internal_properties.rb +46 -0
  24. data/lib/thinking_sphinx/source/sql.rb +124 -0
  25. data/lib/thinking_sphinx/source.rb +150 -0
  26. data/lib/thinking_sphinx/tasks.rb +1 -1
  27. data/lib/thinking_sphinx.rb +3 -1
  28. data/spec/unit/thinking_sphinx/active_record_spec.rb +14 -12
  29. data/spec/unit/thinking_sphinx/attribute_spec.rb +16 -11
  30. data/spec/unit/thinking_sphinx/facet_collection_spec.rb +64 -0
  31. data/spec/unit/thinking_sphinx/facet_spec.rb +278 -0
  32. data/spec/unit/thinking_sphinx/field_spec.rb +18 -9
  33. data/spec/unit/thinking_sphinx/index/builder_spec.rb +347 -1
  34. data/spec/unit/thinking_sphinx/index_spec.rb +22 -27
  35. data/spec/unit/thinking_sphinx/rails_additions_spec.rb +183 -0
  36. data/spec/unit/thinking_sphinx/search_spec.rb +71 -0
  37. data/spec/unit/thinking_sphinx/source_spec.rb +156 -0
  38. data/tasks/distribution.rb +1 -1
  39. data/tasks/testing.rb +7 -15
  40. metadata +19 -3
@@ -2,10 +2,10 @@ module ThinkingSphinx
2
2
  class Index
3
3
  # The Builder class is the core for the index definition block processing.
4
4
  # There are four methods you really need to pay attention to:
5
- # - indexes (aliased to includes and attribute)
6
- # - has (aliased to attribute)
5
+ # - indexes
6
+ # - has
7
7
  # - where
8
- # - set_property (aliased to set_properties)
8
+ # - set_property/set_properties
9
9
  #
10
10
  # The first two of these methods allow you to define what data makes up
11
11
  # your indexes. #where provides a method to add manual SQL conditions, and
@@ -13,251 +13,274 @@ module ThinkingSphinx
13
13
  # out each method's documentation for better ideas of usage.
14
14
  #
15
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
- HiddenMethods = [:parent, :name, :id, :type].each { |method|
22
- define_method(method) {
23
- caller.grep(/irb.completion/).empty? ? method_missing(method) : super
24
- }
16
+ instance_methods.grep(/^[^_]/).each { |method|
17
+ next if method == "instance_eval"
18
+ define_method(method) {
19
+ caller.grep(/irb.completion/).empty? ? method_missing(method) : super
25
20
  }
21
+ }
22
+
23
+ def self.generate(model, &block)
24
+ index = ThinkingSphinx::Index.new(model)
25
+ model.sphinx_facets ||= []
26
26
 
27
- attr_accessor :fields, :attributes, :properties, :conditions,
28
- :groupings
27
+ Builder.new(index, &block) if block_given?
29
28
 
30
- # Set up all the collections. Consider this the equivalent of an
31
- # instance's initialize method.
32
- #
33
- def setup
34
- @fields = []
35
- @attributes = []
36
- @properties = {}
37
- @conditions = []
38
- @groupings = []
39
- end
40
-
41
- # This is how you add fields - the strings Sphinx looks at - to your
42
- # index. Technically, to use this method, you need to pass in some
43
- # columns and options - but there's some neat method_missing stuff
44
- # happening, so lets stick to the expected syntax within a define_index
45
- # block.
46
- #
47
- # Expected options are :as, which points to a column alias in symbol
48
- # form, and :sortable, which indicates whether you want to sort by this
49
- # field.
50
- #
51
- # Adding Single-Column Fields:
52
- #
53
- # You can use symbols or methods - and can chain methods together to
54
- # get access down the associations tree.
55
- #
56
- # indexes :id, :as => :my_id
57
- # indexes :name, :sortable => true
58
- # indexes first_name, last_name, :sortable => true
59
- # indexes users.posts.content, :as => :post_content
60
- # indexes users(:id), :as => :user_ids
61
- #
62
- # Keep in mind that if any keywords for Ruby methods - such as id or
63
- # name - clash with your column names, you need to use the symbol
64
- # version (see the first, second and last examples above).
65
- #
66
- # If you specify multiple columns (example #2), a field will be created
67
- # for each. Don't use the :as option in this case. If you want to merge
68
- # those columns together, continue reading.
69
- #
70
- # Adding Multi-Column Fields:
71
- #
72
- # indexes [first_name, last_name], :as => :name
73
- # indexes [location, parent.location], :as => :location
74
- #
75
- # To combine multiple columns into a single field, you need to wrap
76
- # them in an Array, as shown by the above examples. There's no
77
- # limitations on whether they're symbols or methods or what level of
78
- # associations they come from.
79
- #
80
- # Adding SQL Fragment Fields
81
- #
82
- # You can also define a field using an SQL fragment, useful for when
83
- # you would like to index a calculated value.
84
- #
85
- # indexes "age < 18", :as => :minor
86
- #
87
- def indexes(*args)
88
- options = args.extract_options!
89
- args.each do |columns|
90
- field = Field.new(FauxColumn.coerce(columns), options)
91
- fields << field
92
-
93
- add_sort_attribute field, options if field.sortable
94
- add_facet_attribute field, options if field.faceted
95
- end
96
- end
97
- alias_method :field, :indexes
98
- alias_method :includes, :indexes
99
-
100
- # This is the method to add attributes to your index (hence why it is
101
- # aliased as 'attribute'). The syntax is the same as #indexes, so use
102
- # that as starting point, but keep in mind the following points.
103
- #
104
- # An attribute can have an alias (the :as option), but it is always
105
- # sortable - so you don't need to explicitly request that. You _can_
106
- # specify the data type of the attribute (the :type option), but the
107
- # code's pretty good at figuring that out itself from peering into the
108
- # database.
109
- #
110
- # Attributes are limited to the following types: integers, floats,
111
- # datetimes (converted to timestamps), booleans and strings. Don't
112
- # forget that Sphinx converts string attributes to integers, which are
113
- # useful for sorting, but that's about it.
114
- #
115
- # You can also have a collection of integers for multi-value attributes
116
- # (MVAs). Generally these would be through a has_many relationship,
117
- # like in this example:
118
- #
119
- # has posts(:id), :as => :post_ids
120
- #
121
- # This allows you to filter on any of the values tied to a specific
122
- # record. Might be best to read through the Sphinx documentation to get
123
- # a better idea of that though.
124
- #
125
- # Adding SQL Fragment Attributes
126
- #
127
- # You can also define an attribute using an SQL fragment, useful for
128
- # when you would like to index a calculated value. Don't forget to set
129
- # the type of the attribute though:
130
- #
131
- # has "age < 18", :as => :minor, :type => :boolean
132
- #
133
- # If you're creating attributes for latitude and longitude, don't
134
- # forget that Sphinx expects these values to be in radians.
135
- #
136
- def has(*args)
137
- options = args.extract_options!
138
- args.each do |columns|
139
- attribute = Attribute.new(FauxColumn.coerce(columns), options)
140
- attributes << attribute
141
-
142
- add_facet_attribute attribute, options if attribute.faceted
143
- end
144
- end
145
- alias_method :attribute, :has
29
+ index.delta_object = ThinkingSphinx::Deltas.parse index
30
+ index
31
+ end
32
+
33
+ def initialize(index, &block)
34
+ @index = index
35
+ @source = ThinkingSphinx::Source.new(@index)
36
+ @index.sources << @source
37
+ @explicit_source = false
146
38
 
147
- def facet(*args)
148
- options = args.extract_options!
149
- options[:facet] = true
150
-
151
- args.each do |columns|
152
- attribute = Attribute.new(FauxColumn.coerce(columns), options)
153
- attributes << attribute
154
-
155
- add_facet_attribute attribute, options
156
- end
157
- end
39
+ self.instance_eval &block
158
40
 
159
- # Use this method to add some manual SQL conditions for your index
160
- # request. You can pass in as many strings as you like, they'll get
161
- # joined together with ANDs later on.
162
- #
163
- # where "user_id = 10"
164
- # where "parent_type = 'Article'", "created_at < NOW()"
165
- #
166
- def where(*args)
167
- @conditions += args
41
+ if @index.sources.any? { |source|
42
+ source.fields.length == 0
43
+ }
44
+ raise "At least one field is necessary for an index"
168
45
  end
169
-
170
- # Use this method to add some manual SQL strings to the GROUP BY
171
- # clause. You can pass in as many strings as you'd like, they'll get
172
- # joined together with commas later on.
173
- #
174
- # group_by "lat", "lng"
175
- #
176
- def group_by(*args)
177
- @groupings += args
46
+ end
47
+
48
+ def define_source(&block)
49
+ if @explicit_source
50
+ @source = ThinkingSphinx::Source.new(@index)
51
+ @index.sources << @source
52
+ else
53
+ @explicit_source = true
178
54
  end
179
55
 
180
- # This is what to use to set properties on the index. Chief amongst
181
- # those is the delta property - to allow automatic updates to your
182
- # indexes as new models are added and edited - but also you can
183
- # define search-related properties which will be the defaults for all
184
- # searches on the model.
185
- #
186
- # set_property :delta => true
187
- # set_property :field_weights => {"name" => 100}
188
- # set_property :order => "name ASC"
189
- # set_property :include => :picture
190
- # set_property :select => 'name'
191
- #
192
- # Also, the following two properties are particularly relevant for
193
- # geo-location searching - latitude_attr and longitude_attr. If your
194
- # attributes for these two values are named something other than
195
- # lat/latitude or lon/long/longitude, you can dictate what they are
196
- # when defining the index, so you don't need to specify them for every
197
- # geo-related search.
198
- #
199
- # set_property :latitude_attr => "lt", :longitude_attr => "lg"
200
- #
201
- # Please don't forget to add a boolean field named 'delta' to your
202
- # model's database table if enabling the delta index for it.
203
- # Valid options for the delta property are:
204
- #
205
- # true
206
- # false
207
- # :default
208
- # :delayed
209
- # :datetime
210
- #
211
- # You can also extend ThinkingSphinx::Deltas::DefaultDelta to implement
212
- # your own handling for delta indexing.
213
-
214
- def set_property(*args)
215
- options = args.extract_options!
216
- if options.empty?
217
- @properties[args[0]] = args[1]
218
- else
219
- @properties.merge!(options)
220
- end
56
+ self.instance_eval &block
57
+ end
58
+
59
+ # This is how you add fields - the strings Sphinx looks at - to your
60
+ # index. Technically, to use this method, you need to pass in some
61
+ # columns and options - but there's some neat method_missing stuff
62
+ # happening, so lets stick to the expected syntax within a define_index
63
+ # block.
64
+ #
65
+ # Expected options are :as, which points to a column alias in symbol
66
+ # form, and :sortable, which indicates whether you want to sort by this
67
+ # field.
68
+ #
69
+ # Adding Single-Column Fields:
70
+ #
71
+ # You can use symbols or methods - and can chain methods together to
72
+ # get access down the associations tree.
73
+ #
74
+ # indexes :id, :as => :my_id
75
+ # indexes :name, :sortable => true
76
+ # indexes first_name, last_name, :sortable => true
77
+ # indexes users.posts.content, :as => :post_content
78
+ # indexes users(:id), :as => :user_ids
79
+ #
80
+ # Keep in mind that if any keywords for Ruby methods - such as id or
81
+ # name - clash with your column names, you need to use the symbol
82
+ # version (see the first, second and last examples above).
83
+ #
84
+ # If you specify multiple columns (example #2), a field will be created
85
+ # for each. Don't use the :as option in this case. If you want to merge
86
+ # those columns together, continue reading.
87
+ #
88
+ # Adding Multi-Column Fields:
89
+ #
90
+ # indexes [first_name, last_name], :as => :name
91
+ # indexes [location, parent.location], :as => :location
92
+ #
93
+ # To combine multiple columns into a single field, you need to wrap
94
+ # them in an Array, as shown by the above examples. There's no
95
+ # limitations on whether they're symbols or methods or what level of
96
+ # associations they come from.
97
+ #
98
+ # Adding SQL Fragment Fields
99
+ #
100
+ # You can also define a field using an SQL fragment, useful for when
101
+ # you would like to index a calculated value.
102
+ #
103
+ # indexes "age < 18", :as => :minor
104
+ #
105
+ def indexes(*args)
106
+ options = args.extract_options!
107
+ args.each do |columns|
108
+ field = Field.new(@source, FauxColumn.coerce(columns), options)
109
+
110
+ add_sort_attribute field, options if field.sortable
111
+ add_facet_attribute field, options if field.faceted
221
112
  end
222
- alias_method :set_properties, :set_property
223
-
224
- # Handles the generation of new columns for the field and attribute
225
- # definitions.
226
- #
227
- def method_missing(method, *args)
228
- FauxColumn.new(method, *args)
113
+ end
114
+
115
+ # This is the method to add attributes to your index (hence why it is
116
+ # aliased as 'attribute'). The syntax is the same as #indexes, so use
117
+ # that as starting point, but keep in mind the following points.
118
+ #
119
+ # An attribute can have an alias (the :as option), but it is always
120
+ # sortable - so you don't need to explicitly request that. You _can_
121
+ # specify the data type of the attribute (the :type option), but the
122
+ # code's pretty good at figuring that out itself from peering into the
123
+ # database.
124
+ #
125
+ # Attributes are limited to the following types: integers, floats,
126
+ # datetimes (converted to timestamps), booleans and strings. Don't
127
+ # forget that Sphinx converts string attributes to integers, which are
128
+ # useful for sorting, but that's about it.
129
+ #
130
+ # You can also have a collection of integers for multi-value attributes
131
+ # (MVAs). Generally these would be through a has_many relationship,
132
+ # like in this example:
133
+ #
134
+ # has posts(:id), :as => :post_ids
135
+ #
136
+ # This allows you to filter on any of the values tied to a specific
137
+ # record. Might be best to read through the Sphinx documentation to get
138
+ # a better idea of that though.
139
+ #
140
+ # Adding SQL Fragment Attributes
141
+ #
142
+ # You can also define an attribute using an SQL fragment, useful for
143
+ # when you would like to index a calculated value. Don't forget to set
144
+ # the type of the attribute though:
145
+ #
146
+ # has "age < 18", :as => :minor, :type => :boolean
147
+ #
148
+ # If you're creating attributes for latitude and longitude, don't
149
+ # forget that Sphinx expects these values to be in radians.
150
+ #
151
+ def has(*args)
152
+ options = args.extract_options!
153
+ args.each do |columns|
154
+ attribute = Attribute.new(@source, FauxColumn.coerce(columns), options)
155
+
156
+ add_facet_attribute attribute, options if attribute.faceted
229
157
  end
158
+ end
159
+
160
+ def facet(*args)
161
+ options = args.extract_options!
162
+ options[:facet] = true
230
163
 
231
- # A method to allow adding fields from associations which have names
232
- # that clash with method names in the Builder class (ie: properties,
233
- # fields, attributes).
234
- #
235
- # Example: indexes assoc(:properties).column
236
- #
237
- def assoc(assoc, *args)
238
- FauxColumn.new(assoc, *args)
164
+ args.each do |columns|
165
+ attribute = Attribute.new(@source, FauxColumn.coerce(columns), options)
166
+
167
+ add_facet_attribute attribute, options
239
168
  end
240
-
241
- private
242
-
243
- def add_sort_attribute(field, options)
244
- add_internal_attribute field, options, "_sort"
169
+ end
170
+
171
+ # Use this method to add some manual SQL conditions for your index
172
+ # request. You can pass in as many strings as you like, they'll get
173
+ # joined together with ANDs later on.
174
+ #
175
+ # where "user_id = 10"
176
+ # where "parent_type = 'Article'", "created_at < NOW()"
177
+ #
178
+ def where(*args)
179
+ @source.conditions += args
180
+ end
181
+
182
+ # Use this method to add some manual SQL strings to the GROUP BY
183
+ # clause. You can pass in as many strings as you'd like, they'll get
184
+ # joined together with commas later on.
185
+ #
186
+ # group_by "lat", "lng"
187
+ #
188
+ def group_by(*args)
189
+ @source.groupings += args
190
+ end
191
+
192
+ # This is what to use to set properties on the index. Chief amongst
193
+ # those is the delta property - to allow automatic updates to your
194
+ # indexes as new models are added and edited - but also you can
195
+ # define search-related properties which will be the defaults for all
196
+ # searches on the model.
197
+ #
198
+ # set_property :delta => true
199
+ # set_property :field_weights => {"name" => 100}
200
+ # set_property :order => "name ASC"
201
+ # set_property :include => :picture
202
+ # set_property :select => 'name'
203
+ #
204
+ # Also, the following two properties are particularly relevant for
205
+ # geo-location searching - latitude_attr and longitude_attr. If your
206
+ # attributes for these two values are named something other than
207
+ # lat/latitude or lon/long/longitude, you can dictate what they are
208
+ # when defining the index, so you don't need to specify them for every
209
+ # geo-related search.
210
+ #
211
+ # set_property :latitude_attr => "lt", :longitude_attr => "lg"
212
+ #
213
+ # Please don't forget to add a boolean field named 'delta' to your
214
+ # model's database table if enabling the delta index for it.
215
+ # Valid options for the delta property are:
216
+ #
217
+ # true
218
+ # false
219
+ # :default
220
+ # :delayed
221
+ # :datetime
222
+ #
223
+ # You can also extend ThinkingSphinx::Deltas::DefaultDelta to implement
224
+ # your own handling for delta indexing.
225
+ #
226
+ def set_property(*args)
227
+ options = args.extract_options!
228
+ options.each do |key, value|
229
+ set_single_property key, value
245
230
  end
246
231
 
247
- def add_facet_attribute(resource, options)
248
- add_internal_attribute resource, options, "_facet", true
232
+ set_single_property args[0], args[1] if args.length == 2
233
+ end
234
+ alias_method :set_properties, :set_property
235
+
236
+ # Handles the generation of new columns for the field and attribute
237
+ # definitions.
238
+ #
239
+ def method_missing(method, *args)
240
+ FauxColumn.new(method, *args)
241
+ end
242
+
243
+ # A method to allow adding fields from associations which have names
244
+ # that clash with method names in the Builder class (ie: properties,
245
+ # fields, attributes).
246
+ #
247
+ # Example: indexes assoc(:properties).column
248
+ #
249
+ def assoc(assoc, *args)
250
+ FauxColumn.new(assoc, *args)
251
+ end
252
+
253
+ private
254
+
255
+ def set_single_property(key, value)
256
+ source_options = ThinkingSphinx::Configuration::SourceOptions
257
+ if source_options.include?(key.to_s)
258
+ @source.options.merge! key => value
259
+ else
260
+ @index.local_options.merge! key => value
249
261
  end
262
+ end
263
+
264
+ def add_sort_attribute(field, options)
265
+ add_internal_attribute field, options, "_sort"
266
+ end
267
+
268
+ def add_facet_attribute(property, options)
269
+ add_internal_attribute property, options, "_facet", true
270
+ @index.model.sphinx_facets << property.to_facet
271
+ end
272
+
273
+ def add_internal_attribute(property, options, suffix, crc = false)
274
+ return unless ThinkingSphinx::Facet.translate?(property)
250
275
 
251
- def add_internal_attribute(resource, options, suffix, crc = false)
252
- @attributes << Attribute.new(
253
- resource.columns.collect { |col| col.clone },
254
- options.merge(
255
- :type => resource.is_a?(Field) ? :string : nil,
256
- :as => resource.unique_name.to_s.concat(suffix).to_sym,
257
- :crc => crc
258
- ).except(:facet)
259
- )
260
- end
276
+ Attribute.new(@source,
277
+ property.columns.collect { |col| col.clone },
278
+ options.merge(
279
+ :type => property.is_a?(Field) ? :string : options[:type],
280
+ :as => property.unique_name.to_s.concat(suffix).to_sym,
281
+ :crc => crc
282
+ ).except(:facet)
283
+ )
261
284
  end
262
285
  end
263
286
  end