acts_as_solr 1.0.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 (163) hide show
  1. data/.gitignore +8 -0
  2. data/CHANGE_LOG +233 -0
  3. data/FORKED_CHANGES +3 -0
  4. data/LICENSE +19 -0
  5. data/README.markdown +94 -0
  6. data/README.rdoc +84 -0
  7. data/Rakefile +57 -0
  8. data/TESTING_THE_PLUGIN +25 -0
  9. data/VERSION +1 -0
  10. data/acts_as_solr.gemspec +237 -0
  11. data/config/solr.yml +15 -0
  12. data/config/solr_environment.rb +22 -0
  13. data/init.rb +21 -0
  14. data/install.rb +11 -0
  15. data/lib/acts_as_solr.rb +61 -0
  16. data/lib/acts_methods.rb +284 -0
  17. data/lib/class_methods.rb +239 -0
  18. data/lib/common_methods.rb +89 -0
  19. data/lib/deprecation.rb +61 -0
  20. data/lib/instance_methods.rb +181 -0
  21. data/lib/lazy_document.rb +18 -0
  22. data/lib/parser_methods.rb +230 -0
  23. data/lib/search_results.rb +69 -0
  24. data/lib/solr/connection.rb +191 -0
  25. data/lib/solr/document.rb +78 -0
  26. data/lib/solr/exception.rb +13 -0
  27. data/lib/solr/field.rb +39 -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 +43 -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 +63 -0
  37. data/lib/solr/request/base.rb +36 -0
  38. data/lib/solr/request/commit.rb +31 -0
  39. data/lib/solr/request/delete.rb +50 -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 +51 -0
  43. data/lib/solr/request/optimize.rb +21 -0
  44. data/lib/solr/request/ping.rb +36 -0
  45. data/lib/solr/request/select.rb +56 -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 +17 -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 +28 -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 +64 -0
  63. data/lib/solr/response/xml.rb +42 -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 +21 -0
  69. data/lib/solr_fixtures.rb +13 -0
  70. data/lib/tasks/database.rake +18 -0
  71. data/lib/tasks/solr.rake +137 -0
  72. data/lib/tasks/test.rake +7 -0
  73. data/lib/will_paginate_support.rb +12 -0
  74. data/solr/CHANGES.txt +1207 -0
  75. data/solr/LICENSE.txt +712 -0
  76. data/solr/NOTICE.txt +90 -0
  77. data/solr/etc/jetty.xml +205 -0
  78. data/solr/etc/webdefault.xml +379 -0
  79. data/solr/lib/easymock.jar +0 -0
  80. data/solr/lib/jetty-6.1.3.jar +0 -0
  81. data/solr/lib/jetty-util-6.1.3.jar +0 -0
  82. data/solr/lib/jsp-2.1/ant-1.6.5.jar +0 -0
  83. data/solr/lib/jsp-2.1/core-3.1.1.jar +0 -0
  84. data/solr/lib/jsp-2.1/jsp-2.1.jar +0 -0
  85. data/solr/lib/jsp-2.1/jsp-api-2.1.jar +0 -0
  86. data/solr/lib/servlet-api-2.4.jar +0 -0
  87. data/solr/lib/servlet-api-2.5-6.1.3.jar +0 -0
  88. data/solr/lib/xpp3-1.1.3.4.O.jar +0 -0
  89. data/solr/logs/.empty-dir-for-git +0 -0
  90. data/solr/solr/README.txt +52 -0
  91. data/solr/solr/bin/abc +176 -0
  92. data/solr/solr/bin/abo +176 -0
  93. data/solr/solr/bin/backup +108 -0
  94. data/solr/solr/bin/backupcleaner +142 -0
  95. data/solr/solr/bin/commit +128 -0
  96. data/solr/solr/bin/optimize +129 -0
  97. data/solr/solr/bin/readercycle +129 -0
  98. data/solr/solr/bin/rsyncd-disable +77 -0
  99. data/solr/solr/bin/rsyncd-enable +76 -0
  100. data/solr/solr/bin/rsyncd-start +145 -0
  101. data/solr/solr/bin/rsyncd-stop +105 -0
  102. data/solr/solr/bin/scripts-util +83 -0
  103. data/solr/solr/bin/snapcleaner +148 -0
  104. data/solr/solr/bin/snapinstaller +168 -0
  105. data/solr/solr/bin/snappuller +248 -0
  106. data/solr/solr/bin/snappuller-disable +77 -0
  107. data/solr/solr/bin/snappuller-enable +77 -0
  108. data/solr/solr/bin/snapshooter +109 -0
  109. data/solr/solr/conf/admin-extra.html +31 -0
  110. data/solr/solr/conf/protwords.txt +21 -0
  111. data/solr/solr/conf/schema.xml +126 -0
  112. data/solr/solr/conf/scripts.conf +24 -0
  113. data/solr/solr/conf/solrconfig.xml +458 -0
  114. data/solr/solr/conf/stopwords.txt +57 -0
  115. data/solr/solr/conf/synonyms.txt +31 -0
  116. data/solr/solr/conf/xslt/example.xsl +132 -0
  117. data/solr/solr/conf/xslt/example_atom.xsl +63 -0
  118. data/solr/solr/conf/xslt/example_rss.xsl +62 -0
  119. data/solr/start.jar +0 -0
  120. data/solr/tmp/.empty-dir-for-git +0 -0
  121. data/solr/webapps/solr.war +0 -0
  122. data/test/config/solr.yml +2 -0
  123. data/test/db/connections/mysql/connection.rb +10 -0
  124. data/test/db/connections/sqlite/connection.rb +8 -0
  125. data/test/db/migrate/001_create_books.rb +15 -0
  126. data/test/db/migrate/002_create_movies.rb +12 -0
  127. data/test/db/migrate/003_create_categories.rb +11 -0
  128. data/test/db/migrate/004_create_electronics.rb +16 -0
  129. data/test/db/migrate/005_create_authors.rb +12 -0
  130. data/test/db/migrate/006_create_postings.rb +9 -0
  131. data/test/db/migrate/007_create_posts.rb +13 -0
  132. data/test/db/migrate/008_create_gadgets.rb +11 -0
  133. data/test/fixtures/authors.yml +9 -0
  134. data/test/fixtures/books.yml +13 -0
  135. data/test/fixtures/categories.yml +7 -0
  136. data/test/fixtures/db_definitions/mysql.sql +41 -0
  137. data/test/fixtures/electronics.yml +49 -0
  138. data/test/fixtures/movies.yml +9 -0
  139. data/test/fixtures/postings.yml +10 -0
  140. data/test/functional/acts_as_solr_test.rb +413 -0
  141. data/test/functional/association_indexing_test.rb +37 -0
  142. data/test/functional/faceted_search_test.rb +163 -0
  143. data/test/functional/multi_solr_search_test.rb +51 -0
  144. data/test/models/author.rb +10 -0
  145. data/test/models/book.rb +10 -0
  146. data/test/models/category.rb +8 -0
  147. data/test/models/electronic.rb +21 -0
  148. data/test/models/gadget.rb +9 -0
  149. data/test/models/movie.rb +17 -0
  150. data/test/models/novel.rb +2 -0
  151. data/test/models/post.rb +3 -0
  152. data/test/models/posting.rb +11 -0
  153. data/test/test_helper.rb +51 -0
  154. data/test/unit/acts_methods_shoulda.rb +70 -0
  155. data/test/unit/class_methods_shoulda.rb +90 -0
  156. data/test/unit/common_methods_shoulda.rb +112 -0
  157. data/test/unit/instance_methods_shoulda.rb +326 -0
  158. data/test/unit/lazy_document_shoulda.rb +35 -0
  159. data/test/unit/parser_instance.rb +19 -0
  160. data/test/unit/parser_methods_shoulda.rb +279 -0
  161. data/test/unit/solr_instance.rb +46 -0
  162. data/test/unit/test_helper.rb +26 -0
  163. metadata +259 -0
