jbasdf-muck-solr 0.4.0

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 (156) hide show
  1. data/CHANGE_LOG +239 -0
  2. data/LICENSE +19 -0
  3. data/README.markdown +118 -0
  4. data/README.rdoc +107 -0
  5. data/Rakefile +99 -0
  6. data/TESTING_THE_PLUGIN +25 -0
  7. data/VERSION.yml +4 -0
  8. data/config/solr.yml +15 -0
  9. data/config/solr_environment.rb +32 -0
  10. data/lib/acts_as_solr/acts_methods.rb +352 -0
  11. data/lib/acts_as_solr/class_methods.rb +236 -0
  12. data/lib/acts_as_solr/common_methods.rb +89 -0
  13. data/lib/acts_as_solr/deprecation.rb +61 -0
  14. data/lib/acts_as_solr/instance_methods.rb +165 -0
  15. data/lib/acts_as_solr/lazy_document.rb +18 -0
  16. data/lib/acts_as_solr/parser_methods.rb +203 -0
  17. data/lib/acts_as_solr/search_results.rb +68 -0
  18. data/lib/acts_as_solr/solr_fixtures.rb +13 -0
  19. data/lib/acts_as_solr/tasks/database.rake +16 -0
  20. data/lib/acts_as_solr/tasks/solr.rake +135 -0
  21. data/lib/acts_as_solr/tasks/test.rake +5 -0
  22. data/lib/acts_as_solr/tasks.rb +10 -0
  23. data/lib/acts_as_solr.rb +65 -0
  24. data/lib/solr/connection.rb +177 -0
  25. data/lib/solr/document.rb +75 -0
  26. data/lib/solr/exception.rb +13 -0
  27. data/lib/solr/field.rb +36 -0
  28. data/lib/solr/importer/array_mapper.rb +26 -0
  29. data/lib/solr/importer/delimited_file_source.rb +38 -0
  30. data/lib/solr/importer/hpricot_mapper.rb +27 -0
  31. data/lib/solr/importer/mapper.rb +51 -0
  32. data/lib/solr/importer/solr_source.rb +41 -0
  33. data/lib/solr/importer/xpath_mapper.rb +35 -0
  34. data/lib/solr/importer.rb +19 -0
  35. data/lib/solr/indexer.rb +52 -0
  36. data/lib/solr/request/add_document.rb +58 -0
  37. data/lib/solr/request/base.rb +36 -0
  38. data/lib/solr/request/commit.rb +29 -0
  39. data/lib/solr/request/delete.rb +48 -0
  40. data/lib/solr/request/dismax.rb +46 -0
  41. data/lib/solr/request/index_info.rb +22 -0
  42. data/lib/solr/request/modify_document.rb +46 -0
  43. data/lib/solr/request/optimize.rb +19 -0
  44. data/lib/solr/request/ping.rb +36 -0
  45. data/lib/solr/request/select.rb +54 -0
  46. data/lib/solr/request/spellcheck.rb +30 -0
  47. data/lib/solr/request/standard.rb +402 -0
  48. data/lib/solr/request/update.rb +23 -0
  49. data/lib/solr/request.rb +26 -0
  50. data/lib/solr/response/add_document.rb +17 -0
  51. data/lib/solr/response/base.rb +42 -0
  52. data/lib/solr/response/commit.rb +15 -0
  53. data/lib/solr/response/delete.rb +13 -0
  54. data/lib/solr/response/dismax.rb +8 -0
  55. data/lib/solr/response/index_info.rb +26 -0
  56. data/lib/solr/response/modify_document.rb +17 -0
  57. data/lib/solr/response/optimize.rb +14 -0
  58. data/lib/solr/response/ping.rb +26 -0
  59. data/lib/solr/response/ruby.rb +42 -0
  60. data/lib/solr/response/select.rb +17 -0
  61. data/lib/solr/response/spellcheck.rb +20 -0
  62. data/lib/solr/response/standard.rb +60 -0
  63. data/lib/solr/response/xml.rb +39 -0
  64. data/lib/solr/response.rb +27 -0
  65. data/lib/solr/solrtasks.rb +27 -0
  66. data/lib/solr/util.rb +32 -0
  67. data/lib/solr/xml.rb +44 -0
  68. data/lib/solr.rb +26 -0
  69. data/solr/CHANGES.txt +1207 -0
  70. data/solr/LICENSE.txt +712 -0
  71. data/solr/NOTICE.txt +90 -0
  72. data/solr/etc/jetty.xml +205 -0
  73. data/solr/etc/webdefault.xml +379 -0
  74. data/solr/lib/easymock.jar +0 -0
  75. data/solr/lib/jetty-6.1.3.jar +0 -0
  76. data/solr/lib/jetty-util-6.1.3.jar +0 -0
  77. data/solr/lib/jsp-2.1/ant-1.6.5.jar +0 -0
  78. data/solr/lib/jsp-2.1/core-3.1.1.jar +0 -0
  79. data/solr/lib/jsp-2.1/jsp-2.1.jar +0 -0
  80. data/solr/lib/jsp-2.1/jsp-api-2.1.jar +0 -0
  81. data/solr/lib/servlet-api-2.4.jar +0 -0
  82. data/solr/lib/servlet-api-2.5-6.1.3.jar +0 -0
  83. data/solr/lib/xpp3-1.1.3.4.O.jar +0 -0
  84. data/solr/solr/README.txt +52 -0
  85. data/solr/solr/bin/abc +176 -0
  86. data/solr/solr/bin/abo +176 -0
  87. data/solr/solr/bin/backup +108 -0
  88. data/solr/solr/bin/backupcleaner +142 -0
  89. data/solr/solr/bin/commit +128 -0
  90. data/solr/solr/bin/optimize +129 -0
  91. data/solr/solr/bin/readercycle +129 -0
  92. data/solr/solr/bin/rsyncd-disable +77 -0
  93. data/solr/solr/bin/rsyncd-enable +76 -0
  94. data/solr/solr/bin/rsyncd-start +145 -0
  95. data/solr/solr/bin/rsyncd-stop +105 -0
  96. data/solr/solr/bin/scripts-util +83 -0
  97. data/solr/solr/bin/snapcleaner +148 -0
  98. data/solr/solr/bin/snapinstaller +168 -0
  99. data/solr/solr/bin/snappuller +248 -0
  100. data/solr/solr/bin/snappuller-disable +77 -0
  101. data/solr/solr/bin/snappuller-enable +77 -0
  102. data/solr/solr/bin/snapshooter +109 -0
  103. data/solr/solr/conf/admin-extra.html +31 -0
  104. data/solr/solr/conf/protwords.txt +21 -0
  105. data/solr/solr/conf/schema.xml +126 -0
  106. data/solr/solr/conf/scripts.conf +24 -0
  107. data/solr/solr/conf/solrconfig.xml +458 -0
  108. data/solr/solr/conf/stopwords.txt +57 -0
  109. data/solr/solr/conf/synonyms.txt +31 -0
  110. data/solr/solr/conf/xslt/example.xsl +132 -0
  111. data/solr/solr/conf/xslt/example_atom.xsl +63 -0
  112. data/solr/solr/conf/xslt/example_rss.xsl +62 -0
  113. data/solr/start.jar +0 -0
  114. data/solr/webapps/solr.war +0 -0
  115. data/test/config/solr.yml +2 -0
  116. data/test/db/connections/mysql/connection.rb +10 -0
  117. data/test/db/connections/sqlite/connection.rb +8 -0
  118. data/test/db/migrate/001_create_books.rb +15 -0
  119. data/test/db/migrate/002_create_movies.rb +12 -0
  120. data/test/db/migrate/003_create_categories.rb +11 -0
  121. data/test/db/migrate/004_create_electronics.rb +16 -0
  122. data/test/db/migrate/005_create_authors.rb +12 -0
  123. data/test/db/migrate/006_create_postings.rb +9 -0
  124. data/test/db/migrate/007_create_posts.rb +13 -0
  125. data/test/db/migrate/008_create_gadgets.rb +11 -0
  126. data/test/fixtures/authors.yml +9 -0
  127. data/test/fixtures/books.yml +13 -0
  128. data/test/fixtures/categories.yml +7 -0
  129. data/test/fixtures/db_definitions/mysql.sql +41 -0
  130. data/test/fixtures/electronics.yml +49 -0
  131. data/test/fixtures/movies.yml +9 -0
  132. data/test/fixtures/postings.yml +10 -0
  133. data/test/functional/acts_as_solr_test.rb +413 -0
  134. data/test/functional/association_indexing_test.rb +37 -0
  135. data/test/functional/faceted_search_test.rb +163 -0
  136. data/test/functional/multi_solr_search_test.rb +57 -0
  137. data/test/models/author.rb +10 -0
  138. data/test/models/book.rb +10 -0
  139. data/test/models/category.rb +8 -0
  140. data/test/models/electronic.rb +25 -0
  141. data/test/models/gadget.rb +9 -0
  142. data/test/models/movie.rb +17 -0
  143. data/test/models/novel.rb +2 -0
  144. data/test/models/post.rb +3 -0
  145. data/test/models/posting.rb +11 -0
  146. data/test/test_helper.rb +54 -0
  147. data/test/unit/acts_methods_shoulda.rb +68 -0
  148. data/test/unit/class_methods_shoulda.rb +85 -0
  149. data/test/unit/common_methods_shoulda.rb +111 -0
  150. data/test/unit/instance_methods_shoulda.rb +318 -0
  151. data/test/unit/lazy_document_shoulda.rb +34 -0
  152. data/test/unit/parser_instance.rb +19 -0
  153. data/test/unit/parser_methods_shoulda.rb +268 -0
  154. data/test/unit/solr_instance.rb +49 -0
  155. data/test/unit/test_helper.rb +24 -0
  156. metadata +241 -0
