friendlyfashion-thinking-sphinx 2.0.13

Sign up to get free protection for your applications and to get access to all the features.
Files changed (175) hide show
  1. data/HISTORY +244 -0
  2. data/LICENCE +20 -0
  3. data/README.textile +235 -0
  4. data/features/abstract_inheritance.feature +10 -0
  5. data/features/alternate_primary_key.feature +27 -0
  6. data/features/attribute_transformation.feature +22 -0
  7. data/features/attribute_updates.feature +77 -0
  8. data/features/deleting_instances.feature +67 -0
  9. data/features/direct_attributes.feature +11 -0
  10. data/features/excerpts.feature +21 -0
  11. data/features/extensible_delta_indexing.feature +9 -0
  12. data/features/facets.feature +88 -0
  13. data/features/facets_across_model.feature +29 -0
  14. data/features/field_sorting.feature +18 -0
  15. data/features/handling_edits.feature +94 -0
  16. data/features/retry_stale_indexes.feature +24 -0
  17. data/features/searching_across_models.feature +20 -0
  18. data/features/searching_by_index.feature +40 -0
  19. data/features/searching_by_model.feature +175 -0
  20. data/features/searching_with_find_arguments.feature +56 -0
  21. data/features/sphinx_detection.feature +25 -0
  22. data/features/sphinx_scopes.feature +68 -0
  23. data/features/step_definitions/alpha_steps.rb +16 -0
  24. data/features/step_definitions/beta_steps.rb +7 -0
  25. data/features/step_definitions/common_steps.rb +201 -0
  26. data/features/step_definitions/extensible_delta_indexing_steps.rb +7 -0
  27. data/features/step_definitions/facet_steps.rb +96 -0
  28. data/features/step_definitions/find_arguments_steps.rb +36 -0
  29. data/features/step_definitions/gamma_steps.rb +15 -0
  30. data/features/step_definitions/scope_steps.rb +19 -0
  31. data/features/step_definitions/search_steps.rb +94 -0
  32. data/features/step_definitions/sphinx_steps.rb +35 -0
  33. data/features/sti_searching.feature +19 -0
  34. data/features/support/env.rb +27 -0
  35. data/features/support/lib/generic_delta_handler.rb +8 -0
  36. data/features/thinking_sphinx/database.example.yml +3 -0
  37. data/features/thinking_sphinx/db/.gitignore +1 -0
  38. data/features/thinking_sphinx/db/fixtures/alphas.rb +8 -0
  39. data/features/thinking_sphinx/db/fixtures/authors.rb +1 -0
  40. data/features/thinking_sphinx/db/fixtures/betas.rb +11 -0
  41. data/features/thinking_sphinx/db/fixtures/boxes.rb +9 -0
  42. data/features/thinking_sphinx/db/fixtures/categories.rb +1 -0
  43. data/features/thinking_sphinx/db/fixtures/cats.rb +3 -0
  44. data/features/thinking_sphinx/db/fixtures/comments.rb +24 -0
  45. data/features/thinking_sphinx/db/fixtures/developers.rb +31 -0
  46. data/features/thinking_sphinx/db/fixtures/dogs.rb +3 -0
  47. data/features/thinking_sphinx/db/fixtures/extensible_betas.rb +10 -0
  48. data/features/thinking_sphinx/db/fixtures/foxes.rb +3 -0
  49. data/features/thinking_sphinx/db/fixtures/gammas.rb +10 -0
  50. data/features/thinking_sphinx/db/fixtures/music.rb +4 -0
  51. data/features/thinking_sphinx/db/fixtures/people.rb +1001 -0
  52. data/features/thinking_sphinx/db/fixtures/post_keywords.txt +1 -0
  53. data/features/thinking_sphinx/db/fixtures/posts.rb +10 -0
  54. data/features/thinking_sphinx/db/fixtures/robots.rb +8 -0
  55. data/features/thinking_sphinx/db/fixtures/tags.rb +27 -0
  56. data/features/thinking_sphinx/db/migrations/create_alphas.rb +8 -0
  57. data/features/thinking_sphinx/db/migrations/create_animals.rb +5 -0
  58. data/features/thinking_sphinx/db/migrations/create_authors.rb +3 -0
  59. data/features/thinking_sphinx/db/migrations/create_authors_posts.rb +6 -0
  60. data/features/thinking_sphinx/db/migrations/create_betas.rb +5 -0
  61. data/features/thinking_sphinx/db/migrations/create_boxes.rb +5 -0
  62. data/features/thinking_sphinx/db/migrations/create_categories.rb +3 -0
  63. data/features/thinking_sphinx/db/migrations/create_comments.rb +10 -0
  64. data/features/thinking_sphinx/db/migrations/create_developers.rb +7 -0
  65. data/features/thinking_sphinx/db/migrations/create_extensible_betas.rb +5 -0
  66. data/features/thinking_sphinx/db/migrations/create_gammas.rb +3 -0
  67. data/features/thinking_sphinx/db/migrations/create_genres.rb +3 -0
  68. data/features/thinking_sphinx/db/migrations/create_music.rb +6 -0
  69. data/features/thinking_sphinx/db/migrations/create_people.rb +13 -0
  70. data/features/thinking_sphinx/db/migrations/create_posts.rb +6 -0
  71. data/features/thinking_sphinx/db/migrations/create_robots.rb +4 -0
  72. data/features/thinking_sphinx/db/migrations/create_taggings.rb +5 -0
  73. data/features/thinking_sphinx/db/migrations/create_tags.rb +4 -0
  74. data/features/thinking_sphinx/models/alpha.rb +23 -0
  75. data/features/thinking_sphinx/models/andrew.rb +17 -0
  76. data/features/thinking_sphinx/models/animal.rb +5 -0
  77. data/features/thinking_sphinx/models/author.rb +3 -0
  78. data/features/thinking_sphinx/models/beta.rb +13 -0
  79. data/features/thinking_sphinx/models/box.rb +8 -0
  80. data/features/thinking_sphinx/models/cat.rb +3 -0
  81. data/features/thinking_sphinx/models/category.rb +4 -0
  82. data/features/thinking_sphinx/models/comment.rb +10 -0
  83. data/features/thinking_sphinx/models/developer.rb +21 -0
  84. data/features/thinking_sphinx/models/dog.rb +3 -0
  85. data/features/thinking_sphinx/models/extensible_beta.rb +9 -0
  86. data/features/thinking_sphinx/models/fox.rb +5 -0
  87. data/features/thinking_sphinx/models/gamma.rb +5 -0
  88. data/features/thinking_sphinx/models/genre.rb +3 -0
  89. data/features/thinking_sphinx/models/medium.rb +5 -0
  90. data/features/thinking_sphinx/models/music.rb +10 -0
  91. data/features/thinking_sphinx/models/person.rb +24 -0
  92. data/features/thinking_sphinx/models/post.rb +22 -0
  93. data/features/thinking_sphinx/models/robot.rb +12 -0
  94. data/features/thinking_sphinx/models/tag.rb +3 -0
  95. data/features/thinking_sphinx/models/tagging.rb +4 -0
  96. data/lib/cucumber/thinking_sphinx/external_world.rb +12 -0
  97. data/lib/cucumber/thinking_sphinx/internal_world.rb +137 -0
  98. data/lib/cucumber/thinking_sphinx/sql_logger.rb +28 -0
  99. data/lib/thinking-sphinx.rb +1 -0
  100. data/lib/thinking_sphinx/action_controller.rb +31 -0
  101. data/lib/thinking_sphinx/active_record/attribute_updates.rb +53 -0
  102. data/lib/thinking_sphinx/active_record/collection_proxy.rb +47 -0
  103. data/lib/thinking_sphinx/active_record/collection_proxy_with_scopes.rb +27 -0
  104. data/lib/thinking_sphinx/active_record/delta.rb +67 -0
  105. data/lib/thinking_sphinx/active_record/has_many_association.rb +44 -0
  106. data/lib/thinking_sphinx/active_record/has_many_association_with_scopes.rb +21 -0
  107. data/lib/thinking_sphinx/active_record/log_subscriber.rb +61 -0
  108. data/lib/thinking_sphinx/active_record/scopes.rb +110 -0
  109. data/lib/thinking_sphinx/active_record.rb +386 -0
  110. data/lib/thinking_sphinx/adapters/abstract_adapter.rb +87 -0
  111. data/lib/thinking_sphinx/adapters/mysql_adapter.rb +62 -0
  112. data/lib/thinking_sphinx/adapters/postgresql_adapter.rb +188 -0
  113. data/lib/thinking_sphinx/association.rb +230 -0
  114. data/lib/thinking_sphinx/attribute.rb +405 -0
  115. data/lib/thinking_sphinx/auto_version.rb +40 -0
  116. data/lib/thinking_sphinx/bundled_search.rb +44 -0
  117. data/lib/thinking_sphinx/class_facet.rb +20 -0
  118. data/lib/thinking_sphinx/configuration.rb +375 -0
  119. data/lib/thinking_sphinx/context.rb +76 -0
  120. data/lib/thinking_sphinx/core/string.rb +15 -0
  121. data/lib/thinking_sphinx/deltas/default_delta.rb +62 -0
  122. data/lib/thinking_sphinx/deltas.rb +28 -0
  123. data/lib/thinking_sphinx/deploy/capistrano.rb +99 -0
  124. data/lib/thinking_sphinx/excerpter.rb +23 -0
  125. data/lib/thinking_sphinx/facet.rb +135 -0
  126. data/lib/thinking_sphinx/facet_search.rb +170 -0
  127. data/lib/thinking_sphinx/field.rb +98 -0
  128. data/lib/thinking_sphinx/index/builder.rb +315 -0
  129. data/lib/thinking_sphinx/index/faux_column.rb +118 -0
  130. data/lib/thinking_sphinx/index.rb +159 -0
  131. data/lib/thinking_sphinx/join.rb +37 -0
  132. data/lib/thinking_sphinx/property.rb +187 -0
  133. data/lib/thinking_sphinx/railtie.rb +43 -0
  134. data/lib/thinking_sphinx/search.rb +1061 -0
  135. data/lib/thinking_sphinx/search_methods.rb +439 -0
  136. data/lib/thinking_sphinx/sinatra.rb +7 -0
  137. data/lib/thinking_sphinx/source/internal_properties.rb +51 -0
  138. data/lib/thinking_sphinx/source/sql.rb +174 -0
  139. data/lib/thinking_sphinx/source.rb +194 -0
  140. data/lib/thinking_sphinx/tasks.rb +142 -0
  141. data/lib/thinking_sphinx/test.rb +55 -0
  142. data/lib/thinking_sphinx/version.rb +3 -0
  143. data/lib/thinking_sphinx.rb +297 -0
  144. data/spec/fixtures/data.sql +32 -0
  145. data/spec/fixtures/database.yml.default +3 -0
  146. data/spec/fixtures/models.rb +164 -0
  147. data/spec/fixtures/structure.sql +146 -0
  148. data/spec/spec_helper.rb +61 -0
  149. data/spec/sphinx_helper.rb +60 -0
  150. data/spec/support/rails.rb +25 -0
  151. data/spec/thinking_sphinx/active_record/delta_spec.rb +122 -0
  152. data/spec/thinking_sphinx/active_record/has_many_association_spec.rb +173 -0
  153. data/spec/thinking_sphinx/active_record/scopes_spec.rb +176 -0
  154. data/spec/thinking_sphinx/active_record_spec.rb +573 -0
  155. data/spec/thinking_sphinx/adapters/abstract_adapter_spec.rb +145 -0
  156. data/spec/thinking_sphinx/association_spec.rb +250 -0
  157. data/spec/thinking_sphinx/attribute_spec.rb +552 -0
  158. data/spec/thinking_sphinx/auto_version_spec.rb +103 -0
  159. data/spec/thinking_sphinx/configuration_spec.rb +326 -0
  160. data/spec/thinking_sphinx/context_spec.rb +126 -0
  161. data/spec/thinking_sphinx/core/array_spec.rb +9 -0
  162. data/spec/thinking_sphinx/core/string_spec.rb +9 -0
  163. data/spec/thinking_sphinx/excerpter_spec.rb +49 -0
  164. data/spec/thinking_sphinx/facet_search_spec.rb +176 -0
  165. data/spec/thinking_sphinx/facet_spec.rb +359 -0
  166. data/spec/thinking_sphinx/field_spec.rb +127 -0
  167. data/spec/thinking_sphinx/index/builder_spec.rb +532 -0
  168. data/spec/thinking_sphinx/index/faux_column_spec.rb +36 -0
  169. data/spec/thinking_sphinx/index_spec.rb +189 -0
  170. data/spec/thinking_sphinx/search_methods_spec.rb +156 -0
  171. data/spec/thinking_sphinx/search_spec.rb +1455 -0
  172. data/spec/thinking_sphinx/source_spec.rb +267 -0
  173. data/spec/thinking_sphinx/test_spec.rb +20 -0
  174. data/spec/thinking_sphinx_spec.rb +204 -0
  175. metadata +524 -0