@@ -0,0 +1,239 @@
1
+ require File.dirname(__FILE__) + '/common_methods'
2
+ require File.dirname(__FILE__) + '/parser_methods'
3
+
4
+ module ActsAsSolr #:nodoc:
5
+
6
+ module ClassMethods
7
+ include CommonMethods
8
+ include ParserMethods
9
+
10
+ # Finds instances of a model. Terms are ANDed by default, can be overwritten
11
+ # by using OR between terms
12
+ #
13
+ # Here's a sample (untested) code for your controller:
14
+ #
15
+ # def search
16
+ # results = Book.find_by_solr params[:query]
17
+ # end
18
+ #
19
+ # You can also search for specific fields by searching for 'field:value'
20
+ #
21
+ # ====options:
22
+ # offset:: - The first document to be retrieved (offset)
23
+ # limit:: - The number of rows per page
24
+ # order:: - Orders (sort by) the result set using a given criteria:
25
+ #
26
+ # Book.find_by_solr 'ruby', :order => 'description asc'
27
+ #
28
+ # field_types:: This option is deprecated and will be obsolete by version 1.0.
29
+ # There's no need to specify the :field_types anymore when doing a
30
+ # search in a model that specifies a field type for a field. The field
31
+ # types are automatically traced back when they're included.
32
+ #
33
+ # class Electronic < ActiveRecord::Base
34
+ # acts_as_solr :fields => [{:price => :range_float}]
35
+ # end
36
+ #
37
+ # facets:: This option argument accepts the following arguments:
38
+ # fields:: The fields to be included in the faceted search (Solr's facet.field)
39
+ # query:: The queries to be included in the faceted search (Solr's facet.query)
40
+ # zeros:: Display facets with count of zero. (true|false)
41
+ # sort:: Sorts the faceted resuls by highest to lowest count. (true|false)
42
+ # browse:: This is where the 'drill-down' of the facets work. Accepts an array of
43
+ # fields in the format "facet_field:term"
44
+ # mincount:: Replacement for zeros (it has been deprecated in Solr). Specifies the
45
+ # minimum count necessary for a facet field to be returned. (Solr's
46
+ # facet.mincount) Overrides :zeros if it is specified. Default is 0.
47
+ #
48
+ # dates:: Run date faceted queries using the following arguments:
49
+ # fields:: The fields to be included in the faceted date search (Solr's facet.date).
50
+ # It may be either a String/Symbol or Hash. If it's a hash the options are the
51
+ # same as date_facets minus the fields option (i.e., :start:, :end, :gap, :other,
52
+ # :between). These options if provided will override the base options.
53
+ # (Solr's f.<field_name>.date.<key>=<value>).
54
+ # start:: The lower bound for the first date range for all Date Faceting. Required if
55
+ # :fields is present
56
+ # end:: The upper bound for the last date range for all Date Faceting. Required if
57
+ # :fields is prsent
58
+ # gap:: The size of each date range expressed as an interval to be added to the lower
59
+ # bound using the DateMathParser syntax. Required if :fields is prsent
60
+ # hardend:: A Boolean parameter instructing Solr what do do in the event that
61
+ # facet.date.gap does not divide evenly between facet.date.start and facet.date.end.
62
+ # other:: This param indicates that in addition to the counts for each date range
63
+ # constraint between facet.date.start and facet.date.end, other counds should be
64
+ # calculated. May specify more then one in an Array. The possible options are:
65
+ # before:: - all records with lower bound less than start
66
+ # after:: - all records with upper bound greater than end
67
+ # between:: - all records with field values between start and end
68
+ # none:: - compute no other bounds (useful in per field assignment)
69
+ # all:: - shortcut for before, after, and between
70
+ # filter:: Similar to :query option provided by :facets, in that accepts an array of
71
+ # of date queries to limit results. Can not be used as a part of a :field hash.
72
+ # This is the only option that can be used if :fields is not present.
73
+ #
74
+ # Example:
75
+ #
76
+ # Electronic.find_by_solr "memory", :facets => {:zeros => false, :sort => true,
77
+ # :query => ["price:[* TO 200]",
78
+ # "price:[200 TO 500]",
79
+ # "price:[500 TO *]"],
80
+ # :fields => [:category, :manufacturer],
81
+ # :browse => ["category:Memory","manufacturer:Someone"]}
82
+ #
83
+ #
84
+ # Examples of date faceting:
85
+ #
86
+ # basic:
87
+ # Electronic.find_by_solr "memory", :facets => {:dates => {:fields => [:updated_at, :created_at],
88
+ # :start => 'NOW-10YEARS/DAY', :end => 'NOW/DAY', :gap => '+2YEARS', :other => :before}}
89
+ #
90
+ # advanced:
91
+ # Electronic.find_by_solr "memory", :facets => {:dates => {:fields => [:updated_at,
92
+ # {:created_at => {:start => 'NOW-20YEARS/DAY', :end => 'NOW-10YEARS/DAY', :other => [:before, :after]}
93
+ # }], :start => 'NOW-10YEARS/DAY', :end => 'NOW/DAY', :other => :before, :filter =>
94
+ # ["created_at:[NOW-10YEARS/DAY TO NOW/DAY]", "updated_at:[NOW-1YEAR/DAY TO NOW/DAY]"]}}
95
+ #
96
+ # filter only:
97
+ # Electronic.find_by_solr "memory", :facets => {:dates => {:filter => "updated_at:[NOW-1YEAR/DAY TO NOW/DAY]"}}
98
+ #
99
+ #
100
+ #
101
+ # scores:: If set to true this will return the score as a 'solr_score' attribute
102
+ # for each one of the instances found. Does not currently work with find_id_by_solr
103
+ #
104
+ # books = Book.find_by_solr 'ruby OR splinter', :scores => true
105
+ # books.records.first.solr_score
106
+ # => 1.21321397
107
+ # books.records.last.solr_score
108
+ # => 0.12321548
109
+ #
110
+ # lazy:: If set to true the search will return objects that will touch the database when you ask for one
111
+ # of their attributes for the first time. Useful when you're using fragment caching based solely on
112
+ # types and ids.
113
+ #
114
+ def find_by_solr(query, options={})
115
+ data = parse_query(query, options)
116
+ return parse_results(data, options) if data
117
+ end
118
+
119
+ # Finds instances of a model and returns an array with the ids:
120
+ # Book.find_id_by_solr "rails" => [1,4,7]
121
+ # The options accepted are the same as find_by_solr
122
+ #
123
+ def find_id_by_solr(query, options={})
124
+ data = parse_query(query, options)
125
+ return parse_results(data, {:format => :ids}) if data
126
+ end
127
+
128
+ # This method can be used to execute a search across multiple models:
129
+ # Book.multi_solr_search "Napoleon OR Tom", :models => [Movie]
130
+ #
131
+ # ====options:
132
+ # Accepts the same options as find_by_solr plus:
133
+ # models:: The additional models you'd like to include in the search
134
+ # results_format:: Specify the format of the results found
135
+ # :objects :: Will return an array with the results being objects (default). Example:
136
+ # Book.multi_solr_search "Napoleon OR Tom", :models => [Movie], :results_format => :objects
137
+ # :ids :: Will return an array with the ids of each entry found. Example:
138
+ # Book.multi_solr_search "Napoleon OR Tom", :models => [Movie], :results_format => :ids
139
+ # => [{"id" => "Movie:1"},{"id" => Book:1}]
140
+ # Where the value of each array is as Model:instance_id
141
+ # scores:: If set to true this will return the score as a 'solr_score' attribute
142
+ # for each one of the instances found. Does not currently work with find_id_by_solr
143
+ #
144
+ # books = Book.multi_solr_search 'ruby OR splinter', :scores => true
145
+ # books.records.first.solr_score
146
+ # => 1.21321397
147
+ # books.records.last.solr_score
148
+ # => 0.12321548
149
+ #
150
+ def multi_solr_search(query, options = {})
151
+ models = multi_model_suffix(options)
152
+ options.update(:results_format => :objects) unless options[:results_format]
153
+ data = parse_query(query, options, models)
154
+
155
+ if data.nil? or data.total_hits == 0
156
+ return SearchResults.new(:docs => [], :total => 0)
157
+ end
158
+
159
+ result = find_multi_search_objects(data, options)
160
+ if options[:scores] and options[:results_format] == :objects
161
+ add_scores(result, data)
162
+ end
163
+ SearchResults.new :docs => result, :total => data.total_hits
164
+ end
165
+
166
+ def find_multi_search_objects(data, options)
167
+ result = []
168
+ if options[:results_format] == :objects
169
+ data.hits.each do |doc|
170
+ k = doc.fetch('id').first.to_s.split(':')
171
+ result << k[0].constantize.find_by_id(k[1])
172
+ end
173
+ elsif options[:results_format] == :ids
174
+ data.hits.each{|doc| result << {"id" => doc["id"].to_s}}
175
+ end
176
+ result
177
+ end
178
+
179
+ def multi_model_suffix(options)
180
+ models = "AND (#{solr_configuration[:type_field]}:#{self.name}"
181
+ models << " OR " + options[:models].collect {|m| "#{solr_configuration[:type_field]}:" + m.to_s}.join(" OR ") if options[:models].is_a?(Array)
182
+ models << ")"
183
+ end
184
+
185
+ # returns the total number of documents found in the query specified:
186
+ # Book.count_by_solr 'rails' => 3
187
+ #
188
+ def count_by_solr(query, options = {})
189
+ data = parse_query(query, options)
190
+ data.total_hits
191
+ end
192
+
193
+ # It's used to rebuild the Solr index for a specific model.
194
+ # Book.rebuild_solr_index
195
+ #
196
+ # If batch_size is greater than 0, adds will be done in batches.
197
+ # NOTE: If using sqlserver, be sure to use a finder with an explicit order.
198
+ # Non-edge versions of rails do not handle pagination correctly for sqlserver
199
+ # without an order clause.
200
+ #
201
+ # If a finder block is given, it will be called to retrieve the items to index.
202
+ # This can be very useful for things such as updating based on conditions or
203
+ # using eager loading for indexed associations.
204
+ def rebuild_solr_index(batch_size=0, &finder)
205
+ finder ||= lambda { |ar, options| ar.find(:all, options.merge({:order => self.primary_key})) }
206
+ start_time = Time.now
207
+
208
+ if batch_size > 0
209
+ items_processed = 0
210
+ limit = batch_size
211
+ offset = 0
212
+ begin
213
+ iteration_start = Time.now
214
+ items = finder.call(self, {:limit => limit, :offset => offset})
215
+ add_batch = items.collect { |content| content.to_solr_doc }
216
+
217
+ if items.size > 0
218
+ solr_add add_batch
219
+ solr_commit
220
+ end
221
+
222
+ items_processed += items.size
223
+ last_id = items.last.id if items.last
224
+ time_so_far = Time.now - start_time
225
+ iteration_time = Time.now - iteration_start
226
+ 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}"
227
+ offset += items.size
228
+ end while items.nil? || items.size > 0
229
+ else
230
+ items = finder.call(self, {})
231
+ items.each { |content| content.solr_save }
232
+ items_processed = items.size
233
+ end
234
+ solr_optimize
235
+ logger.info items_processed > 0 ? "Index for #{self.name} has been rebuilt" : "Nothing to index for #{self.name}"
236
+ end
237
+ end
238
+
239
+ 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
@@ -0,0 +1,61 @@
1
+ module ActsAsSolr #:nodoc:
2
+
3
+ class Post
4
+ def initialize(body, mode = :search)
5
+ @body = body
6
+ @mode = mode
7
+ puts "The method ActsAsSolr::Post.new(body, mode).execute_post is depracated. " +
8
+ "Use ActsAsSolr::Post.execute(body, mode) instead!"
9
+ end
10
+
11
+ def execute_post
12
+ ActsAsSolr::Post.execute(@body, @mode)
13
+ end
14
+ end
15
+
16
+ module ClassMethods
17
+ def find_with_facet(query, options={})
18
+ Deprecation.plog "The method find_with_facet is deprecated. Use find_by_solr instead, passing the " +
19
+ "arguments the same way you used to do with find_with_facet."
20
+ find_by_solr(query, options)
21
+ end
22
+ end
23
+
24
+ class Deprecation
25
+ # Validates the options passed during query
26
+ def self.validate_query options={}
27
+ if options[:field_types]
28
+ plog "The option :field_types for searching is deprecated. " +
29
+ "The field types are automatically traced back when you specify a field type in your model."
30
+ end
31
+ if options[:sort_by]
32
+ plog "The option :sort_by is deprecated, use :order instead!"
33
+ options[:order] ||= options[:sort_by]
34
+ end
35
+ if options[:start]
36
+ plog "The option :start is deprecated, use :offset instead!"
37
+ options[:offset] ||= options[:start]
38
+ end
39
+ if options[:rows]
40
+ plog "The option :rows is deprecated, use :limit instead!"
41
+ options[:limit] ||= options[:rows]
42
+ end
43
+ end
44
+
45
+ # Validates the options passed during indexing
46
+ def self.validate_index options={}
47
+ if options[:background]
48
+ plog "The :background option is being deprecated. There are better and more efficient " +
49
+ "ways to handle delayed saving of your records."
50
+ end
51
+ end
52
+
53
+ # This will print the text to stdout and log the text
54
+ # if rails logger is available
55
+ def self.plog text
56
+ puts text
57
+ RAILS_DEFAULT_LOGGER.warn text if defined? RAILS_DEFAULT_LOGGER
58
+ end
59
+ end
60
+
61
+ end
@@ -0,0 +1,181 @@
1
+ module ActsAsSolr #:nodoc:
2
+
3
+ module InstanceMethods
4
+
5
+ # Solr id is <class.name>:<id> to be unique across all models
6
+ def solr_id
7
+ "#{self.class.name}:#{record_id(self)}"
8
+ end
9
+
10
+ def init_solr(data)
11
+ @solr_data = data
12
+ end
13
+
14
+ def method_missing_with_solr_magic(method, *a, &b)
15
+ if method.to_s =~ /^highlighted_(.*)$/ && a.length == 0
16
+ original_field = $1
17
+ @solr_data && @solr_data[:highlights] && @solr_data[:highlights][id] &&
18
+ @solr_data[:highlights][id][original_field] &&
19
+ @solr_data[:highlights][id][original_field].join(" ") || send(original_field)
20
+ else
21
+ method_missing_without_solr_magic(method, *a, &b)
22
+ end
23
+ end
24
+
25
+ # saves to the Solr index
26
+ def solr_save
27
+ return true if indexing_disabled?
28
+ if evaluate_condition(:if, self)
29
+ logger.debug "solr_save: #{self.class.name} : #{record_id(self)}"
30
+ solr_add to_solr_doc
31
+ solr_commit if configuration[:auto_commit]
32
+ true
33
+ else
34
+ solr_destroy
35
+ end
36
+ end
37
+
38
+ def indexing_disabled?
39
+ evaluate_condition(:offline, self) || !configuration[:if]
40
+ end
41
+
42
+ # remove from index
43
+ def solr_destroy
44
+ return true if indexing_disabled?
45
+ logger.debug "solr_destroy: #{self.class.name} : #{record_id(self)}"
46
+ solr_delete solr_id
47
+ solr_commit if configuration[:auto_commit]
48
+ true
49
+ end
50
+
51
+ # convert instance to Solr document
52
+ def to_solr_doc
53
+ logger.debug "to_solr_doc: creating doc for class: #{self.class.name}, id: #{record_id(self)}"
54
+ doc = Solr::Document.new
55
+ doc.boost = validate_boost(configuration[:boost]) if configuration[:boost]
56
+
57
+ doc << {:id => solr_id,
58
+ solr_configuration[:type_field] => self.class.name,
59
+ solr_configuration[:primary_key_field] => record_id(self).to_s}
60
+
61
+ # iterate through the fields and add them to the document,
62
+ configuration[:solr_fields].each do |field_name, options|
63
+ #field_type = configuration[:facets] && configuration[:facets].include?(field) ? :facet : :text
64
+
65
+ field_boost = options[:boost] || solr_configuration[:default_boost]
66
+ field_type = get_solr_field_type(options[:type])
67
+ solr_name = options[:as] || field_name
68
+
69
+ value = self.send("#{field_name}_for_solr")
70
+ value = set_value_if_nil(field_type) if value.to_s == ""
71
+
72
+ # add the field to the document, but only if it's not the id field
73
+ # or the type field (from single table inheritance), since these
74
+ # fields have already been added above.
75
+ if field_name.to_s != self.class.primary_key and field_name.to_s != "type"
76
+ suffix = get_solr_field_type(field_type)
77
+ # This next line ensures that e.g. nil dates are excluded from the
78
+ # document, since they choke Solr. Also ignores e.g. empty strings,
79
+ # but these can't be searched for anyway:
80
+ # http://www.mail-archive.com/solr-dev@lucene.apache.org/msg05423.html
81
+ next if value.nil? || value.to_s.strip.empty?
82
+ [value].flatten.each do |v|
83
+ v = set_value_if_nil(suffix) if value.to_s == ""
84
+ field = Solr::Field.new("#{solr_name}_#{suffix}" => ERB::Util.html_escape(v.to_s))
85
+ field.boost = validate_boost(field_boost)
86
+ doc << field
87
+ end
88
+ end
89
+ end
90
+
91
+ add_includes(doc)
92
+ logger.debug doc.to_xml
93
+ doc
94
+ end
95
+
96
+ private
97
+ def add_includes(doc)
98
+ if configuration[:solr_includes].respond_to?(:each)
99
+ configuration[:solr_includes].each do |association, options|
100
+ data = options[:multivalued] ? [] : ""
101
+ field_name = options[:as] || association.to_s.singularize
102
+ field_type = get_solr_field_type(options[:type])
103
+ field_boost = options[:boost] || solr_configuration[:default_boost]
104
+ suffix = get_solr_field_type(field_type)
105
+ case self.class.reflect_on_association(association).macro
106
+ when :has_many, :has_and_belongs_to_many
107
+ records = self.send(association).to_a
108
+ unless records.empty?
109
+ records.each {|r| data << include_value(r, options)}
110
+ [data].flatten.each do |value|
111
+ field = Solr::Field.new("#{field_name}_#{suffix}" => value)
112
+ field.boost = validate_boost(field_boost)
113
+ doc << field
114
+ end
115
+ end
116
+ when :has_one, :belongs_to
117
+ record = self.send(association)
118
+ unless record.nil?
119
+ doc["#{field_name}_#{suffix}"] = include_value(record, options)
120
+ end
121
+ end
122
+ end
123
+ end
124
+ end
125
+
126
+ def include_value(record, options)
127
+ if options[:using].is_a? Proc
128
+ options[:using].call(record)
129
+ elsif options[:using].is_a? Symbol
130
+ record.send(options[:using])
131
+ else
132
+ record.attributes.inject([]){|k,v| k << "#{v.first}=#{ERB::Util.html_escape(v.last)}"}.join(" ")
133
+ end
134
+ end
135
+
136
+ def validate_boost(boost)
137
+ boost_value = case boost
138
+ when Float
139
+ return solr_configuration[:default_boost] if boost < 0
140
+ boost
141
+ when Proc
142
+ boost.call(self)
143
+ when Symbol
144
+ if self.respond_to?(boost)
145
+ self.send(boost)
146
+ end
147
+ end
148
+
149
+ boost_value || solr_configuration[:default_boost]
150
+ end
151
+
152
+ def condition_block?(condition)
153
+ condition.respond_to?("call") && (condition.arity == 1 || condition.arity == -1)
154
+ end
155
+
156
+ def evaluate_condition(which_condition, field)
157
+ condition = configuration[which_condition]
158
+ case condition
159
+ when Symbol
160
+ field.send(condition)
161
+ when String
162
+ eval(condition, binding)
163
+ when FalseClass, NilClass
164
+ false
165
+ when TrueClass
166
+ true
167
+ else
168
+ if condition_block?(condition)
169
+ condition.call(field)
170
+ else
171
+ raise(
172
+ ArgumentError,
173
+ "The :#{which_condition} option has to be either a symbol, string (to be eval'ed), proc/method, true/false, or " +
174
+ "class implementing a static validation method"
175
+ )
176
+ end
177
+ end
178
+ end
179
+
180
+ end
181
+ end
@@ -0,0 +1,18 @@
1
+ module ActsAsSolr
2
+ class LazyDocument
3
+ attr_reader :id, :clazz
4
+
5
+ def initialize(id, clazz)
6
+ @id = id
7
+ @clazz = clazz
8
+ end
9
+
10
+ def method_missing(name, *args)
11
+ unless @__instance
12
+ @__instance = @clazz.find(@id)
13
+ end
14
+
15
+ @__instance.send(name, *args)
16
+ end
17
+ end
18
+ end