@@ -0,0 +1,352 @@
1
+ module ActsAsSolr #:nodoc:
2
+
3
+ module ActsMethods
4
+
5
+ # declares a class as solr-searchable
6
+ #
7
+ # ==== options:
8
+ # fields:: This option can be used to specify only the fields you'd
9
+ # like to index. If not given, all the attributes from the
10
+ # class will be indexed. You can also use this option to
11
+ # include methods that should be indexed as fields
12
+ #
13
+ # class Movie < ActiveRecord::Base
14
+ # acts_as_solr :fields => [:name, :description, :current_time]
15
+ # def current_time
16
+ # Time.now.to_s
17
+ # end
18
+ # end
19
+ #
20
+ # Each field passed can also be a hash with the value being a field type
21
+ #
22
+ # class Electronic < ActiveRecord::Base
23
+ # acts_as_solr :fields => [{:price => :range_float}]
24
+ # def current_time
25
+ # Time.now
26
+ # end
27
+ # end
28
+ #
29
+ # The field types accepted are:
30
+ #
31
+ # :float:: Index the field value as a float (ie.: 12.87)
32
+ # :integer:: Index the field value as an integer (ie.: 31)
33
+ # :boolean:: Index the field value as a boolean (ie.: true/false)
34
+ # :date:: Index the field value as a date (ie.: Wed Nov 15 23:13:03 PST 2006)
35
+ # :string:: Index the field value as a text string, not applying the same indexing
36
+ # filters as a regular text field
37
+ # :range_integer:: Index the field value for integer range queries (ie.:[5 TO 20])
38
+ # :range_float:: Index the field value for float range queries (ie.:[14.56 TO 19.99])
39
+ #
40
+ # Setting the field type preserves its original type when indexed
41
+ #
42
+ # The field may also be passed with a hash value containing options
43
+ #
44
+ # class Author < ActiveRecord::Base
45
+ # acts_as_solr :fields => [{:full_name => {:type => :text, :as => :name}}]
46
+ # def full_name
47
+ # self.first_name + ' ' + self.last_name
48
+ # end
49
+ # end
50
+ #
51
+ # The options accepted are:
52
+ #
53
+ # :type:: Index the field using the specified type
54
+ # :as:: Index the field using the specified field name
55
+ #
56
+ # additional_fields:: This option takes fields to be include in the index
57
+ # in addition to those derived from the database. You
58
+ # can also use this option to include custom fields
59
+ # derived from methods you define. This option will be
60
+ # ignored if the :fields option is given. It also accepts
61
+ # the same field types as the option above
62
+ #
63
+ # class Movie < ActiveRecord::Base
64
+ # acts_as_solr :additional_fields => [:current_time]
65
+ # def current_time
66
+ # Time.now.to_s
67
+ # end
68
+ # end
69
+ #
70
+ # exclude_fields:: This option taks an array of fields that should be ignored from indexing:
71
+ #
72
+ # class User < ActiveRecord::Base
73
+ # acts_as_solr :exclude_fields => [:password, :login, :credit_card_number]
74
+ # end
75
+ #
76
+ # include:: This option can be used for association indexing, which
77
+ # means you can include any :has_one, :has_many, :belongs_to
78
+ # and :has_and_belongs_to_many association to be indexed:
79
+ #
80
+ # class Category < ActiveRecord::Base
81
+ # has_many :books
82
+ # acts_as_solr :include => [:books]
83
+ # end
84
+ #
85
+ # Each association may also be specified as a hash with an option hash as a value
86
+ #
87
+ # class Book < ActiveRecord::Base
88
+ # belongs_to :author
89
+ # has_many :distribution_companies
90
+ # has_many :copyright_dates
91
+ # has_many :media_types
92
+ # acts_as_solr(
93
+ # :fields => [:name, :description],
94
+ # :include => [
95
+ # {:author => {:using => :fullname, :as => :name}},
96
+ # {:media_types => {:using => lambda{|media| type_lookup(media.id)}}}
97
+ # {:distribution_companies => {:as => :distributor, :multivalued => true}},
98
+ # {:copyright_dates => {:as => :copyright, :type => :date}}
99
+ # ]
100
+ # ]
101
+ #
102
+ # The options accepted are:
103
+ #
104
+ # :type:: Index the associated objects using the specified type
105
+ # :as:: Index the associated objects using the specified field name
106
+ # :using:: Index the associated objects using the value returned by the specified method or proc. If a method
107
+ # symbol is supplied, it will be sent to each object to look up the value to index; if a proc is
108
+ # supplied, it will be called once for each object with the object as the only argument
109
+ # :multivalued:: Index the associated objects using one field for each object rather than joining them
110
+ # all into a single field
111
+ #
112
+ # facets:: This option can be used to specify the fields you'd like to
113
+ # index as facet fields
114
+ #
115
+ # class Electronic < ActiveRecord::Base
116
+ # acts_as_solr :facets => [:category, :manufacturer]
117
+ # end
118
+ #
119
+ # boost:: You can pass a boost (float) value that will be used to boost the document and/or a field. To specify a more
120
+ # boost for the document, you can either pass a block or a symbol. The block will be called with the record
121
+ # as an argument, a symbol will result in the according method being called:
122
+ #
123
+ # class Electronic < ActiveRecord::Base
124
+ # acts_as_solr :fields => [{:price => {:boost => 5.0}}], :boost => 10.0
125
+ # end
126
+ #
127
+ # class Electronic < ActiveRecord::Base
128
+ # acts_as_solr :fields => [{:price => {:boost => 5.0}}], :boost => proc {|record| record.id + 120*37}
129
+ # end
130
+ #
131
+ # class Electronic < ActiveRecord::Base
132
+ # acts_as_solr :fields => [{:price => {:boost => :price_rating}}], :boost => 10.0
133
+ # end
134
+ #
135
+ # if:: Only indexes the record if the condition evaluated is true. The argument has to be
136
+ # either a symbol, string (to be eval'ed), proc/method, or class implementing a static
137
+ # validation method. It behaves the same way as ActiveRecord's :if option.
138
+ #
139
+ # class Electronic < ActiveRecord::Base
140
+ # acts_as_solr :if => proc{|record| record.is_active?}
141
+ # end
142
+ #
143
+ # offline:: Assumes that your using an outside mechanism to explicitly trigger indexing records, e.g. you only
144
+ # want to update your index through some asynchronous mechanism. Will accept either a boolean or a block
145
+ # that will be evaluated before actually contacting the index for saving or destroying a document. Defaults
146
+ # to false. It doesn't refer to the mechanism of an offline index in general, but just to get a centralized point
147
+ # where you can control indexing. Note: This is only enabled for saving records. acts_as_solr doesn't always like
148
+ # it, if you have a different number of results coming from the database and the index. This might be rectified in
149
+ # another patch to support lazy loading.
150
+ #
151
+ # class Electronic < ActiveRecord::Base
152
+ # acts_as_solr :offline => proc {|record| record.automatic_indexing_disabled?}
153
+ # end
154
+ #
155
+ # auto_commit:: The commit command will be sent to Solr only if its value is set to true:
156
+ #
157
+ # class Author < ActiveRecord::Base
158
+ # acts_as_solr :auto_commit => false
159
+ # end
160
+ #
161
+ def acts_as_solr(options={}, solr_options={}, &deferred_solr_configuration)
162
+
163
+ extend ClassMethods
164
+ include InstanceMethods
165
+ include CommonMethods
166
+ include ParserMethods
167
+
168
+ define_solr_configuration_methods
169
+
170
+ after_save :solr_save
171
+ after_destroy :solr_destroy
172
+
173
+ if deferred_solr_configuration
174
+ self.deferred_solr_configuration = deferred_solr_configuration
175
+ else
176
+ process_acts_as_solr(options, solr_options)
177
+ end
178
+ end
179
+
180
+ def process_acts_as_solr(options, solr_options)
181
+ process_solr_options(options, solr_options)
182
+ end
183
+
184
+ def define_solr_configuration_methods
185
+ # I'd like to use cattr_accessor, but it does not support lazy loaders and delegation to the class in the instance methods.
186
+ # TODO: Reconcile with cattr_accessor, or a more appropriate method.
187
+ class_eval(<<-EOS, __FILE__, __LINE__)
188
+ @@configuration = nil unless defined?(@@configuration)
189
+ @@solr_configuration = nil unless defined?(@@solr_configuration)
190
+ @@deferred_solr_configuration = nil unless defined?(@@deferred_solr_configuration)
191
+
192
+ def self.configuration
193
+ return @@configuration if @@configuration
194
+ process_deferred_solr_configuration
195
+ @@configuration
196
+ end
197
+ def configuration
198
+ self.class.configuration
199
+ end
200
+ def self.configuration=(value)
201
+ @@configuration = value
202
+ end
203
+ def configuration=(value)
204
+ self.class.configuration = value
205
+ end
206
+
207
+ def self.solr_configuration
208
+ return @@solr_configuration if @@solr_configuration
209
+ process_deferred_solr_configuration
210
+ @@solr_configuration
211
+ end
212
+ def solr_configuration
213
+ self.class.solr_configuration
214
+ end
215
+ def self.solr_configuration=(value)
216
+ @@solr_configuration = value
217
+ end
218
+ def solr_configuration=(value)
219
+ self.class.solr_configuration = value
220
+ end
221
+
222
+ def self.deferred_solr_configuration
223
+ return @@deferred_solr_configuration if @@deferred_solr_configuration
224
+ @@deferred_solr_configuration
225
+ end
226
+ def deferred_solr_configuration
227
+ self.class.deferred_solr_configuration
228
+ end
229
+ def self.deferred_solr_configuration=(value)
230
+ @@deferred_solr_configuration = value
231
+ end
232
+ def deferred_solr_configuration=(value)
233
+ self.class.deferred_solr_configuration = value
234
+ end
235
+ EOS
236
+ end
237
+
238
+ def process_deferred_solr_configuration
239
+ return unless deferred_solr_configuration
240
+ options, solr_options = deferred_solr_configuration.call
241
+ self.deferred_solr_configuration = nil
242
+ self.process_solr_options(options, solr_options)
243
+ end
244
+
245
+ def process_solr_options(options={}, solr_options={})
246
+ self.configuration = {
247
+ :fields => nil,
248
+ :additional_fields => nil,
249
+ :exclude_fields => [],
250
+ :auto_commit => true,
251
+ :include => nil,
252
+ :facets => nil,
253
+ :boost => nil,
254
+ :if => "true",
255
+ :offline => false
256
+ }
257
+ self.solr_configuration = {
258
+ :type_field => "type_s",
259
+ :primary_key_field => "pk_i",
260
+ :default_boost => 1.0
261
+ }
262
+
263
+ configuration.update(options) if options.is_a?(Hash)
264
+ solr_configuration.update(solr_options) if solr_options.is_a?(Hash)
265
+ Deprecation.validate_index(configuration)
266
+
267
+ configuration[:solr_fields] = {}
268
+ configuration[:solr_includes] = {}
269
+
270
+ if configuration[:fields].respond_to?(:each)
271
+ process_fields(configuration[:fields])
272
+ else
273
+ process_fields(self.new.attributes.keys.map { |k| k.to_sym })
274
+ process_fields(configuration[:additional_fields])
275
+ end
276
+
277
+ if configuration[:include].respond_to?(:each)
278
+ process_includes(configuration[:include])
279
+ end
280
+ end
281
+
282
+ private
283
+
284
+ def get_field_value(field)
285
+ field_name, options = determine_field_name_and_options(field)
286
+ configuration[:solr_fields][field_name] = options
287
+
288
+ define_method("#{field_name}_for_solr".to_sym) do
289
+ begin
290
+ value = self[field_name] || self.instance_variable_get("@#{field_name.to_s}".to_sym) || self.send(field_name.to_sym)
291
+ case options[:type]
292
+ # format dates properly; return nil for nil dates
293
+ when :date
294
+ value ? (value.respond_to?(:utc) ? value.utc : value).strftime("%Y-%m-%dT%H:%M:%SZ") : nil
295
+ else value
296
+ end
297
+ rescue
298
+ puts $!
299
+ logger.debug "There was a problem getting the value for the field '#{field_name}': #{$!}"
300
+ value = ''
301
+ end
302
+ end
303
+ end
304
+
305
+ def process_fields(raw_field)
306
+ if raw_field.respond_to?(:each)
307
+ raw_field.each do |field|
308
+ next if configuration[:exclude_fields].include?(field)
309
+ get_field_value(field)
310
+ end
311
+ end
312
+ end
313
+
314
+ def process_includes(includes)
315
+ if includes.respond_to?(:each)
316
+ includes.each do |assoc|
317
+ field_name, options = determine_field_name_and_options(assoc)
318
+ configuration[:solr_includes][field_name] = options
319
+ end
320
+ end
321
+ end
322
+
323
+ def determine_field_name_and_options(field)
324
+ if field.is_a?(Hash)
325
+ name = field.keys.first
326
+ options = field.values.first
327
+ if options.is_a?(Hash)
328
+ [name, {:type => type_for_field(field)}.merge(options)]
329
+ else
330
+ [name, {:type => options}]
331
+ end
332
+ else
333
+ [field, {:type => type_for_field(field)}]
334
+ end
335
+ end
336
+
337
+ def type_for_field(field)
338
+ if configuration[:facets] && configuration[:facets].include?(field)
339
+ :facet
340
+ elsif column = columns_hash[field.to_s]
341
+ case column.type
342
+ when :string then :text
343
+ when :datetime then :date
344
+ when :time then :date
345
+ else column.type
346
+ end
347
+ else
348
+ :text
349
+ end
350
+ end
351
+ end
352
+ end
@@ -0,0 +1,236 @@
1
+ module ActsAsSolr #:nodoc:
2
+
3
+ module ClassMethods
4
+ include CommonMethods
5
+ include ParserMethods
6
+
7
+ # Finds instances of a model. Terms are ANDed by default, can be overwritten
8
+ # by using OR between terms
9
+ #
10
+ # Here's a sample (untested) code for your controller:
11
+ #
12
+ # def search
13
+ # results = Book.find_by_solr params[:query]
14
+ # end
15
+ #
16
+ # You can also search for specific fields by searching for 'field:value'
17
+ #
18
+ # ====options:
19
+ # offset:: - The first document to be retrieved (offset)
20
+ # limit:: - The number of rows per page
21
+ # order:: - Orders (sort by) the result set using a given criteria:
22
+ #
23
+ # Book.find_by_solr 'ruby', :order => 'description asc'
24
+ #
25
+ # field_types:: This option is deprecated and will be obsolete by version 1.0.
26
+ # There's no need to specify the :field_types anymore when doing a
27
+ # search in a model that specifies a field type for a field. The field
28
+ # types are automatically traced back when they're included.
29
+ #
30
+ # class Electronic < ActiveRecord::Base
31
+ # acts_as_solr :fields => [{:price => :range_float}]
32
+ # end
33
+ #
34
+ # facets:: This option argument accepts the following arguments:
35
+ # fields:: The fields to be included in the faceted search (Solr's facet.field)
36
+ # query:: The queries to be included in the faceted search (Solr's facet.query)
37
+ # zeros:: Display facets with count of zero. (true|false)
38
+ # sort:: Sorts the faceted resuls by highest to lowest count. (true|false)
39
+ # browse:: This is where the 'drill-down' of the facets work. Accepts an array of
40
+ # fields in the format "facet_field:term"
41
+ # mincount:: Replacement for zeros (it has been deprecated in Solr). Specifies the
42
+ # minimum count necessary for a facet field to be returned. (Solr's
43
+ # facet.mincount) Overrides :zeros if it is specified. Default is 0.
44
+ #
45
+ # dates:: Run date faceted queries using the following arguments:
46
+ # fields:: The fields to be included in the faceted date search (Solr's facet.date).
47
+ # It may be either a String/Symbol or Hash. If it's a hash the options are the
48
+ # same as date_facets minus the fields option (i.e., :start:, :end, :gap, :other,
49
+ # :between). These options if provided will override the base options.
50
+ # (Solr's f.<field_name>.date.<key>=<value>).
51
+ # start:: The lower bound for the first date range for all Date Faceting. Required if
52
+ # :fields is present
53
+ # end:: The upper bound for the last date range for all Date Faceting. Required if
54
+ # :fields is prsent
55
+ # gap:: The size of each date range expressed as an interval to be added to the lower
56
+ # bound using the DateMathParser syntax. Required if :fields is prsent
57
+ # hardend:: A Boolean parameter instructing Solr what do do in the event that
58
+ # facet.date.gap does not divide evenly between facet.date.start and facet.date.end.
59
+ # other:: This param indicates that in addition to the counts for each date range
60
+ # constraint between facet.date.start and facet.date.end, other counds should be
61
+ # calculated. May specify more then one in an Array. The possible options are:
62
+ # before:: - all records with lower bound less than start
63
+ # after:: - all records with upper bound greater than end
64
+ # between:: - all records with field values between start and end
65
+ # none:: - compute no other bounds (useful in per field assignment)
66
+ # all:: - shortcut for before, after, and between
67
+ # filter:: Similar to :query option provided by :facets, in that accepts an array of
68
+ # of date queries to limit results. Can not be used as a part of a :field hash.
69
+ # This is the only option that can be used if :fields is not present.
70
+ #
71
+ # Example:
72
+ #
73
+ # Electronic.find_by_solr "memory", :facets => {:zeros => false, :sort => true,
74
+ # :query => ["price:[* TO 200]",
75
+ # "price:[200 TO 500]",
76
+ # "price:[500 TO *]"],
77
+ # :fields => [:category, :manufacturer],
78
+ # :browse => ["category:Memory","manufacturer:Someone"]}
79
+ #
80
+ #
81
+ # Examples of date faceting:
82
+ #
83
+ # basic:
84
+ # Electronic.find_by_solr "memory", :facets => {:dates => {:fields => [:updated_at, :created_at],
85
+ # :start => 'NOW-10YEARS/DAY', :end => 'NOW/DAY', :gap => '+2YEARS', :other => :before}}
86
+ #
87
+ # advanced:
88
+ # Electronic.find_by_solr "memory", :facets => {:dates => {:fields => [:updated_at,
89
+ # {:created_at => {:start => 'NOW-20YEARS/DAY', :end => 'NOW-10YEARS/DAY', :other => [:before, :after]}
90
+ # }], :start => 'NOW-10YEARS/DAY', :end => 'NOW/DAY', :other => :before, :filter =>
91
+ # ["created_at:[NOW-10YEARS/DAY TO NOW/DAY]", "updated_at:[NOW-1YEAR/DAY TO NOW/DAY]"]}}
92
+ #
93
+ # filter only:
94
+ # Electronic.find_by_solr "memory", :facets => {:dates => {:filter => "updated_at:[NOW-1YEAR/DAY TO NOW/DAY]"}}
95
+ #
96
+ #
97
+ #
98
+ # scores:: If set to true this will return the score as a 'solr_score' attribute
99
+ # for each one of the instances found. Does not currently work with find_id_by_solr
100
+ #
101
+ # books = Book.find_by_solr 'ruby OR splinter', :scores => true
102
+ # books.records.first.solr_score
103
+ # => 1.21321397
104
+ # books.records.last.solr_score
105
+ # => 0.12321548
106
+ #
107
+ # lazy:: If set to true the search will return objects that will touch the database when you ask for one
108
+ # of their attributes for the first time. Useful when you're using fragment caching based solely on
109
+ # types and ids.
110
+ #
111
+ def find_by_solr(query, options={})
112
+ data = parse_query(query, options)
113
+ return parse_results(data, options)
114
+ end
115
+
116
+ # Finds instances of a model and returns an array with the ids:
117
+ # Book.find_id_by_solr "rails" => [1,4,7]
118
+ # The options accepted are the same as find_by_solr
119
+ #
120
+ def find_id_by_solr(query, options={})
121
+ data = parse_query(query, options)
122
+ return parse_results(data, {:format => :ids})
123
+ end
124
+
125
+ # This method can be used to execute a search across multiple models:
126
+ # Book.multi_solr_search "Napoleon OR Tom", :models => [Movie]
127
+ #
128
+ # ====options:
129
+ # Accepts the same options as find_by_solr plus:
130
+ # models:: The additional models you'd like to include in the search
131
+ # results_format:: Specify the format of the results found
132
+ # :objects :: Will return an array with the results being objects (default). Example:
133
+ # Book.multi_solr_search "Napoleon OR Tom", :models => [Movie], :results_format => :objects
134
+ # :ids :: Will return an array with the ids of each entry found. Example:
135
+ # Book.multi_solr_search "Napoleon OR Tom", :models => [Movie], :results_format => :ids
136
+ # => [{"id" => "Movie:1"},{"id" => Book:1}]
137
+ # Where the value of each array is as Model:instance_id
138
+ # scores:: If set to true this will return the score as a 'solr_score' attribute
139
+ # for each one of the instances found. Does not currently work with find_id_by_solr
140
+ #
141
+ # books = Book.multi_solr_search 'ruby OR splinter', :scores => true
142
+ # books.records.first.solr_score
143
+ # => 1.21321397
144
+ # books.records.last.solr_score
145
+ # => 0.12321548
146
+ #
147
+ def multi_solr_search(query, options = {})
148
+ models = multi_model_suffix(options)
149
+ options.update(:results_format => :objects) unless options[:results_format]
150
+ data = parse_query(query, options, models)
151
+
152
+ if data.nil? or data.total_hits == 0
153
+ return SearchResults.new(:docs => [], :total => 0)
154
+ end
155
+
156
+ result = find_multi_search_objects(data, options)
157
+ if options[:scores] and options[:results_format] == :objects
158
+ add_scores(result, data)
159
+ end
160
+ SearchResults.new :docs => result, :total => data.total_hits
161
+ end
162
+
163
+ def find_multi_search_objects(data, options)
164
+ result = []
165
+ if options[:results_format] == :objects
166
+ data.hits.each do |doc|
167
+ k = doc.fetch('id').first.to_s.split(':')
168
+ result << k[0].constantize.find_by_id(k[1])
169
+ end
170
+ elsif options[:results_format] == :ids
171
+ data.hits.each{|doc| result << {"id" => doc["id"].to_s}}
172
+ end
173
+ result
174
+ end
175
+
176
+ def multi_model_suffix(options)
177
+ models = "AND (#{solr_configuration[:type_field]}:#{self.name}"
178
+ models << " OR " + options[:models].collect {|m| "#{solr_configuration[:type_field]}:" + m.to_s}.join(" OR ") if options[:models].is_a?(Array)
179
+ models << ")"
180
+ end
181
+
182
+ # returns the total number of documents found in the query specified:
183
+ # Book.count_by_solr 'rails' => 3
184
+ #
185
+ def count_by_solr(query, options = {})
186
+ data = parse_query(query, options)
187
+ data.total_hits
188
+ end
189
+
190
+ # It's used to rebuild the Solr index for a specific model.
191
+ # Book.rebuild_solr_index
192
+ #
193
+ # If batch_size is greater than 0, adds will be done in batches.
194
+ # NOTE: If using sqlserver, be sure to use a finder with an explicit order.
195
+ # Non-edge versions of rails do not handle pagination correctly for sqlserver
196
+ # without an order clause.
197
+ #
198
+ # If a finder block is given, it will be called to retrieve the items to index.
199
+ # This can be very useful for things such as updating based on conditions or
200
+ # using eager loading for indexed associations.
201
+ def rebuild_solr_index(batch_size=0, &finder)
202
+ finder ||= lambda { |ar, options| ar.find(:all, options.merge({:order => self.primary_key})) }
203
+ start_time = Time.now
204
+
205
+ if batch_size > 0
206
+ items_processed = 0
207
+ limit = batch_size
208
+ offset = 0
209
+ begin
210
+ iteration_start = Time.now
211
+ items = finder.call(self, {:limit => limit, :offset => offset})
212
+ add_batch = items.collect { |content| content.to_solr_doc }
213
+
214
+ if items.size > 0
215
+ solr_add add_batch
216
+ solr_commit
217
+ end
218
+
219
+ items_processed += items.size
220
+ last_id = items.last.id if items.last
221
+ time_so_far = Time.now - start_time
222
+ iteration_time = Time.now - iteration_start
223
+ logger.info "#{Process.pid}: #{items_processed} items for #{self.name} have been batch added to index in #{'%.3f' % time_so_far}s at #{'%.3f' % (items_processed / time_so_far)} items/sec (#{'%.3f' % (items.size / iteration_time)} items/sec for the last batch). Last id: #{last_id}"
224
+ offset += items.size
225
+ end while items.nil? || items.size > 0
226
+ else
227
+ items = finder.call(self, {})
228
+ items.each { |content| content.solr_save }
229
+ items_processed = items.size
230
+ end
231
+ solr_optimize
232
+ logger.info items_processed > 0 ? "Index for #{self.name} has been rebuilt" : "Nothing to index for #{self.name}"
233
+ end
234
+ end
235
+
236
+ end
@@ -0,0 +1,89 @@
1
+ module ActsAsSolr #:nodoc:
2
+
3
+ module CommonMethods
4
+
5
+ # Converts field types into Solr types
6
+ def get_solr_field_type(field_type)
7
+ if field_type.is_a?(Symbol)
8
+ case field_type
9
+ when :float
10
+ return "f"
11
+ when :integer
12
+ return "i"
13
+ when :boolean
14
+ return "b"
15
+ when :string
16
+ return "s"
17
+ when :date
18
+ return "d"
19
+ when :range_float
20
+ return "rf"
21
+ when :range_integer
22
+ return "ri"
23
+ when :facet
24
+ return "facet"
25
+ when :text
26
+ return "t"
27
+ else
28
+ raise "Unknown field_type symbol: #{field_type}"
29
+ end
30
+ elsif field_type.is_a?(String)
31
+ return field_type
32
+ else
33
+ raise "Unknown field_type class: #{field_type.class}: #{field_type}"
34
+ end
35
+ end
36
+
37
+ # Sets a default value when value being set is nil.
38
+ def set_value_if_nil(field_type)
39
+ case field_type
40
+ when "b", :boolean
41
+ return "false"
42
+ when "s", "t", "d", :date, :string, :text
43
+ return ""
44
+ when "f", "rf", :float, :range_float
45
+ return 0.00
46
+ when "i", "ri", :integer, :range_integer
47
+ return 0
48
+ else
49
+ return ""
50
+ end
51
+ end
52
+
53
+ # Sends an add command to Solr
54
+ def solr_add(add_xml)
55
+ ActsAsSolr::Post.execute(Solr::Request::AddDocument.new(add_xml))
56
+ end
57
+
58
+ # Sends the delete command to Solr
59
+ def solr_delete(solr_ids)
60
+ ActsAsSolr::Post.execute(Solr::Request::Delete.new(:id => solr_ids))
61
+ end
62
+
63
+ # Sends the commit command to Solr
64
+ def solr_commit
65
+ ActsAsSolr::Post.execute(Solr::Request::Commit.new)
66
+ end
67
+
68
+ # Optimizes the Solr index. Solr says:
69
+ #
70
+ # Optimizations can take nearly ten minutes to run.
71
+ # We are presuming optimizations should be run once following large
72
+ # batch-like updates to the collection and/or once a day.
73
+ #
74
+ # One of the solutions for this would be to create a cron job that
75
+ # runs every day at midnight and optmizes the index:
76
+ # 0 0 * * * /your_rails_dir/script/runner -e production "Model.solr_optimize"
77
+ #
78
+ def solr_optimize
79
+ ActsAsSolr::Post.execute(Solr::Request::Optimize.new)
80
+ end
81
+
82
+ # Returns the id for the given instance
83
+ def record_id(object)
84
+ eval "object.#{object.class.primary_key}"
85
+ end
86
+
87
+ end
88
+
89
+ end