pixeltrix-thinking-sphinx 1.1.5 → 1.2.1

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 (76) hide show
  1. data/README.textile +147 -0
  2. data/lib/thinking_sphinx/active_record/attribute_updates.rb +48 -0
  3. data/lib/thinking_sphinx/active_record/delta.rb +14 -1
  4. data/lib/thinking_sphinx/active_record/scopes.rb +37 -0
  5. data/lib/thinking_sphinx/active_record.rb +46 -12
  6. data/lib/thinking_sphinx/adapters/abstract_adapter.rb +9 -1
  7. data/lib/thinking_sphinx/adapters/mysql_adapter.rb +3 -2
  8. data/lib/thinking_sphinx/adapters/postgresql_adapter.rb +12 -5
  9. data/lib/thinking_sphinx/association.rb +20 -0
  10. data/lib/thinking_sphinx/attribute.rb +187 -116
  11. data/lib/thinking_sphinx/class_facet.rb +15 -0
  12. data/lib/thinking_sphinx/configuration.rb +46 -14
  13. data/lib/thinking_sphinx/core/string.rb +3 -10
  14. data/lib/thinking_sphinx/deltas/datetime_delta.rb +3 -3
  15. data/lib/thinking_sphinx/deltas/default_delta.rb +9 -6
  16. data/lib/thinking_sphinx/deltas/delayed_delta/delta_job.rb +1 -1
  17. data/lib/thinking_sphinx/deltas/delayed_delta.rb +4 -2
  18. data/lib/thinking_sphinx/deltas.rb +14 -6
  19. data/lib/thinking_sphinx/deploy/capistrano.rb +98 -0
  20. data/lib/thinking_sphinx/excerpter.rb +22 -0
  21. data/lib/thinking_sphinx/facet.rb +68 -18
  22. data/lib/thinking_sphinx/facet_search.rb +134 -0
  23. data/lib/thinking_sphinx/field.rb +7 -97
  24. data/lib/thinking_sphinx/index/builder.rb +255 -201
  25. data/lib/thinking_sphinx/index.rb +28 -343
  26. data/lib/thinking_sphinx/property.rb +160 -0
  27. data/lib/thinking_sphinx/rails_additions.rb +7 -4
  28. data/lib/thinking_sphinx/search.rb +593 -587
  29. data/lib/thinking_sphinx/search_methods.rb +421 -0
  30. data/lib/thinking_sphinx/source/internal_properties.rb +46 -0
  31. data/lib/thinking_sphinx/source/sql.rb +128 -0
  32. data/lib/thinking_sphinx/source.rb +150 -0
  33. data/lib/thinking_sphinx/tasks.rb +45 -11
  34. data/lib/thinking_sphinx.rb +88 -14
  35. data/rails/init.rb +14 -0
  36. data/spec/{unit → lib}/thinking_sphinx/active_record/delta_spec.rb +7 -7
  37. data/spec/{unit → lib}/thinking_sphinx/active_record/has_many_association_spec.rb +0 -0
  38. data/spec/lib/thinking_sphinx/active_record/scopes_spec.rb +92 -0
  39. data/spec/{unit → lib}/thinking_sphinx/active_record_spec.rb +115 -42
  40. data/spec/{unit → lib}/thinking_sphinx/association_spec.rb +4 -5
  41. data/spec/lib/thinking_sphinx/attribute_spec.rb +465 -0
  42. data/spec/{unit → lib}/thinking_sphinx/configuration_spec.rb +118 -7
  43. data/spec/{unit → lib}/thinking_sphinx/core/string_spec.rb +0 -0
  44. data/spec/lib/thinking_sphinx/excerpter_spec.rb +49 -0
  45. data/spec/lib/thinking_sphinx/facet_search_spec.rb +176 -0
  46. data/spec/lib/thinking_sphinx/facet_spec.rb +302 -0
  47. data/spec/{unit → lib}/thinking_sphinx/field_spec.rb +26 -17
  48. data/spec/lib/thinking_sphinx/index/builder_spec.rb +355 -0
  49. data/spec/{unit → lib}/thinking_sphinx/index/faux_column_spec.rb +0 -0
  50. data/spec/{unit → lib}/thinking_sphinx/index_spec.rb +3 -12
  51. data/spec/lib/thinking_sphinx/rails_additions_spec.rb +191 -0
  52. data/spec/lib/thinking_sphinx/search_methods_spec.rb +152 -0
  53. data/spec/lib/thinking_sphinx/search_spec.rb +887 -0
  54. data/spec/lib/thinking_sphinx/source_spec.rb +217 -0
  55. data/spec/{unit → lib}/thinking_sphinx_spec.rb +30 -8
  56. data/tasks/distribution.rb +20 -1
  57. data/tasks/testing.rb +7 -15
  58. data/vendor/after_commit/init.rb +3 -0
  59. data/vendor/after_commit/lib/after_commit/active_record.rb +27 -4
  60. data/vendor/after_commit/lib/after_commit/connection_adapters.rb +1 -1
  61. data/vendor/after_commit/lib/after_commit.rb +4 -1
  62. data/vendor/riddle/lib/riddle/client/message.rb +4 -3
  63. data/vendor/riddle/lib/riddle/client.rb +3 -0
  64. data/vendor/riddle/lib/riddle/configuration/section.rb +8 -2
  65. data/vendor/riddle/lib/riddle/controller.rb +1 -1
  66. data/vendor/riddle/lib/riddle.rb +1 -1
  67. metadata +75 -39
  68. data/README +0 -107
  69. data/lib/thinking_sphinx/active_record/search.rb +0 -57
  70. data/lib/thinking_sphinx/collection.rb +0 -142
  71. data/lib/thinking_sphinx/facet_collection.rb +0 -44
  72. data/spec/unit/thinking_sphinx/active_record/search_spec.rb +0 -107
  73. data/spec/unit/thinking_sphinx/attribute_spec.rb +0 -212
  74. data/spec/unit/thinking_sphinx/collection_spec.rb +0 -14
  75. data/spec/unit/thinking_sphinx/index/builder_spec.rb +0 -5
  76. data/spec/unit/thinking_sphinx/search_spec.rb +0 -59
