rmla 1.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.
@@ -0,0 +1,11 @@
1
+ # A wrapper for slingshot elastic-search adapter for Mongoid
2
+ module Mebla
3
+ # Represents the parent module for all errors in Mebla
4
+ module Errors
5
+ autoload :MeblaError, 'mebla/errors/mebla_error'
6
+ autoload :MeblaFatal, 'mebla/errors/mebla_fatal'
7
+ autoload :MeblaConfigurationException, 'mebla/errors/mebla_configuration_exception'
8
+ autoload :MeblaIndexException, 'mebla/errors/mebla_index_exception'
9
+ autoload :MeblaSynchronizationException, 'mebla/errors/mebla_synchronization_exception'
10
+ end
11
+ end
@@ -0,0 +1,10 @@
1
+ # A wrapper for slingshot elastic-search adapter for Mongoid
2
+ module Mebla
3
+ # Represents the parent module for all errors in Mebla
4
+ module Errors
5
+ # Thrown when configuration fails
6
+ # @note this is a fatal exception
7
+ class MeblaConfigurationException < MeblaFatal
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,14 @@
1
+ # A wrapper for slingshot elastic-search adapter for Mongoid
2
+ module Mebla
3
+ # Represents the parent module for all errors in Mebla
4
+ module Errors
5
+ # Default parent Mebla error for all custom non-fatal errors.
6
+ class MeblaError < ::StandardError
7
+ def initialize(message)
8
+ super message
9
+ ::ActiveSupport::Notifications.
10
+ instrument('mebla_error.mebla', :message => message)
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,14 @@
1
+ # A wrapper for slingshot elastic-search adapter for Mongoid
2
+ module Mebla
3
+ # Represents the parent module for all errors in Mebla
4
+ module Errors
5
+ # Default parent Mebla error for all custom fatal errors.
6
+ class MeblaFatal < ::StandardError
7
+ def initialize(message)
8
+ super message
9
+ ::ActiveSupport::Notifications.
10
+ instrument('mebla_fatal.mebla', :message => message)
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,10 @@
1
+ # A wrapper for slingshot elastic-search adapter for Mongoid
2
+ module Mebla
3
+ # Represents the parent module for all errors in Mebla
4
+ module Errors
5
+ # Thrown when an index operation fails
6
+ # @note this is a fatal exception
7
+ class MeblaIndexException < MeblaFatal
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,9 @@
1
+ # A wrapper for slingshot elastic-search adapter for Mongoid
2
+ module Mebla
3
+ # Represents the parent module for all errors in Mebla
4
+ module Errors
5
+ # Thrown when a synchronization operation fails
6
+ class MeblaSynchronizationException < MeblaError
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,74 @@
1
+ require 'active_support/log_subscriber'
2
+
3
+ # A wrapper for slingshot elastic-search adapter for Mongoid
4
+ module Mebla
5
+ # Handles logging
6
+ class LogSubscriber < ActiveSupport::LogSubscriber
7
+ # Debug message
8
+ def mebla_debug(event)
9
+ debug_green event.payload[:message]
10
+ end
11
+
12
+ # Error message
13
+ def mebla_error(event)
14
+ error_red event.payload[:message]
15
+ end
16
+
17
+ # Info message
18
+ def mebla_info(event)
19
+ info_blue event.payload[:message]
20
+ end
21
+
22
+ # Fatal message
23
+ def mebla_fatal(event)
24
+ fatal_magenta event.payload[:message]
25
+ end
26
+
27
+ # Warning message
28
+ def mebla_warn(event)
29
+ warn_yellow event.payload[:message]
30
+ end
31
+
32
+ # Unknown message
33
+ def mebla_unknown(event)
34
+ unknown event.payload[:message]
35
+ end
36
+
37
+ # --
38
+ # -------------------------------------------------------------
39
+ # Add some colors
40
+ # -------------------------------------------------------------
41
+ # ++
42
+
43
+ # Print a debug message to the log file
44
+ def debug_green(msg)
45
+ debug color(msg, LogSubscriber::GREEN)
46
+ end
47
+
48
+ # Print an error message to the log file
49
+ def error_red(msg)
50
+ error color(msg, LogSubscriber::RED)
51
+ end
52
+
53
+ # Print an info message to the log file
54
+ def info_blue(msg)
55
+ info color(msg, LogSubscriber::BLUE)
56
+ end
57
+
58
+ # Print a fatal message to the log file
59
+ def fatal_magenta(msg)
60
+ fatal color(msg, LogSubscriber::MAGENTA)
61
+ end
62
+
63
+ # Print a warn message to the log file
64
+ def warn_yellow(msg)
65
+ warn color(msg, LogSubscriber::YELLOW)
66
+ end
67
+
68
+ # Returns the main logger for Mebla
69
+ # @return [Logger]
70
+ def self.logger
71
+ Mebla::Configuration.instance.logger
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,341 @@
1
+ # @private
2
+ module Mongoid
3
+ # A wrapper for slingshot elastic-search adapter for Mongoid
4
+ module Mebla
5
+ extend ActiveSupport::Concern
6
+ included do
7
+ # Used to properly represent data types
8
+ unless defined?(SLINGSHOT_TYPE_MAPPING)
9
+ SLINGSHOT_TYPE_MAPPING = {
10
+ 'Date' => 'date',
11
+ 'DateTime' => 'date',
12
+ 'Time' => 'date',
13
+ 'Float' => 'float',
14
+ 'Integer' => 'integer',
15
+ 'BigDecimal' => 'float',
16
+ 'Boolean' => 'boolean'
17
+ }
18
+ end
19
+
20
+ class_attribute :embedded_as
21
+ class_attribute :embedded_parent
22
+ class_attribute :embedded_parent_foreign_key
23
+ class_attribute :slingshot_mappings
24
+ class_attribute :search_fields
25
+ class_attribute :search_relations
26
+ class_attribute :whiny_indexing # set to true to raise errors if indexing fails
27
+
28
+ # make sure critical data remain read only
29
+ private_class_method :"search_fields=", :"slingshot_mappings=",
30
+ :"embedded_parent_foreign_key=", :"embedded_parent=", :"embedded_as="
31
+
32
+ # add callbacks to synchronize modifications with elasticsearch
33
+ after_save :add_to_index
34
+ before_destroy :remove_from_index
35
+
36
+ # by default if synchronizing fails no error is raised
37
+ self.whiny_indexing = false
38
+ end
39
+
40
+ # Defines class methods for Mongoid::Mebla
41
+ module ClassMethods
42
+ # Defines which fields should be indexed and searched
43
+ # @param [*opts] fields
44
+ # @return [nil]
45
+ #
46
+ # Defines a search index on a normal document with custom mappings on "body"::
47
+ #
48
+ # class Document
49
+ # include Mongoid::Document
50
+ # include Mongoid::Mebla
51
+ # field :title
52
+ # field :body
53
+ # field :publish_date, :type => Date
54
+ # #...
55
+ # search_in :title, :publish_date, :body => { :boost => 2.0, :analyzer => 'snowball' }
56
+ # end
57
+ #
58
+ # Defines a search index on a normal document with an index on a field inside a relation::
59
+ #
60
+ # class Document
61
+ # include Mongoid::Document
62
+ # include Mongoid::Mebla
63
+ # field :title
64
+ # field :body
65
+ # field :publish_date, :type => Date
66
+ #
67
+ # referenced_in :blog
68
+ # #...
69
+ # # relations mappings are detected automatically
70
+ # search_in :title, :publish_date, :body => { :boost => 2.0, :analyzer => 'snowball' }, :search_relations => {
71
+ # :blog => [:author, :name]
72
+ # }
73
+ # end
74
+ #
75
+ # Defines a search index on a normal document with an index on method "permalink"::
76
+ #
77
+ # class Document
78
+ # include Mongoid::Document
79
+ # include Mongoid::Mebla
80
+ # field :title
81
+ # field :body
82
+ # field :publish_date, :type => Date
83
+ #
84
+ # def permalink
85
+ # self.title.gsub(/\s/, "-").downcase
86
+ # end
87
+ # #...
88
+ # # methods can also define custom mappings if needed
89
+ # search_in :title, :publish_date, :permalink, :body => { :boost => 2.0, :analyzer => 'snowball' }
90
+ # end
91
+ #
92
+ # Defines a search index on an embedded document with a single parent and custom mappings on "body"::
93
+ #
94
+ # class Document
95
+ # include Mongoid::Document
96
+ # include Mongoid::Mebla
97
+ # field :title
98
+ # field :body
99
+ # field :publish_date, :type => Date
100
+ # #...
101
+ # embedded_in :category
102
+ # search_in :title, :publish_date, :body => { :boost => 2.0, :analyzer => 'snowball' }, :embedded_in => :category
103
+ # end
104
+ def search_in(*opts)
105
+ # Extract advanced indeces
106
+ options = opts.extract_options!.symbolize_keys
107
+ # Extract simple indeces
108
+ attrs = opts.flatten
109
+
110
+
111
+ # If this document is embedded check for the embedded_in option and raise an error if none is specified
112
+ # Example::
113
+ # embedded in a regular class (e.g.: using the default convention for naming the foreign key)
114
+ # :embedded_in => :parent
115
+ if self.embedded?
116
+ if (embedor = options.delete(:embedded_in))
117
+ relation = self.relations[embedor.to_s]
118
+
119
+ # Infer the attributes of the relation
120
+ self.embedded_parent = relation.class_name.constantize
121
+ self.embedded_parent_foreign_key = relation.key.to_s
122
+ self.embedded_as = relation[:inverse_of] || relation.inverse_setter.to_s.gsub(/=$/, '')
123
+
124
+ if self.embedded_as.blank?
125
+ raise ::Mebla::Errors::MeblaConfigurationException.new("Couldn't infer #{embedor.to_s} inverse relation, please set :inverse_of option on the relation.")
126
+ end
127
+ else
128
+ raise ::Mebla::Errors::MeblaConfigurationException.new("#{self.name} is embedded: embedded_in option should be set to the parent class if the document is embedded.")
129
+ end
130
+ end
131
+
132
+ self.search_relations = {}
133
+ # Keep track of relational indecies
134
+ unless (relations_inedcies = options.delete(:search_relations)).nil?
135
+ relations_inedcies.each do |relation, index|
136
+ self.search_relations[relation] = index
137
+ end
138
+ end
139
+
140
+ # Keep track of searchable fields (for indexing)
141
+ self.search_fields = attrs + options.keys
142
+
143
+ # Generate simple indeces' mappings
144
+ attrs_mappings = {}
145
+
146
+ attrs.each do |attribute|
147
+ unless (attr_field = self.fields[attribute.to_s]).nil?
148
+ unless (field_type = attr_field.type.to_s) == "Array" # arrays don't need mappings
149
+ attrs_mappings[attribute] = {:type => SLINGSHOT_TYPE_MAPPING[field_type] || "string"}
150
+ end
151
+ else
152
+ attrs_mappings[attribute] = {:type => "string"}
153
+ end
154
+ end
155
+
156
+ # Generate advanced indeces' mappings
157
+ opts_mappings = {}
158
+
159
+ options.each do |opt, properties|
160
+ unless (attr_field = self.fields[opt.to_s]).nil?
161
+ unless (field_type = attr_field.type.to_s) == "Array"
162
+ opts_mappings[opt] = {:type => SLINGSHOT_TYPE_MAPPING[field_type] || "string" }.merge!(properties)
163
+ end
164
+ else
165
+ opts_mappings[opt] = {:type => "string"}.merge!(properties)
166
+ end
167
+ end
168
+
169
+ # Merge mappings
170
+ self.slingshot_mappings = {}.merge!(attrs_mappings).merge!(opts_mappings)
171
+
172
+ # Keep track of indexed models (for bulk indexing)
173
+ ::Mebla.context.add_indexed_model(self, self.slingshot_type_name.to_sym => prepare_mappings)
174
+ end
175
+
176
+ # Searches the model using Slingshot search DSL
177
+ # @param [String] query a string representing the search query
178
+ # @return [Mebla::Search]
179
+ #
180
+ # Search for all posts with a field 'title' of value 'Testing Search'::
181
+ #
182
+ # Post.search "title: Testing Search"
183
+ def search(query = "")
184
+ ::Mebla.search(query, self.slingshot_type_name)
185
+ end
186
+
187
+ # Retrieves the type name of the model
188
+ # (used to populate the _type variable while indexing)
189
+ # @return [String]
190
+ def slingshot_type_name
191
+ "#{self.name.underscore}"
192
+ end
193
+
194
+ # Enables the modification of records without indexing
195
+ # @return [nil]
196
+ # Example::
197
+ # create record without it being indexed
198
+ # Class.without_indexing do
199
+ # create :title => "This is not indexed", :body => "Nothing will be indexed within this block"
200
+ # end
201
+ # @note you can skip indexing to create, update or delete records without affecting the index
202
+ def without_indexing(&block)
203
+ skip_callback(:save, :after, :add_to_index)
204
+ skip_callback(:destroy, :before, :remove_from_index)
205
+ yield
206
+ set_callback(:save, :after, :add_to_index)
207
+ set_callback(:destroy, :before, :remove_from_index)
208
+ end
209
+
210
+ # Checks if the class is a subclass
211
+ # @return [Boolean] true if class is a subclass
212
+ def sub_class?
213
+ self.superclass != Object
214
+ end
215
+
216
+ private
217
+ # Prepare the mappings required for this document
218
+ # @return [Hash]
219
+ def prepare_mappings
220
+ if self.embedded?
221
+ mappings = {
222
+ :_parent => { :type => self.embedded_parent.name.underscore },
223
+ :_routing => {
224
+ :required => true,
225
+ :path => self.embedded_parent_foreign_key + "_id"
226
+ }
227
+ }
228
+ else
229
+ mappings = {}
230
+ end
231
+
232
+ mappings.merge!({
233
+ :properties => self.slingshot_mappings
234
+ })
235
+ end
236
+ end
237
+
238
+ private
239
+ # Adds the document to the index
240
+ # @return [Boolean] true if the operation is successful
241
+ def add_to_index
242
+ return false unless ::Mebla.context.index_exists? # only try to index if the index exists
243
+ return false unless ::Mebla.context.indexed_models.include?(self.class.name)
244
+
245
+ # Prepare attributes to hash
246
+ to_index_hash = {:id => self.id.to_s}
247
+
248
+ # If the document is embedded set _parent to the parent's id
249
+ if self.embedded?
250
+ parent_id = self.send(self.class.embedded_parent_foreign_key.to_sym).id.to_s
251
+ to_index_hash.merge!({
252
+ (self.class.embedded_parent_foreign_key + "_id").to_sym => parent_id,
253
+ :_parent => parent_id
254
+ })
255
+ end
256
+
257
+ # Add indexed fields to the hash
258
+ self.class.search_fields.each do |sfield|
259
+ if self.class.fields[sfield.to_s]
260
+ to_index_hash[sfield] = self.attributes[sfield.to_s]
261
+ else
262
+ to_index_hash[sfield] = self.send(sfield)
263
+ end
264
+ end
265
+
266
+ # Add indexed relations to the hash
267
+ self.class.search_relations.each do |relation, fields|
268
+ entries = self.send(relation.to_sym)
269
+
270
+ next if entries.nil?
271
+
272
+ if entries.is_a?(Array) || entries.is_a?(Mongoid::Relations::Targets::Enumerable)
273
+ next if entries.empty?
274
+ to_index_hash[relation] = []
275
+ entries.each do |entry|
276
+ if fields.is_a?(Array)
277
+ to_index_hash[relation] << entry.attributes.reject{|key, value| !fields.include?(key.to_sym)}
278
+ else
279
+ to_index_hash[relation] << { fields => entry.attributes[fields.to_s] }
280
+ end
281
+ end
282
+ else
283
+ to_index_hash[relation] = {}
284
+ if fields.is_a?(Array)
285
+ to_index_hash[relation].merge!(entries.attributes.reject{|key, value| !fields.include?(key.to_sym)})
286
+ else
287
+ to_index_hash[relation].merge!({ fields => entries.attributes[fields.to_s] })
288
+ end
289
+ end
290
+ end
291
+
292
+ ::Mebla.log("Indexing #{self.class.slingshot_type_name}: #{to_index_hash.to_s}", :debug)
293
+
294
+ # Index the data under its correct type
295
+ response = ::Mebla.context.slingshot_index.store(self.class.slingshot_type_name.to_sym, to_index_hash)
296
+
297
+ ::Mebla.log("Response for indexing #{self.class.slingshot_type_name}: #{response.to_s}", :debug)
298
+
299
+ # Refresh the index
300
+ ::Mebla.context.refresh_index
301
+ return true
302
+ rescue => error
303
+ raise_synchronization_exception(error)
304
+
305
+ return false
306
+ end
307
+
308
+ # Deletes the document from the index
309
+ # @return [Boolean] true if the operation is successful
310
+ def remove_from_index
311
+ return false unless ::Mebla.context.index_exists? # only try to index if the index exists
312
+
313
+ ::Mebla.log("Removing #{self.class.slingshot_type_name} with id: #{self.id.to_s}", :debug)
314
+
315
+ # Delete the document
316
+ response = Slingshot::Configuration.client.delete "#{::Mebla::Configuration.instance.url}/#{::Mebla.context.slingshot_index_name}/#{self.class.slingshot_type_name}/#{self.id.to_s}"
317
+
318
+ ::Mebla.log("Response for removing #{self.class.slingshot_type_name}: #{response.to_s}", :debug)
319
+
320
+ # Refresh the index
321
+ ::Mebla.context.refresh_index
322
+ return true
323
+ rescue => error
324
+ raise_synchronization_exception(error)
325
+
326
+ return false
327
+ end
328
+
329
+ # Raises synchronization exception in either #add_to_index or #remove_from_index
330
+ def raise_synchronization_exception(error)
331
+ exception_message = "#{self.class.slingshot_type_name} synchronization failed with the following error: #{error.message}"
332
+ if self.class.whiny_indexing
333
+ # Whine when mebla is not able to synchronize
334
+ raise ::Mebla::Errors::MeblaSynchronizationException.new(exception_message)
335
+ else
336
+ # Whining is not allowed, silently log the exception
337
+ ::Mebla.log(exception_message, :warn)
338
+ end
339
+ end
340
+ end
341
+ end