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