@@ -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,220 +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.to_s == "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
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
40
38
 
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
- fields << Field.new(FauxColumn.coerce(columns), options)
91
-
92
- if fields.last.sortable || fields.last.faceted
93
- attributes << Attribute.new(
94
- fields.last.columns.collect { |col| col.clone },
95
- options.merge(
96
- :type => :string,
97
- :as => fields.last.unique_name.to_s.concat("_sort").to_sym
98
- ).except(:facet)
99
- )
100
- end
101
- end
102
- end
103
- alias_method :field, :indexes
104
- alias_method :includes, :indexes
39
+ self.instance_eval &block
105
40
 
106
- # This is the method to add attributes to your index (hence why it is
107
- # aliased as 'attribute'). The syntax is the same as #indexes, so use
108
- # that as starting point, but keep in mind the following points.
109
- #
110
- # An attribute can have an alias (the :as option), but it is always
111
- # sortable - so you don't need to explicitly request that. You _can_
112
- # specify the data type of the attribute (the :type option), but the
113
- # code's pretty good at figuring that out itself from peering into the
114
- # database.
115
- #
116
- # Attributes are limited to the following types: integers, floats,
117
- # datetimes (converted to timestamps), booleans and strings. Don't
118
- # forget that Sphinx converts string attributes to integers, which are
119
- # useful for sorting, but that's about it.
120
- #
121
- # You can also have a collection of integers for multi-value attributes
122
- # (MVAs). Generally these would be through a has_many relationship,
123
- # like in this example:
124
- #
125
- # has posts(:id), :as => :post_ids
126
- #
127
- # This allows you to filter on any of the values tied to a specific
128
- # record. Might be best to read through the Sphinx documentation to get
129
- # a better idea of that though.
130
- #
131
- # Adding SQL Fragment Attributes
132
- #
133
- # You can also define an attribute using an SQL fragment, useful for
134
- # when you would like to index a calculated value. Don't forget to set
135
- # the type of the attribute though:
136
- #
137
- # has "age < 18", :as => :minor, :type => :boolean
138
- #
139
- # If you're creating attributes for latitude and longitude, don't
140
- # forget that Sphinx expects these values to be in radians.
141
- #
142
- def has(*args)
143
- options = args.extract_options!
144
- args.each do |columns|
145
- attributes << Attribute.new(FauxColumn.coerce(columns), options)
146
- end
41
+ if @index.sources.any? { |source|
42
+ source.fields.length == 0
43
+ }
44
+ raise "At least one field is necessary for an index"
45
+ end
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
147
54
  end
148
- alias_method :attribute, :has
149
55
 
150
- def facet(*args)
151
- options = args.extract_options!
152
- options[:facet] = true
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)
153
109
 
154
- args.each do |columns|
155
- attributes << Attribute.new(FauxColumn.coerce(columns), options)
156
- end
110
+ add_sort_attribute field, options if field.sortable
111
+ add_facet_attribute field, options if field.faceted
157
112
  end
158
-
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
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
168
157
  end
158
+ end
159
+
160
+ def facet(*args)
161
+ options = args.extract_options!
162
+ options[:facet] = true
169
163
 
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
164
+ args.each do |columns|
165
+ attribute = Attribute.new(@source, FauxColumn.coerce(columns), options)
166
+
167
+ add_facet_attribute attribute, options
178
168
  end
179
-
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
- #
204
- def set_property(*args)
205
- options = args.extract_options!
206
- if options.empty?
207
- @properties[args[0]] = args[1]
208
- else
209
- @properties.merge!(options)
210
- end
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
211
230
  end
212
- alias_method :set_properties, :set_property
213
231
 
214
- # Handles the generation of new columns for the field and attribute
215
- # definitions.
216
- #
217
- def method_missing(method, *args)
218
- FauxColumn.new(method, *args)
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
219
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)
220
275
 
221
- # A method to allow adding fields from associations which have names
222
- # that clash with method names in the Builder class (ie: properties,
223
- # fields, attributes).
224
- #
225
- # Example: indexes assoc(:properties).column
226
- #
227
- def assoc(assoc)
228
- FauxColumn.new(method)
229
- 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
+ )
230
284
  end
231
285
  end
232
286
  end