acts_as_solr_reloaded 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (197) hide show
  1. data/LICENSE +22 -0
  2. data/README.markdown +64 -0
  3. data/README.rdoc +93 -0
  4. data/Rakefile +71 -0
  5. data/TESTING_THE_PLUGIN +25 -0
  6. data/VERSION +1 -0
  7. data/config/solr.yml +14 -0
  8. data/config/solr_environment.rb +35 -0
  9. data/generators/dynamic_attributes_migration/dynamic_attributes_migration_generator.rb +7 -0
  10. data/generators/dynamic_attributes_migration/templates/migration.rb +15 -0
  11. data/generators/local_migration/local_migration_generator.rb +7 -0
  12. data/generators/local_migration/templates/migration.rb +16 -0
  13. data/lib/acts_as_solr.rb +65 -0
  14. data/lib/acts_as_solr/acts_methods.rb +363 -0
  15. data/lib/acts_as_solr/class_methods.rb +240 -0
  16. data/lib/acts_as_solr/common_methods.rb +89 -0
  17. data/lib/acts_as_solr/deprecation.rb +61 -0
  18. data/lib/acts_as_solr/dynamic_attribute.rb +3 -0
  19. data/lib/acts_as_solr/instance_methods.rb +194 -0
  20. data/lib/acts_as_solr/lazy_document.rb +18 -0
  21. data/lib/acts_as_solr/local.rb +4 -0
  22. data/lib/acts_as_solr/parser_methods.rb +248 -0
  23. data/lib/acts_as_solr/search_results.rb +74 -0
  24. data/lib/acts_as_solr/solr_fixtures.rb +13 -0
  25. data/lib/acts_as_solr/tasks.rb +10 -0
  26. data/lib/acts_as_solr/tasks/database.rake +16 -0
  27. data/lib/acts_as_solr/tasks/solr.rake +142 -0
  28. data/lib/acts_as_solr/tasks/test.rake +5 -0
  29. data/lib/solr.rb +26 -0
  30. data/lib/solr/connection.rb +177 -0
  31. data/lib/solr/document.rb +75 -0
  32. data/lib/solr/exception.rb +13 -0
  33. data/lib/solr/field.rb +36 -0
  34. data/lib/solr/importer.rb +19 -0
  35. data/lib/solr/importer/array_mapper.rb +26 -0
  36. data/lib/solr/importer/delimited_file_source.rb +38 -0
  37. data/lib/solr/importer/hpricot_mapper.rb +27 -0
  38. data/lib/solr/importer/mapper.rb +51 -0
  39. data/lib/solr/importer/solr_source.rb +41 -0
  40. data/lib/solr/importer/xpath_mapper.rb +35 -0
  41. data/lib/solr/indexer.rb +52 -0
  42. data/lib/solr/request.rb +26 -0
  43. data/lib/solr/request/add_document.rb +58 -0
  44. data/lib/solr/request/base.rb +36 -0
  45. data/lib/solr/request/commit.rb +29 -0
  46. data/lib/solr/request/delete.rb +48 -0
  47. data/lib/solr/request/dismax.rb +46 -0
  48. data/lib/solr/request/index_info.rb +22 -0
  49. data/lib/solr/request/modify_document.rb +46 -0
  50. data/lib/solr/request/optimize.rb +19 -0
  51. data/lib/solr/request/ping.rb +36 -0
  52. data/lib/solr/request/select.rb +54 -0
  53. data/lib/solr/request/spellcheck.rb +30 -0
  54. data/lib/solr/request/standard.rb +406 -0
  55. data/lib/solr/request/update.rb +23 -0
  56. data/lib/solr/response.rb +27 -0
  57. data/lib/solr/response/add_document.rb +17 -0
  58. data/lib/solr/response/base.rb +42 -0
  59. data/lib/solr/response/commit.rb +15 -0
  60. data/lib/solr/response/delete.rb +13 -0
  61. data/lib/solr/response/dismax.rb +8 -0
  62. data/lib/solr/response/index_info.rb +26 -0
  63. data/lib/solr/response/modify_document.rb +17 -0
  64. data/lib/solr/response/optimize.rb +14 -0
  65. data/lib/solr/response/ping.rb +26 -0
  66. data/lib/solr/response/ruby.rb +42 -0
  67. data/lib/solr/response/select.rb +17 -0
  68. data/lib/solr/response/spellcheck.rb +20 -0
  69. data/lib/solr/response/standard.rb +65 -0
  70. data/lib/solr/response/xml.rb +39 -0
  71. data/lib/solr/solrtasks.rb +27 -0
  72. data/lib/solr/util.rb +32 -0
  73. data/lib/solr/xml.rb +44 -0
  74. data/solr/README.txt +36 -0
  75. data/solr/etc/jetty.xml +212 -0
  76. data/solr/etc/webdefault.xml +379 -0
  77. data/solr/exampledocs/books.csv +11 -0
  78. data/solr/exampledocs/hd.xml +46 -0
  79. data/solr/exampledocs/ipod_other.xml +50 -0
  80. data/solr/exampledocs/ipod_video.xml +35 -0
  81. data/solr/exampledocs/locales.xml +16 -0
  82. data/solr/exampledocs/mem.xml +58 -0
  83. data/solr/exampledocs/monitor.xml +31 -0
  84. data/solr/exampledocs/monitor2.xml +30 -0
  85. data/solr/exampledocs/mp500.xml +39 -0
  86. data/solr/exampledocs/post.jar +0 -0
  87. data/solr/exampledocs/post.sh +28 -0
  88. data/solr/exampledocs/sd500.xml +33 -0
  89. data/solr/exampledocs/solr.xml +38 -0
  90. data/solr/exampledocs/spellchecker.xml +58 -0
  91. data/solr/exampledocs/test_utf8.sh +83 -0
  92. data/solr/exampledocs/utf8-example.xml +42 -0
  93. data/solr/exampledocs/vidcard.xml +52 -0
  94. data/solr/lib/jetty-6.1.3.jar +0 -0
  95. data/solr/lib/jetty-util-6.1.3.jar +0 -0
  96. data/solr/lib/jsp-2.1/ant-1.6.5.jar +0 -0
  97. data/solr/lib/jsp-2.1/core-3.1.1.jar +0 -0
  98. data/solr/lib/jsp-2.1/jsp-2.1.jar +0 -0
  99. data/solr/lib/jsp-2.1/jsp-api-2.1.jar +0 -0
  100. data/solr/lib/servlet-api-2.5-6.1.3.jar +0 -0
  101. data/solr/multicore/README.txt +3 -0
  102. data/solr/multicore/core0/conf/schema.xml +41 -0
  103. data/solr/multicore/core0/conf/solrconfig.xml +40 -0
  104. data/solr/multicore/core1/conf/schema.xml +41 -0
  105. data/solr/multicore/core1/conf/solrconfig.xml +40 -0
  106. data/solr/multicore/exampledocs/ipod_other.xml +34 -0
  107. data/solr/multicore/exampledocs/ipod_video.xml +22 -0
  108. data/solr/multicore/solr.xml +35 -0
  109. data/solr/solr/README.txt +52 -0
  110. data/solr/solr/bin/abc +190 -0
  111. data/solr/solr/bin/abo +190 -0
  112. data/solr/solr/bin/backup +117 -0
  113. data/solr/solr/bin/backupcleaner +142 -0
  114. data/solr/solr/bin/commit +133 -0
  115. data/solr/solr/bin/optimize +134 -0
  116. data/solr/solr/bin/readercycle +129 -0
  117. data/solr/solr/bin/rsyncd-disable +77 -0
  118. data/solr/solr/bin/rsyncd-enable +76 -0
  119. data/solr/solr/bin/rsyncd-start +145 -0
  120. data/solr/solr/bin/rsyncd-stop +105 -0
  121. data/solr/solr/bin/scripts-util +99 -0
  122. data/solr/solr/bin/snapcleaner +154 -0
  123. data/solr/solr/bin/snapinstaller +198 -0
  124. data/solr/solr/bin/snappuller +269 -0
  125. data/solr/solr/bin/snappuller-disable +77 -0
  126. data/solr/solr/bin/snappuller-enable +77 -0
  127. data/solr/solr/bin/snapshooter +136 -0
  128. data/solr/solr/conf/admin-extra.html +31 -0
  129. data/solr/solr/conf/elevate.xml +36 -0
  130. data/solr/solr/conf/mapping-ISOLatin1Accent.txt +246 -0
  131. data/solr/solr/conf/protwords.txt +21 -0
  132. data/solr/solr/conf/schema.xml +132 -0
  133. data/solr/solr/conf/scripts.conf +24 -0
  134. data/solr/solr/conf/solrconfig.xml +906 -0
  135. data/solr/solr/conf/spellings.txt +2 -0
  136. data/solr/solr/conf/stopwords.txt +58 -0
  137. data/solr/solr/conf/synonyms.txt +31 -0
  138. data/solr/solr/conf/xslt/example.xsl +132 -0
  139. data/solr/solr/conf/xslt/example_atom.xsl +67 -0
  140. data/solr/solr/conf/xslt/example_rss.xsl +66 -0
  141. data/solr/solr/conf/xslt/luke.xsl +337 -0
  142. data/solr/solr/lib/localsolr.jar +0 -0
  143. data/solr/solr/lib/lucene-spatial-2.9-dev.jar +0 -0
  144. data/solr/start.jar +0 -0
  145. data/solr/webapps/solr.war +0 -0
  146. data/test/config/solr.yml +2 -0
  147. data/test/db/connections/mysql/connection.rb +11 -0
  148. data/test/db/connections/sqlite/connection.rb +8 -0
  149. data/test/db/migrate/001_create_books.rb +15 -0
  150. data/test/db/migrate/002_create_movies.rb +12 -0
  151. data/test/db/migrate/003_create_categories.rb +11 -0
  152. data/test/db/migrate/004_create_electronics.rb +16 -0
  153. data/test/db/migrate/005_create_authors.rb +12 -0
  154. data/test/db/migrate/006_create_postings.rb +9 -0
  155. data/test/db/migrate/007_create_posts.rb +13 -0
  156. data/test/db/migrate/008_create_gadgets.rb +11 -0
  157. data/test/db/migrate/009_create_dynamic_attributes.rb +15 -0
  158. data/test/db/migrate/010_create_advertises.rb +13 -0
  159. data/test/db/migrate/011_create_locals.rb +15 -0
  160. data/test/db/test.db +0 -0
  161. data/test/fixtures/advertises.yml +12 -0
  162. data/test/fixtures/authors.yml +9 -0
  163. data/test/fixtures/books.yml +13 -0
  164. data/test/fixtures/categories.yml +7 -0
  165. data/test/fixtures/db_definitions/mysql.sql +41 -0
  166. data/test/fixtures/dynamic_attributes.yml +11 -0
  167. data/test/fixtures/electronics.yml +49 -0
  168. data/test/fixtures/locals.yml +9 -0
  169. data/test/fixtures/movies.yml +9 -0
  170. data/test/fixtures/postings.yml +10 -0
  171. data/test/functional/acts_as_solr_test.rb +463 -0
  172. data/test/functional/association_indexing_test.rb +37 -0
  173. data/test/functional/faceted_search_test.rb +163 -0
  174. data/test/functional/multi_solr_search_test.rb +57 -0
  175. data/test/models/advertise.rb +6 -0
  176. data/test/models/author.rb +10 -0
  177. data/test/models/book.rb +10 -0
  178. data/test/models/category.rb +8 -0
  179. data/test/models/dynamic_attribute.rb +7 -0
  180. data/test/models/electronic.rb +25 -0
  181. data/test/models/gadget.rb +9 -0
  182. data/test/models/local.rb +7 -0
  183. data/test/models/movie.rb +17 -0
  184. data/test/models/novel.rb +2 -0
  185. data/test/models/post.rb +3 -0
  186. data/test/models/posting.rb +11 -0
  187. data/test/test_helper.rb +56 -0
  188. data/test/unit/acts_methods_shoulda.rb +95 -0
  189. data/test/unit/class_methods_shoulda.rb +85 -0
  190. data/test/unit/common_methods_shoulda.rb +111 -0
  191. data/test/unit/instance_methods_shoulda.rb +372 -0
  192. data/test/unit/lazy_document_shoulda.rb +34 -0
  193. data/test/unit/parser_instance.rb +19 -0
  194. data/test/unit/parser_methods_shoulda.rb +338 -0
  195. data/test/unit/solr_instance.rb +74 -0
  196. data/test/unit/test_helper.rb +24 -0
  197. metadata +290 -0