@@ -0,0 +1,439 @@
1
+ module ThinkingSphinx
2
+ module SearchMethods
3
+ def self.included(base)
4
+ base.class_eval do
5
+ extend ThinkingSphinx::SearchMethods::ClassMethods
6
+ end
7
+ end
8
+
9
+ module ClassMethods
10
+ def search_context
11
+ # Comparing to name string to avoid class inheritance complications
12
+ case self.class.name
13
+ when 'Class'
14
+ self
15
+ else
16
+ nil
17
+ end
18
+ end
19
+
20
+ # Searches through the Sphinx indexes for relevant matches. There's
21
+ # various ways to search, sort, group and filter - which are covered
22
+ # below.
23
+ #
24
+ # Also, if you have WillPaginate installed, the search method can be used
25
+ # just like paginate. The same parameters - :page and :per_page - work as
26
+ # expected, and the returned result set can be used by the will_paginate
27
+ # helper.
28
+ #
29
+ # == Basic Searching
30
+ #
31
+ # The simplest way of searching is straight text.
32
+ #
33
+ # ThinkingSphinx.search "pat"
34
+ # ThinkingSphinx.search "google"
35
+ # User.search "pat", :page => (params[:page] || 1)
36
+ # Article.search "relevant news issue of the day"
37
+ #
38
+ # If you specify :include, like in an #find call, this will be respected
39
+ # when loading the relevant models from the search results.
40
+ #
41
+ # User.search "pat", :include => :posts
42
+ #
43
+ # == Match Modes
44
+ #
45
+ # Sphinx supports 5 different matching modes. By default Thinking Sphinx
46
+ # uses :all, which unsurprisingly requires all the supplied search terms
47
+ # to match a result.
48
+ #
49
+ # Alternative modes include:
50
+ #
51
+ # User.search "pat allan", :match_mode => :any
52
+ # User.search "pat allan", :match_mode => :phrase
53
+ # User.search "pat | allan", :match_mode => :boolean
54
+ # User.search "@name pat | @username pat", :match_mode => :extended
55
+ #
56
+ # Any will find results with any of the search terms. Phrase treats the
57
+ # search terms a single phrase instead of individual words. Boolean and
58
+ # extended allow for more complex query syntax, refer to the sphinx
59
+ # documentation for further details.
60
+ #
61
+ # == Weighting
62
+ #
63
+ # Sphinx has support for weighting, where matches in one field can be
64
+ # considered more important than in another. Weights are integers, with 1
65
+ # as the default. They can be set per-search like this:
66
+ #
67
+ # User.search "pat allan", :field_weights => { :alias => 4, :aka => 2 }
68
+ #
69
+ # If you're searching multiple models, you can set per-index weights:
70
+ #
71
+ # ThinkingSphinx.search "pat", :index_weights => { User => 10 }
72
+ #
73
+ # See http://sphinxsearch.com/doc.html#weighting for further details.
74
+ #
75
+ # == Searching by Fields
76
+ #
77
+ # If you want to step it up a level, you can limit your search terms to
78
+ # specific fields:
79
+ #
80
+ # User.search :conditions => {:name => "pat"}
81
+ #
82
+ # This uses Sphinx's extended match mode, unless you specify a different
83
+ # match mode explicitly (but then this way of searching won't work). Also
84
+ # note that you don't need to put in a search string.
85
+ #
86
+ # == Searching by Attributes
87
+ #
88
+ # Also known as filters, you can limit your searches to documents that
89
+ # have specific values for their attributes. There are three ways to do
90
+ # this. The first two techniques work in all scenarios - using the :with
91
+ # or :with_all options.
92
+ #
93
+ # ThinkingSphinx.search :with => {:tag_ids => 10}
94
+ # ThinkingSphinx.search :with => {:tag_ids => [10,12]}
95
+ # ThinkingSphinx.search :with_all => {:tag_ids => [10,12]}
96
+ #
97
+ # The first :with search will match records with a tag_id attribute of 10.
98
+ # The second :with will match records with a tag_id attribute of 10 OR 12.
99
+ # If you need to find records that are tagged with ids 10 AND 12, you
100
+ # will need to use the :with_all search parameter. This is particuarly
101
+ # useful in conjunction with Multi Value Attributes (MVAs).
102
+ #
103
+ # The third filtering technique is only viable if you're searching with a
104
+ # specific model (not multi-model searching). With a single model,
105
+ # Thinking Sphinx can figure out what attributes and fields are available,
106
+ # so you can put it all in the :conditions hash, and it will sort it out.
107
+ #
108
+ # Node.search :conditions => {:parent_id => 10}
109
+ #
110
+ # Filters can be single values, arrays of values, or ranges.
111
+ #
112
+ # Article.search "East Timor", :conditions => {:rating => 3..5}
113
+ #
114
+ # == Excluding by Attributes
115
+ #
116
+ # Sphinx also supports negative filtering - where the filters are of
117
+ # attribute values to exclude. This is done with the :without option:
118
+ #
119
+ # User.search :without => {:role_id => 1}
120
+ #
121
+ # == Excluding by Primary Key
122
+ #
123
+ # There is a shortcut to exclude records by their ActiveRecord primary
124
+ # key:
125
+ #
126
+ # User.search :without_ids => 1
127
+ #
128
+ # Pass an array or a single value.
129
+ #
130
+ # The primary key must be an integer as a negative filter is used. Note
131
+ # that for multi-model search, an id may occur in more than one model.
132
+ #
133
+ # == Infix (Star) Searching
134
+ #
135
+ # Enable infix searching by something like this in config/sphinx.yml:
136
+ #
137
+ # development:
138
+ # enable_star: 1
139
+ # min_infix_len: 2
140
+ #
141
+ # Note that this will make indexing take longer.
142
+ #
143
+ # With those settings (and after reindexing), wildcard asterisks can be
144
+ # used in queries:
145
+ #
146
+ # Location.search "*elbourn*"
147
+ #
148
+ # To automatically add asterisks around every token (but not operators),
149
+ # pass the :star option:
150
+ #
151
+ # Location.search "elbourn -ustrali", :star => true,
152
+ # :match_mode => :boolean
153
+ #
154
+ # This would become "*elbourn* -*ustrali*". The :star option only adds the
155
+ # asterisks. You need to make the config/sphinx.yml changes yourself.
156
+ #
157
+ # By default, the tokens are assumed to match the regular expression
158
+ # /\w\+/u\+. If you've modified the charset_table, pass another regular
159
+ # expression, e.g.
160
+ #
161
+ # User.search("oo@bar.c", :star => /[\w@.]+/u)
162
+ #
163
+ # to search for "*oo@bar.c*" and not "*oo*@*bar*.*c*".
164
+ #
165
+ # == Sorting
166
+ #
167
+ # Sphinx can only sort by attributes, so generally you will need to avoid
168
+ # using field names in your :order option. However, if you're searching
169
+ # on a single model, and have specified some fields as sortable, you can
170
+ # use those field names and Thinking Sphinx will interpret accordingly.
171
+ # Remember: this will only happen for single-model searches, and only
172
+ # through the :order option.
173
+ #
174
+ # Location.search "Melbourne", :order => :state
175
+ # User.search :conditions => {:role_id => 2}, :order => "name ASC"
176
+ #
177
+ # Keep in mind that if you use a string, you *must* specify the direction
178
+ # (ASC or DESC) else Sphinx won't return any results. If you use a symbol
179
+ # then Thinking Sphinx assumes ASC, but if you wish to state otherwise,
180
+ # use the :sort_mode option:
181
+ #
182
+ # Location.search "Melbourne", :order => :state, :sort_mode => :desc
183
+ #
184
+ # Of course, there are other sort modes - check out the Sphinx
185
+ # documentation[http://sphinxsearch.com/doc.html] for that level of
186
+ # detail though.
187
+ #
188
+ # If desired, you can sort by a column in your model instead of a sphinx
189
+ # field or attribute. This sort only applies to the current page, so is
190
+ # most useful when performing a search with a single page of results.
191
+ #
192
+ # User.search("pat", :sql_order => "name")
193
+ #
194
+ # == Grouping
195
+ #
196
+ # For this you can use the group_by, group_clause and group_function
197
+ # options - which are all directly linked to Sphinx's expectations. No
198
+ # magic from Thinking Sphinx. It can get a little tricky, so make sure
199
+ # you read all the relevant
200
+ # documentation[http://sphinxsearch.com/doc.html#clustering] first.
201
+ #
202
+ # Grouping is done via three parameters within the options hash
203
+ # * <tt>:group_function</tt> determines the way grouping is done
204
+ # * <tt>:group_by</tt> determines the field which is used for grouping
205
+ # * <tt>:group_clause</tt> determines the sorting order
206
+ #
207
+ # As a convenience, you can also use
208
+ # * <tt>:group</tt>
209
+ # which sets :group_by and defaults to :group_function of :attr
210
+ #
211
+ # === group_function
212
+ #
213
+ # Valid values for :group_function are
214
+ # * <tt>:day</tt>, <tt>:week</tt>, <tt>:month</tt>, <tt>:year</tt> - Grouping is done by the respective timeframes.
215
+ # * <tt>:attr</tt>, <tt>:attrpair</tt> - Grouping is done by the specified attributes(s)
216
+ #
217
+ # === group_by
218
+ #
219
+ # This parameter denotes the field by which grouping is done. Note that
220
+ # the specified field must be a sphinx attribute or index.
221
+ #
222
+ # === group_clause
223
+ #
224
+ # This determines the sorting order of the groups. In a grouping search,
225
+ # the matches within a group will sorted by the <tt>:sort_mode</tt> and
226
+ # <tt>:order</tt> parameters. The group matches themselves however, will
227
+ # be sorted by <tt>:group_clause</tt>.
228
+ #
229
+ # The syntax for this is the same as an order parameter in extended sort
230
+ # mode. Namely, you can specify an SQL-like sort expression with up to 5
231
+ # attributes (including internal attributes), eg: "@relevance DESC, price
232
+ # ASC, @id DESC"
233
+ #
234
+ # === Grouping by timestamp
235
+ #
236
+ # Timestamp grouping groups off items by the day, week, month or year of
237
+ # the attribute given. In order to do this you need to define a timestamp
238
+ # attribute, which pretty much looks like the standard defintion for any
239
+ # attribute.
240
+ #
241
+ # define_index do
242
+ # #
243
+ # # All your other stuff
244
+ # #
245
+ # has :created_at
246
+ # end
247
+ #
248
+ # When you need to fire off your search, it'll go something to the tune of
249
+ #
250
+ # Fruit.search "apricot", :group_function => :day,
251
+ # :group_by => 'created_at'
252
+ #
253
+ # The <tt>@groupby</tt> special attribute will contain the date for that
254
+ # group. Depending on the <tt>:group_function</tt> parameter, the date
255
+ # format will be:
256
+ #
257
+ # * <tt>:day</tt> - YYYYMMDD
258
+ # * <tt>:week</tt> - YYYYNNN (NNN is the first day of the week in question,
259
+ # counting from the start of the year )
260
+ # * <tt>:month</tt> - YYYYMM
261
+ # * <tt>:year</tt> - YYYY
262
+ #
263
+ # === Grouping by attribute
264
+ #
265
+ # The syntax is the same as grouping by timestamp, except for the fact
266
+ # that the <tt>:group_function</tt> parameter is changed.
267
+ #
268
+ # Fruit.search "apricot", :group_function => :attr, :group_by => 'size'
269
+ #
270
+ # == Geo/Location Searching
271
+ #
272
+ # Sphinx - and therefore Thinking Sphinx - has the facility to search
273
+ # around a geographical point, using a given latitude and longitude. To
274
+ # take advantage of this, you will need to have both of those values in
275
+ # attributes. To search with that point, you can then use one of the
276
+ # following syntax examples:
277
+ #
278
+ # Address.search "Melbourne", :geo => [1.4, -2.217],
279
+ # :order => "@geodist asc"
280
+ # Address.search "Australia", :geo => [-0.55, 3.108],
281
+ # :order => "@geodist asc" :latitude_attr => "latit",
282
+ # :longitude_attr => "longit"
283
+ #
284
+ # The first example applies when your latitude and longitude attributes
285
+ # are named any of lat, latitude, lon, long or longitude. If that's not
286
+ # the case, you will need to explicitly state them in your search, _or_
287
+ # you can do so in your model:
288
+ #
289
+ # define_index do
290
+ # has :latit # Float column, stored in radians
291
+ # has :longit # Float column, stored in radians
292
+ #
293
+ # set_property :latitude_attr => "latit"
294
+ # set_property :longitude_attr => "longit"
295
+ # end
296
+ #
297
+ # Now, geo-location searching really only has an affect if you have a
298
+ # filter, sort or grouping clause related to it - otherwise it's just a
299
+ # normal search, and _will not_ return a distance value otherwise. To
300
+ # make use of the positioning difference, use the special attribute
301
+ # "@geodist" in any of your filters or sorting or grouping clauses.
302
+ #
303
+ # And don't forget - both the latitude and longitude you use in your
304
+ # search, and the values in your indexes, need to be stored as a float in
305
+ # radians, _not_ degrees. Keep in mind that if you do this conversion in
306
+ # SQL you will need to explicitly declare a column type of :float.
307
+ #
308
+ # define_index do
309
+ # has 'RADIANS(lat)', :as => :lat, :type => :float
310
+ # # ...
311
+ # end
312
+ #
313
+ # Once you've got your results set, you can access the distances as
314
+ # follows:
315
+ #
316
+ # @results.each_with_geodist do |result, distance|
317
+ # # ...
318
+ # end
319
+ #
320
+ # The distance value is returned as a float, representing the distance in
321
+ # metres.
322
+ #
323
+ # == Filtering by custom attributes
324
+ #
325
+ # Do note that this applies only to sphinx 0.9.9
326
+ #
327
+ # Should you find yourself in desperate need of a filter that involves
328
+ # selecting either one of multiple conditions, one solution could be
329
+ # provided by the :sphinx_select option within the search.
330
+ # This handles which fields are selected by sphinx from its store.
331
+ #
332
+ # The default value is "*", and you can add custom fields using syntax
333
+ # similar to sql:
334
+ #
335
+ # Flower.search "foo",
336
+ # :sphinx_select => "*, petals < 1 or color = 2 as grass"
337
+ #
338
+ # This will add the 'grass' attribute, which will now be usable in your
339
+ # filters.
340
+ #
341
+ # == Handling a Stale Index
342
+ #
343
+ # Especially if you don't use delta indexing, you risk having records in
344
+ # the Sphinx index that are no longer in the database. By default, those
345
+ # will simply come back as nils:
346
+ #
347
+ # >> pat_user.delete
348
+ # >> User.search("pat")
349
+ # Sphinx Result: [1,2]
350
+ # => [nil, <#User id: 2>]
351
+ #
352
+ # (If you search across multiple models, you'll get
353
+ # ActiveRecord::RecordNotFound.)
354
+ #
355
+ # You can simply Array#compact these results or handle the nils in some
356
+ # other way, but Sphinx will still report two results, and the missing
357
+ # records may upset your layout.
358
+ #
359
+ # If you pass :retry_stale => true to a single-model search, missing
360
+ # records will cause Thinking Sphinx to retry the query but excluding
361
+ # those records. Since search is paginated, the new search could
362
+ # potentially include missing records as well, so by default Thinking
363
+ # Sphinx will retry three times. Pass :retry_stale => 5 to retry five
364
+ # times, and so on. If there are still missing ids on the last retry, they
365
+ # are shown as nils.
366
+ #
367
+ def search(*args)
368
+ ThinkingSphinx::Search.new *search_options(args)
369
+ end
370
+
371
+ # Searches for results that match the parameters provided. Will only
372
+ # return the ids for the matching objects. See #search for syntax
373
+ # examples.
374
+ #
375
+ # Note that this only searches the Sphinx index, with no ActiveRecord
376
+ # queries. Thus, if your index is not in sync with the database, this
377
+ # method may return ids that no longer exist there.
378
+ #
379
+ def search_for_ids(*args)
380
+ ThinkingSphinx::Search.new *search_options(args, :ids_only => true)
381
+ end
382
+
383
+ # Checks if a document with the given id exists within a specific index.
384
+ # Expected parameters:
385
+ #
386
+ # - ID of the document
387
+ # - Index to check within
388
+ # - Options hash (defaults to {})
389
+ #
390
+ # Example:
391
+ #
392
+ # ThinkingSphinx.search_for_id(10, "user_core", :class => User)
393
+ #
394
+ def search_for_id(id, index, options = {})
395
+ ThinkingSphinx::Search.new(
396
+ *search_options([],
397
+ :ids_only => true,
398
+ :index => index,
399
+ :id_range => id..id
400
+ )
401
+ ).any?
402
+ end
403
+
404
+ def count(*args)
405
+ search_context ? super : search_count(*args)
406
+ end
407
+
408
+ def search_count(*args)
409
+ search = ThinkingSphinx::Search.new(
410
+ *search_options(args, :ids_only => true)
411
+ )
412
+ search.first # forces the query
413
+ search.total_entries
414
+ end
415
+
416
+ # Model.facets *args
417
+ # ThinkingSphinx.facets *args
418
+ # ThinkingSphinx.facets *args, :all_facets => true
419
+ # ThinkingSphinx.facets *args, :class_facet => false
420
+ #
421
+ def facets(*args)
422
+ ThinkingSphinx::FacetSearch.new *search_options(args)
423
+ end
424
+
425
+ private
426
+
427
+ def search_options(args, options = {})
428
+ options = args.extract_options!.merge(options)
429
+ options[:classes] ||= classes_option
430
+ args << options
431
+ end
432
+
433
+ def classes_option
434
+ classes_option = [search_context].compact
435
+ classes_option.empty? ? nil : classes_option
436
+ end
437
+ end
438
+ end
439
+ end
@@ -0,0 +1,7 @@
1
+ require 'thinking_sphinx'
2
+
3
+ ThinkingSphinx::Configuration.instance
4
+
5
+ ActiveSupport.on_load :active_record do
6
+ include ThinkingSphinx::ActiveRecord
7
+ end
@@ -0,0 +1,51 @@
1
+ module ThinkingSphinx
2
+ class Source
3
+ module InternalProperties
4
+ def add_internal_attributes_and_facets
5
+ add_internal_attribute :sphinx_internal_id, nil,
6
+ @model.primary_key_for_sphinx.to_sym
7
+ add_internal_attribute :sphinx_deleted, :integer, "0"
8
+ add_internal_attribute :class_crc, :integer, crc_column, true
9
+
10
+ unless Riddle.loaded_version.to_i < 2
11
+ add_internal_attribute :sphinx_internal_class, :string, internal_class_column, true
12
+ add_internal_facet :sphinx_internal_class
13
+ else
14
+ add_internal_facet :class_crc
15
+ end
16
+ end
17
+
18
+ def add_internal_attribute(name, type, contents, facet = false)
19
+ return unless attribute_by_alias(name).nil?
20
+
21
+ Attribute.new(self,
22
+ ThinkingSphinx::Index::FauxColumn.new(contents),
23
+ :type => type,
24
+ :as => name,
25
+ :facet => facet,
26
+ :admin => true
27
+ )
28
+ end
29
+
30
+ def add_internal_facet(name)
31
+ return unless facet_by_alias(name).nil?
32
+
33
+ @model.sphinx_facets << ClassFacet.new(attribute_by_alias(name))
34
+ end
35
+
36
+ def attribute_by_alias(attr_alias)
37
+ @attributes.detect { |attrib| attrib.alias == attr_alias }
38
+ end
39
+
40
+ def facet_by_alias(name)
41
+ @model.sphinx_facets.detect { |facet| facet.name == name }
42
+ end
43
+
44
+ def subclasses_to_s
45
+ "'" + (@model.send(:descendants).collect { |klass|
46
+ klass.to_crc32.to_s
47
+ } << @model.to_crc32.to_s).join(",") + "'"
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,174 @@
1
+ module ThinkingSphinx
2
+ class Source
3
+ module SQL
4
+ # Generates the big SQL statement to get the data back for all the fields
5
+ # and attributes, using all the relevant association joins. If you want
6
+ # the version filtered for delta values, send through :delta => true in
7
+ # the options. Won't do much though if the index isn't set up to support a
8
+ # delta sibling.
9
+ #
10
+ # Examples:
11
+ #
12
+ # source.to_sql
13
+ # source.to_sql(:delta => true)
14
+ #
15
+ def to_sql(options={})
16
+ relation = @model.unscoped
17
+ pre_select = 'SQL_NO_CACHE ' if adapter.sphinx_identifier == "mysql"
18
+ relation = relation.select(
19
+ pre_select.to_s + sql_select_clause(options[:offset])
20
+ )
21
+
22
+ all_associations.each do |assoc|
23
+ relation = relation.joins(assoc.arel_join)
24
+ end
25
+
26
+ relation = relation.where(sql_where_clause(options))
27
+ relation = relation.group(sql_group_clause)
28
+ relation = relation.order('NULL') if adapter.sphinx_identifier == "mysql"
29
+ relation.to_sql
30
+ end
31
+
32
+ # Simple helper method for the query range SQL - which is a statement that
33
+ # returns minimum and maximum id values. These can be filtered by delta -
34
+ # so pass in :delta => true to get the delta version of the SQL.
35
+ #
36
+ def to_sql_query_range(options={})
37
+ return nil if @index.options[:disable_range]
38
+
39
+ min_statement = adapter.convert_nulls(
40
+ "MIN(#{quote_column(@model.primary_key_for_sphinx)})", 1
41
+ )
42
+ max_statement = adapter.convert_nulls(
43
+ "MAX(#{quote_column(@model.primary_key_for_sphinx)})", 1
44
+ )
45
+
46
+ sql = "SELECT #{min_statement}, #{max_statement} " +
47
+ "FROM #{@model.quoted_table_name} "
48
+ if self.delta? && !@index.delta_object.clause(@model, options[:delta]).blank?
49
+ sql << "WHERE #{@index.delta_object.clause(@model, options[:delta])}"
50
+ end
51
+
52
+ sql
53
+ end
54
+
55
+ # Simple helper method for the query info SQL - which is a statement that
56
+ # returns the single row for a corresponding id.
57
+ #
58
+ def to_sql_query_info(offset)
59
+ "SELECT * FROM #{@model.quoted_table_name} WHERE " +
60
+ "#{quote_column(@model.primary_key_for_sphinx)} = (($id - #{offset}) / #{ThinkingSphinx.context.indexed_models.size})"
61
+ end
62
+
63
+ def sql_select_clause(offset)
64
+ unique_id_expr = ThinkingSphinx.unique_id_expression(adapter, offset)
65
+
66
+ (
67
+ ["#{@model.quoted_table_name}.#{quote_column(@model.primary_key_for_sphinx)} #{unique_id_expr} AS #{quote_column(@model.primary_key_for_sphinx)} "] +
68
+ @fields.collect { |field| field.to_select_sql } +
69
+ @attributes.collect { |attribute| attribute.to_select_sql }
70
+ ).compact.join(", ")
71
+ end
72
+
73
+ def sql_where_clause(options)
74
+ logic = []
75
+ logic += [
76
+ "#{@model.quoted_table_name}.#{quote_column(@model.primary_key_for_sphinx)} >= $start",
77
+ "#{@model.quoted_table_name}.#{quote_column(@model.primary_key_for_sphinx)} <= $end"
78
+ ] unless @index.options[:disable_range]
79
+
80
+ if self.delta? && !@index.delta_object.clause(@model, options[:delta]).blank?
81
+ logic << "#{@index.delta_object.clause(@model, options[:delta])}"
82
+ end
83
+
84
+ logic += (@conditions || [])
85
+ logic.join(' AND ')
86
+ end
87
+
88
+ def sql_group_clause
89
+ internal_groupings = []
90
+ if @model.column_names.include?(@model.inheritance_column)
91
+ internal_groupings << "#{@model.quoted_table_name}.#{quote_column(@model.inheritance_column)}"
92
+ end
93
+
94
+ (
95
+ ["#{@model.quoted_table_name}.#{quote_column(@model.primary_key_for_sphinx)}"] +
96
+ @fields.collect { |field| field.to_group_sql }.compact +
97
+ @attributes.collect { |attribute| attribute.to_group_sql }.compact +
98
+ @groupings + internal_groupings
99
+ ).join(", ")
100
+ end
101
+
102
+ def sql_query_pre_for_core
103
+ if self.delta? && !@index.delta_object.reset_query(@model).blank?
104
+ [@index.delta_object.reset_query(@model)]
105
+ else
106
+ []
107
+ end
108
+ end
109
+
110
+ def sql_query_pre_for_delta
111
+ [""]
112
+ end
113
+
114
+ def quote_column(column)
115
+ @model.connection.quote_column_name(column)
116
+ end
117
+
118
+ def crc_column
119
+ if @model.table_exists? &&
120
+ @model.column_names.include?(@model.inheritance_column)
121
+
122
+ types = types_to_crcs
123
+ return @model.to_crc32.to_s if types.empty?
124
+
125
+ adapter.case(adapter.convert_nulls(
126
+ adapter.quote_with_table(@model.inheritance_column), @model.name),
127
+ types, @model.to_crc32)
128
+ else
129
+ @model.to_crc32.to_s
130
+ end
131
+ end
132
+
133
+ def internal_class_column
134
+ quoted_name = "'#{@model.name}'"
135
+
136
+ if @model.table_exists? &&
137
+ @model.column_names.include?(@model.inheritance_column)
138
+
139
+ types = types_to_hash
140
+ return quoted_name if types.empty?
141
+
142
+ adapter.case(adapter.convert_nulls(
143
+ adapter.quote_with_table(@model.inheritance_column), @model.name),
144
+ types, quoted_name)
145
+ else
146
+ quoted_name
147
+ end
148
+ end
149
+
150
+ def type_values
151
+ return @model.sphinx_types unless @model.sphinx_types.nil?
152
+
153
+ @model.connection.select_values <<-SQL
154
+ SELECT DISTINCT #{@model.inheritance_column}
155
+ FROM #{@model.table_name}
156
+ SQL
157
+ end
158
+
159
+ def types_to_crcs
160
+ type_values.compact.inject({}) { |hash, type|
161
+ hash[type] = type.to_crc32
162
+ hash
163
+ }
164
+ end
165
+
166
+ def types_to_hash
167
+ type_values.compact.inject({}) { |hash, type|
168
+ hash[type] = "'#{type}'"
169
+ hash
170
+ }
171
+ end
172
+ end
173
+ end
174
+ end