acts_as_solr 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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