@@ -0,0 +1,363 @@
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
+ # dynamic_attributes: Default false. When true, requires a has_many relationship to a DynamicAttribute
162
+ # (:name, :value) model. Then, all dynamic attributes will be mapped as normal attributes
163
+ # in Solr, so you can filter like this: Model.find_by_solr "#{dynamic_attribute.name}:Lorem"
164
+ # taggable: Default false. When true, indexes tags with field name tag. Tags are taken from taggings.tag
165
+ # spatial: Default false. When true, indexes model.local.latitude and model.local.longitude as coordinates.
166
+ def acts_as_solr(options={}, solr_options={}, &deferred_solr_configuration)
167
+
168
+ extend ClassMethods
169
+ include InstanceMethods
170
+ include CommonMethods
171
+ include ParserMethods
172
+
173
+ define_solr_configuration_methods
174
+
175
+ acts_as_taggable_on :tags if options[:taggable]
176
+ has_many :dynamic_attributes, :as => "dynamicable" if options[:dynamic_attributes]
177
+ has_one :local, :as => "localizable" if options[:spatial]
178
+
179
+ after_save :solr_save
180
+ after_destroy :solr_destroy
181
+
182
+ if deferred_solr_configuration
183
+ self.deferred_solr_configuration = deferred_solr_configuration
184
+ else
185
+ process_acts_as_solr(options, solr_options)
186
+ end
187
+ end
188
+
189
+ def process_acts_as_solr(options, solr_options)
190
+ process_solr_options(options, solr_options)
191
+ end
192
+
193
+ def define_solr_configuration_methods
194
+ # I'd like to use cattr_accessor, but it does not support lazy loaders and delegation to the class in the instance methods.
195
+ # TODO: Reconcile with cattr_accessor, or a more appropriate method.
196
+ class_eval(<<-EOS, __FILE__, __LINE__)
197
+ @@configuration = nil unless defined?(@@configuration)
198
+ @@solr_configuration = nil unless defined?(@@solr_configuration)
199
+ @@deferred_solr_configuration = nil unless defined?(@@deferred_solr_configuration)
200
+
201
+ def self.configuration
202
+ return @@configuration if @@configuration
203
+ process_deferred_solr_configuration
204
+ @@configuration
205
+ end
206
+ def configuration
207
+ self.class.configuration
208
+ end
209
+ def self.configuration=(value)
210
+ @@configuration = value
211
+ end
212
+ def configuration=(value)
213
+ self.class.configuration = value
214
+ end
215
+
216
+ def self.solr_configuration
217
+ return @@solr_configuration if @@solr_configuration
218
+ process_deferred_solr_configuration
219
+ @@solr_configuration
220
+ end
221
+ def solr_configuration
222
+ self.class.solr_configuration
223
+ end
224
+ def self.solr_configuration=(value)
225
+ @@solr_configuration = value
226
+ end
227
+ def solr_configuration=(value)
228
+ self.class.solr_configuration = value
229
+ end
230
+
231
+ def self.deferred_solr_configuration
232
+ return @@deferred_solr_configuration if @@deferred_solr_configuration
233
+ @@deferred_solr_configuration
234
+ end
235
+ def deferred_solr_configuration
236
+ self.class.deferred_solr_configuration
237
+ end
238
+ def self.deferred_solr_configuration=(value)
239
+ @@deferred_solr_configuration = value
240
+ end
241
+ def deferred_solr_configuration=(value)
242
+ self.class.deferred_solr_configuration = value
243
+ end
244
+ EOS
245
+ end
246
+
247
+ def process_deferred_solr_configuration
248
+ return unless deferred_solr_configuration
249
+ options, solr_options = deferred_solr_configuration.call
250
+ self.deferred_solr_configuration = nil
251
+ self.process_solr_options(options, solr_options)
252
+ end
253
+
254
+ def process_solr_options(options={}, solr_options={})
255
+ self.configuration = {
256
+ :fields => nil,
257
+ :additional_fields => nil,
258
+ :dynamic_attributes => false,
259
+ :exclude_fields => [],
260
+ :auto_commit => true,
261
+ :include => nil,
262
+ :facets => nil,
263
+ :boost => nil,
264
+ :if => "true",
265
+ :offline => false,
266
+ :spatial => false
267
+ }
268
+ self.solr_configuration = {
269
+ :type_field => "type_s",
270
+ :primary_key_field => "pk_i",
271
+ :default_boost => 1.0
272
+ }
273
+
274
+ configuration.update(options) if options.is_a?(Hash)
275
+ solr_configuration.update(solr_options) if solr_options.is_a?(Hash)
276
+ Deprecation.validate_index(configuration)
277
+
278
+ configuration[:solr_fields] = {}
279
+ configuration[:solr_includes] = {}
280
+
281
+ if configuration[:fields].respond_to?(:each)
282
+ process_fields(configuration[:fields])
283
+ else
284
+ process_fields(self.new.attributes.keys.map { |k| k.to_sym })
285
+ process_fields(configuration[:additional_fields])
286
+ end
287
+
288
+ if configuration[:include].respond_to?(:each)
289
+ process_includes(configuration[:include])
290
+ end
291
+ end
292
+
293
+ private
294
+
295
+ def get_field_value(field)
296
+ field_name, options = determine_field_name_and_options(field)
297
+ configuration[:solr_fields][field_name] = options
298
+
299
+ define_method("#{field_name}_for_solr".to_sym) do
300
+ begin
301
+ value = self[field_name] || self.instance_variable_get("@#{field_name.to_s}".to_sym) || self.send(field_name.to_sym)
302
+ case options[:type]
303
+ # format dates properly; return nil for nil dates
304
+ when :date
305
+ value ? (value.respond_to?(:utc) ? value.utc : value).strftime("%Y-%m-%dT%H:%M:%SZ") : nil
306
+ else value
307
+ end
308
+ rescue
309
+ puts $!
310
+ logger.debug "There was a problem getting the value for the field '#{field_name}': #{$!}"
311
+ value = ''
312
+ end
313
+ end
314
+ end
315
+
316
+ def process_fields(raw_field)
317
+ if raw_field.respond_to?(:each)
318
+ raw_field.each do |field|
319
+ next if configuration[:exclude_fields].include?(field)
320
+ get_field_value(field)
321
+ end
322
+ end
323
+ end
324
+
325
+ def process_includes(includes)
326
+ if includes.respond_to?(:each)
327
+ includes.each do |assoc|
328
+ field_name, options = determine_field_name_and_options(assoc)
329
+ configuration[:solr_includes][field_name] = options
330
+ end
331
+ end
332
+ end
333
+
334
+ def determine_field_name_and_options(field)
335
+ if field.is_a?(Hash)
336
+ name = field.keys.first
337
+ options = field.values.first
338
+ if options.is_a?(Hash)
339
+ [name, {:type => type_for_field(field)}.merge(options)]
340
+ else
341
+ [name, {:type => options}]
342
+ end
343
+ else
344
+ [field, {:type => type_for_field(field)}]
345
+ end
346
+ end
347
+
348
+ def type_for_field(field)
349
+ if configuration[:facets] && configuration[:facets].include?(field)
350
+ :facet
351
+ elsif column = columns_hash[field.to_s]
352
+ case column.type
353
+ when :string then :text
354
+ when :datetime then :date
355
+ when :time then :date
356
+ else column.type
357
+ end
358
+ else
359
+ :text
360
+ end
361
+ end
362
+ end
363
+ end
@@ -0,0 +1,240 @@
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
+ # relevance:: Sets fields relevance
112
+ #
113
+ # Book.find_by_solr "zidane", :relevance => {:title => 5, :author => 2}
114
+ #
115
+ def find_by_solr(query, options={})
116
+ data = parse_query(query, options)
117
+ return parse_results(data, options)
118
+ end
119
+ alias :search :find_by_solr
120
+
121
+ # Finds instances of a model and returns an array with the ids:
122
+ # Book.find_id_by_solr "rails" => [1,4,7]
123
+ # The options accepted are the same as find_by_solr
124
+ #
125
+ def find_id_by_solr(query, options={})
126
+ data = parse_query(query, options)
127
+ return parse_results(data, {:format => :ids})
128
+ end
129
+
130
+ # This method can be used to execute a search across multiple models:
131
+ # Book.multi_solr_search "Napoleon OR Tom", :models => [Movie]
132
+ #
133
+ # ====options:
134
+ # Accepts the same options as find_by_solr plus:
135
+ # models:: The additional models you'd like to include in the search
136
+ # results_format:: Specify the format of the results found
137
+ # :objects :: Will return an array with the results being objects (default). Example:
138
+ # Book.multi_solr_search "Napoleon OR Tom", :models => [Movie], :results_format => :objects
139
+ # :ids :: Will return an array with the ids of each entry found. Example:
140
+ # Book.multi_solr_search "Napoleon OR Tom", :models => [Movie], :results_format => :ids
141
+ # => [{"id" => "Movie:1"},{"id" => Book:1}]
142
+ # Where the value of each array is as Model:instance_id
143
+ # scores:: If set to true this will return the score as a 'solr_score' attribute
144
+ # for each one of the instances found. Does not currently work with find_id_by_solr
145
+ #
146
+ # books = Book.multi_solr_search 'ruby OR splinter', :scores => true
147
+ # books.records.first.solr_score
148
+ # => 1.21321397
149
+ # books.records.last.solr_score
150
+ # => 0.12321548
151
+ #
152
+ def multi_solr_search(query, options = {})
153
+ models = multi_model_suffix(options)
154
+ options.update(:results_format => :objects) unless options[:results_format]
155
+ data = parse_query(query, options, models)
156
+
157
+ if data.nil? or data.total_hits == 0
158
+ return SearchResults.new(:docs => [], :total => 0)
159
+ end
160
+
161
+ result = find_multi_search_objects(data, options)
162
+ if options[:scores] and options[:results_format] == :objects
163
+ add_scores(result, data)
164
+ end
165
+ SearchResults.new :docs => result, :total => data.total_hits
166
+ end
167
+
168
+ def find_multi_search_objects(data, options)
169
+ result = []
170
+ if options[:results_format] == :objects
171
+ data.hits.each do |doc|
172
+ k = doc.fetch('id').first.to_s.split(':')
173
+ result << k[0].constantize.find_by_id(k[1])
174
+ end
175
+ elsif options[:results_format] == :ids
176
+ data.hits.each{|doc| result << {"id" => doc["id"].to_s}}
177
+ end
178
+ result
179
+ end
180
+
181
+ def multi_model_suffix(options)
182
+ models = "AND (#{solr_configuration[:type_field]}:#{self.name}"
183
+ models << " OR " + options[:models].collect {|m| "#{solr_configuration[:type_field]}:" + m.to_s}.join(" OR ") if options[:models].is_a?(Array)
184
+ models << ")"
185
+ end
186
+
187
+ # returns the total number of documents found in the query specified:
188
+ # Book.count_by_solr 'rails' => 3
189
+ #
190
+ def count_by_solr(query, options = {})
191
+ data = parse_query(query, options)
192
+ data.total_hits
193
+ end
194
+
195
+ # It's used to rebuild the Solr index for a specific model.
196
+ # Book.rebuild_solr_index
197
+ #
198
+ # If batch_size is greater than 0, adds will be done in batches.
199
+ # NOTE: If using sqlserver, be sure to use a finder with an explicit order.
200
+ # Non-edge versions of rails do not handle pagination correctly for sqlserver
201
+ # without an order clause.
202
+ #
203
+ # If a finder block is given, it will be called to retrieve the items to index.
204
+ # This can be very useful for things such as updating based on conditions or
205
+ # using eager loading for indexed associations.
206
+ def rebuild_solr_index(batch_size=0, &finder)
207
+ finder ||= lambda { |ar, options| ar.find(:all, options.merge({:order => self.primary_key})) }
208
+ start_time = Time.now
209
+
210
+ if batch_size > 0
211
+ items_processed = 0
212
+ limit = batch_size
213
+ offset = 0
214
+ begin
215
+ iteration_start = Time.now
216
+ items = finder.call(self, {:limit => limit, :offset => offset})
217
+ add_batch = items.collect { |content| content.to_solr_doc }
218
+
219
+ if items.size > 0
220
+ solr_add add_batch
221
+ solr_commit
222
+ end
223
+
224
+ items_processed += items.size
225
+ last_id = items.last.id if items.last
226
+ time_so_far = Time.now - start_time
227
+ iteration_time = Time.now - iteration_start
228
+ 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}"
229
+ offset += items.size
230
+ end while items.nil? || items.size > 0
231
+ else
232
+ items = finder.call(self, {})
233
+ items.each { |content| content.solr_save }
234
+ items_processed = items.size
235
+ end
236
+ solr_optimize
237
+ logger.info items_processed > 0 ? "Index for #{self.name} has been rebuilt" : "Nothing to index for #{self.name}"
238
+ end
239
+ end
240
+ end