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