mebla 1.0.0.rc2

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,225 @@
1
+ # @private
2
+ module Mebla
3
+ # Handles indexing and reindexing
4
+ class Context
5
+ attr_reader :indexed_models, :slingshot_index, :slingshot_index_name
6
+ attr_reader :mappings
7
+
8
+ # @private
9
+ def initialize
10
+ @indexed_models = []
11
+ @mappings = {}
12
+ @slingshot_index = Slingshot::Index.new(Mebla::Configuration.instance.index)
13
+ @slingshot_index_name = Mebla::Configuration.instance.index
14
+ end
15
+
16
+ # @private
17
+ # Adds a model to the list of indexed models
18
+ def add_indexed_model(model, mappings = {})
19
+ model = model.name if model.is_a?(Class)
20
+
21
+ @indexed_models << model
22
+ @indexed_models.uniq!
23
+ @indexed_models.sort!
24
+
25
+ @mappings.merge!(mappings)
26
+ end
27
+
28
+ # Deletes and rebuilds the index
29
+ # @note Doesn't index the data, use Mebla::Context#reindex_data to rebuild the index and index the data
30
+ # @return [nil]
31
+ def rebuild_index
32
+ # Only rebuild if the index exists
33
+ raise ::Mebla::Errors::MeblaIndexException.new("#{@slingshot_index_name} does not exist !! use #create_index to create the index first.") unless index_exists?
34
+
35
+ ::Mebla.log("Rebuilding index")
36
+
37
+ # Delete the index
38
+ if drop_index
39
+ # Create the index
40
+ return build_index
41
+ end
42
+ end
43
+
44
+ # Creates and indexes the document
45
+ # @note Doesn't index the data, use Mebla::Context#index_data to create the index and index the data
46
+ # @return [Boolean] true if operation is successful
47
+ def create_index
48
+ # Only create the index if it doesn't exist
49
+ raise ::Mebla::Errors::MeblaIndexException.new("#{@slingshot_index_name} already exists !! use #rebuild_index to rebuild the index.") if index_exists?
50
+
51
+ ::Mebla.log("Creating index")
52
+
53
+ # Create the index
54
+ build_index
55
+ end
56
+
57
+ # Deletes the index of the document
58
+ # @return [Boolean] true if operation is successful
59
+ def drop_index
60
+ # Only drop the index if it exists
61
+ return true unless index_exists?
62
+
63
+ ::Mebla.log("Dropping index: #{self.slingshot_index_name}", :debug)
64
+
65
+ # Drop the index
66
+ result = @slingshot_index.delete
67
+
68
+ ::Mebla.log("Dropped #{self.slingshot_index_name}: #{result.to_s}", :debug)
69
+
70
+ # Check that the index doesn't exist
71
+ !index_exists?
72
+ end
73
+
74
+ # Checks if the index exists and is available
75
+ # @return [Boolean] true if the index exists and is available, false otherwise
76
+ def index_exists?
77
+ begin
78
+ result = Slingshot::Configuration.client.get "#{Mebla::Configuration.instance.url}/#{@slingshot_index_name}/_status"
79
+ return (result =~ /error/) ? false : true
80
+ rescue RestClient::ResourceNotFound
81
+ return false
82
+ end
83
+ end
84
+
85
+ # Creates the index and indexes the data for all models or a list of models given
86
+ # @param *models a list of symbols each representing a model name to be indexed
87
+ # @return [nil]
88
+ def index_data(*models)
89
+ if models.empty?
90
+ only_index = @indexed_models
91
+ else
92
+ only_index = models.collect{|m| m.to_s}
93
+ end
94
+
95
+ ::Mebla.log("Indexing #{only_index.join(", ")}", :debug)
96
+
97
+ # Build up a bulk query to save processing and time
98
+ bulk_query = ""
99
+ # Keep track of indexed documents
100
+ indexed_count = {}
101
+
102
+ # Create the index
103
+ if create_index
104
+ # Start collecting documents
105
+ only_index.each do |model|
106
+ ::Mebla.log("Indexing: #{model}")
107
+ # Get the class
108
+ to_index = model.camelize.constantize
109
+
110
+ # Get the records
111
+ entries = []
112
+ unless to_index.embedded?
113
+ entries = to_index.all.only(to_index.search_fields)
114
+ else
115
+ parent = to_index.embedded_parent
116
+ access_method = to_index.embedded_as
117
+
118
+ parent.all.each do |parent_record|
119
+ entries += parent_record.send(access_method.to_sym).all.only(to_index.search_fields)
120
+ end
121
+ end
122
+
123
+ # Save the number of entries to be indexed
124
+ indexed_count[model] = entries.count
125
+
126
+ # Build the queries for this model
127
+ entries.each do |document|
128
+ attrs = document.attributes.dup # make sure we dont modify the document it self
129
+ attrs["id"] = attrs.delete("_id") # the id is already added in the meta data of the action part of the query
130
+
131
+ if document.embedded?
132
+ parent_id = document.send(document.class.embedded_parent_foreign_key.to_sym).id.to_s
133
+ attrs[(document.class.embedded_parent_foreign_key + "_id").to_sym] = parent_id
134
+
135
+ # Build add to the bulk query
136
+ bulk_query << build_bulk_query(@slingshot_index_name, to_index.slingshot_type_name, document.id.to_s, attrs, parent_id)
137
+ else
138
+ # Build add to the bulk query
139
+ bulk_query << build_bulk_query(@slingshot_index_name, to_index.slingshot_type_name, document.id.to_s, attrs)
140
+ end
141
+ end
142
+ end
143
+ else
144
+ raise ::Mebla::Errors::MeblaIndexException.new("Could not create #{@slingshot_index_name}!!!")
145
+ end
146
+
147
+ # Add a new line to the query
148
+ bulk_query << '\n'
149
+
150
+ ::Mebla.log("Bulk indexing:\n#{bulk_query}", :debug)
151
+
152
+ # Send the query
153
+ response = Slingshot::Configuration.client.post "#{Mebla::Configuration.instance.url}/_bulk", bulk_query
154
+
155
+ # Only refresh the index if no error ocurred
156
+ unless response =~ /error/
157
+ # Log results
158
+ ::Mebla.log("Indexed #{only_index.count} model(s) to #{self.slingshot_index_name}: #{response}")
159
+ ::Mebla.log("Indexing Report:")
160
+ indexed_count.each do |model_name, count|
161
+ ::Mebla.log("Indexed #{model_name}: #{count} document(s)")
162
+ end
163
+
164
+ # Refresh the index
165
+ refresh_index
166
+ else
167
+ raise ::Mebla::Errors::MeblaIndexException.new("Indexing #{only_index.join(", ")} failed with the following response:\n #{response}")
168
+ end
169
+ rescue RestClient::Exception => error
170
+ raise ::Mebla::Errors::MeblaIndexException.new("Indexing #{only_index.join(", ")} failed with the following error: #{error.message}")
171
+ end
172
+
173
+ # Rebuilds the index and indexes the data for all models or a list of models given
174
+ # @param *models a list of symbols each representing a model name to rebuild it's index
175
+ # @return [nil]
176
+ def reindex_data(*models)
177
+ ::Mebla.log("Rendexing: #{self.slingshot_index_name}")
178
+
179
+ unless drop_index
180
+ raise ::Mebla::Errors::MeblaIndexException.new("Could not drop #{@slingshot_index_name}!!!")
181
+ end
182
+
183
+ # Create the index and index the data
184
+ index_data(models)
185
+ end
186
+
187
+ # Refreshes the index
188
+ # @return [nil]
189
+ def refresh_index
190
+ ::Mebla.log("Refreshing: #{self.slingshot_index_name}", :debug)
191
+
192
+ result = @slingshot_index.refresh
193
+
194
+ ::Mebla.log("Refreshed #{self.slingshot_index_name}: #{result}")
195
+ end
196
+
197
+ private
198
+ # Builds the index according to the mappings set
199
+ # @return [Boolean] true if the index was created successfully, false otherwise
200
+ def build_index
201
+ ::Mebla.log("Building index", :debug)
202
+ # Create the index
203
+ result = @slingshot_index.create :mappings => @mappings
204
+
205
+ ::Mebla.log("Created index: #{result.to_s}")
206
+
207
+ # Check if the index exists
208
+ index_exists?
209
+ end
210
+
211
+ # --
212
+ # OPTIMIZE: should find a solution for not refreshing the index while indexing embedded documents
213
+ # ++
214
+
215
+ # Builds a bulk index query
216
+ # @return [String]
217
+ def build_bulk_query(index_name, type, id, attributes, parent = nil)
218
+ attrs_to_json = attributes.collect{|k,v| "\"#{k}\" : \"#{v}\""}.join(", ")
219
+ <<-eos
220
+ { "index" : { "_index" : "#{index_name}", "_type" : "#{type}", "_id" : "#{id}"#{", \"_parent\" : \"#{parent}\"" if parent}, "refresh" : "true"} }
221
+ {#{attrs_to_json}}
222
+ eos
223
+ end
224
+ end
225
+ end
@@ -0,0 +1,10 @@
1
+ # @private
2
+ module Mebla
3
+ # @private
4
+ module Errors
5
+ # Thrown when configuration fails
6
+ # @note this is a fatal exception
7
+ class MeblaIndexException < MeblaFatal
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,14 @@
1
+ # @private
2
+ module Mebla
3
+ # @private
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
+ # @private
2
+ module Mebla
3
+ # @private
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
+ # @private
2
+ module Mebla
3
+ # @private
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
+ # @private
2
+ module Mebla
3
+ # @private
4
+ module Errors
5
+ # Thrown when a synchronization operation fails
6
+ class MeblaSynchronizationException < MeblaError
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,77 @@
1
+ require 'active_support/log_subscriber'
2
+
3
+ # @private
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
+ # Unkown message
33
+ def mebla_unkown(event)
34
+ unkown 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
+ ingo 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 logger
71
+ Mebla::Configuration.instance.logger
72
+ end
73
+ end
74
+ end
75
+
76
+ # Register the logger
77
+ Mebla::LogSubscriber.attach_to :mebla
@@ -0,0 +1,258 @@
1
+ # @private
2
+ module Mongoid
3
+ # --
4
+ # TODO: add ability to index embedded documents (as part of the parent document)
5
+ # ++
6
+
7
+ # A wrapper for slingshot elastic-search adapter for Mongoid
8
+ module Mebla
9
+ extend ActiveSupport::Concern
10
+ included do
11
+ # Used to properly represent data types
12
+ unless defined?(SLINGSHOT_TYPE_MAPPING)
13
+ SLINGSHOT_TYPE_MAPPING = {
14
+ 'Array' => 'array',
15
+ 'Date' => 'date',
16
+ 'DateTime' => 'date',
17
+ 'Time' => 'date',
18
+ 'Float' => 'float',
19
+ 'Integer' => 'integer',
20
+ 'BigDecimal' => 'float',
21
+ 'Boolean' => 'boolean'
22
+ }
23
+ end
24
+
25
+ cattr_accessor :embedded_as
26
+ cattr_accessor :embedded_parent
27
+ cattr_accessor :embedded_parent_foreign_key
28
+ cattr_accessor :index_mappings
29
+ cattr_accessor :index_options
30
+ cattr_accessor :search_fields
31
+ cattr_accessor :whiny_indexing # set to true to raise errors if indexing fails
32
+
33
+ # make sure critical data remain read only
34
+ private_class_method :"search_fields=", :"index_options=", :"index_mappings=",
35
+ :"embedded_parent_foreign_key=", :"embedded_parent=", :"embedded_as="
36
+
37
+ # add callbacks to synchronize modifications with elasticsearch
38
+ after_save :add_to_index
39
+ before_destroy :remove_from_index
40
+
41
+ # by default if synchronizing fails no error is raised
42
+ self.whiny_indexing = false
43
+ end
44
+
45
+ module ClassMethods
46
+ # Defines which fields should be indexed and searched
47
+ # @param [*opts] fields
48
+ # @return [nil]
49
+ #
50
+ # Defines a search index on a normal document with custom mappings on "body"::
51
+ #
52
+ # class Document
53
+ # include Mongoid::Document
54
+ # include Mongoid::Mebla
55
+ # field :title
56
+ # field :body
57
+ # field :publish_date, :type => Date
58
+ # #...
59
+ # search_in :title, :publish_date, :body => { :boost => 2.0, :analyzer => 'snowball' }
60
+ # end
61
+ #
62
+ # Defines a search index on an embedded document with a single parent and custom mappings on "body"::
63
+ #
64
+ # class Document
65
+ # include Mongoid::Document
66
+ # include Mongoid::Mebla
67
+ # field :title
68
+ # field :body
69
+ # field :publish_date, :type => Date
70
+ # #...
71
+ # embedded_in :category
72
+ # search_in :title, :publish_date, :body => { :boost => 2.0, :analyzer => 'snowball' }, :embedded_in => :category
73
+ # end
74
+ def search_in(*opts)
75
+ # Extract advanced indeces
76
+ options = opts.extract_options!.symbolize_keys
77
+ # Extract simple indeces
78
+ attrs = opts.flatten
79
+
80
+
81
+ # If this document is embedded check for the embedded_in option and raise an error if none is specified
82
+ # Example::
83
+ # embedded in a regular class (e.g.: using the default convention for naming the foreign key)
84
+ # :embedded_in => :parent
85
+ if self.embedded?
86
+ if (embedor = options.delete(:embedded_in))
87
+ relation = self.relations[embedor.to_s]
88
+
89
+ # Infer the attributes of the relation
90
+ self.embedded_parent = relation.class_name.constantize
91
+ self.embedded_parent_foreign_key = relation.key.to_s
92
+ self.embedded_as = relation[:inverse_of] || relation.inverse_setter.to_s.gsub(/=$/, '')
93
+
94
+ if self.embedded_as.blank?
95
+ raise ::Mebla::Errors::MeblaConfigurationException.new("Couldn't infer #{embedor.to_s} inverse relation, please set :inverse_of option on the relation.")
96
+ end
97
+ else
98
+ raise ::Mebla::Errors::MeblaConfigurationException.new("#{self.model_name} is embedded: embedded_in option should be set to the parent class if the document is embedded.")
99
+ end
100
+ end
101
+
102
+ # Keep track of searchable fields (for indexing)
103
+ self.search_fields = attrs + options.keys
104
+
105
+ # Generate simple indeces' mappings
106
+ attrs_mappings = {}
107
+
108
+ attrs.each do |attribute|
109
+ attrs_mappings[attribute] = {:type => SLINGSHOT_TYPE_MAPPING[self.fields[attribute.to_s].type.to_s] || "string"}
110
+ end
111
+
112
+ # Generate advanced indeces' mappings
113
+ opts_mappings = {}
114
+
115
+ options.each do |opt, properties|
116
+ opts_mappings[opt] = {:type => SLINGSHOT_TYPE_MAPPING[self.fields[opt.to_s].type.to_s] || "string" }.merge!(properties)
117
+ end
118
+
119
+ # Merge mappings
120
+ self.index_mappings = {}.merge!(attrs_mappings).merge!(opts_mappings)
121
+
122
+ # Keep track of indexed models (for bulk indexing)
123
+ ::Mebla.context.add_indexed_model(self, self.slingshot_type_name.to_sym => prepare_mappings)
124
+ end
125
+
126
+ # Searches the model using Slingshot search DSL
127
+ # @return [ResultSet]
128
+ #
129
+ # Search for posts with the title 'Testing Search'::
130
+ #
131
+ # Post.search do
132
+ # query do
133
+ # string "title: Testing Search"
134
+ # end
135
+ # end
136
+ #
137
+ # @note For more information about Slingshot search DSL, check http://karmi.github.com/slingshot
138
+ def search(&block)
139
+ ::Mebla.search(self.slingshot_type_name, &block)
140
+ end
141
+
142
+ # Retrieves the type name of the model
143
+ # (used to populate the _type variable while indexing)
144
+ # @return [String]
145
+ def slingshot_type_name #:nodoc:
146
+ "#{self.model_name.underscore}"
147
+ end
148
+
149
+ # Enables the modification of records without indexing
150
+ # @return [nil]
151
+ # Example::
152
+ # create record without it being indexed
153
+ # Class.without_indexing do
154
+ # create :title => "This is not indexed", :body => "Nothing will be indexed within this block"
155
+ # end
156
+ # @note you can skip indexing to create, update or delete records without affecting the index
157
+ def without_indexing(&block)
158
+ skip_callback(:save, :after, :add_to_index)
159
+ skip_callback(:destroy, :before, :remove_from_index)
160
+ yield
161
+ set_callback(:save, :after, :add_to_index)
162
+ set_callback(:destroy, :before, :remove_from_index)
163
+ end
164
+
165
+ private
166
+ # Prepare the mappings required for this document
167
+ # @return [Hash]
168
+ def prepare_mappings
169
+ if self.embedded?
170
+ mappings = {
171
+ :_parent => { :type => self.embedded_parent.name.underscore },
172
+ :_routing => {
173
+ :required => true,
174
+ :path => self.embedded_parent_foreign_key + "_id"
175
+ }
176
+ }
177
+ else
178
+ mappings = {}
179
+ end
180
+
181
+ mappings.merge!({
182
+ :properties => self.index_mappings
183
+ })
184
+ end
185
+ end
186
+
187
+ private
188
+ # Adds the document to the index
189
+ # @return [Boolean] true if the operation is successful
190
+ def add_to_index
191
+ return false unless ::Mebla.context.index_exists? # only try to index if the index exists
192
+
193
+ # Prepare attributes to hash
194
+ to_index_hash = {:id => self.id.to_s}
195
+
196
+ # If the document is embedded set _parent to the parent's id
197
+ if self.embedded?
198
+ parent_id = self.send(self.class.embedded_parent_foreign_key.to_sym).id.to_s
199
+ to_index_hash.merge!({
200
+ (self.class.embedded_parent_foreign_key + "_id").to_sym => parent_id,
201
+ :_parent => parent_id
202
+ })
203
+ end
204
+
205
+ # Add indexed fields to the hash
206
+ self.search_fields.each do |sfield|
207
+ to_index_hash[sfield] = self.attributes[sfield]
208
+ end
209
+
210
+ ::Mebla.log("Indexing #{self.class.slingshot_type_name}: #{to_index_hash.to_s}", :debug)
211
+
212
+ # Index the data under its correct type
213
+ response = ::Mebla.context.slingshot_index.store(self.class.slingshot_type_name.to_sym, to_index_hash)
214
+
215
+ ::Mebla.log("Response for indexing #{self.class.slingshot_type_name}: #{response.to_s}", :debug)
216
+
217
+ # Refresh the index
218
+ ::Mebla.context.refresh_index
219
+ return true
220
+ rescue => error
221
+ raise_synchronization_exception(error)
222
+
223
+ return false
224
+ end
225
+
226
+ # Deletes the document from the index
227
+ # @return [Boolean] true if the operation is successful
228
+ def remove_from_index
229
+ return false unless ::Mebla.context.index_exists? # only try to index if the index exists
230
+
231
+ ::Mebla.log("Removing #{self.class.slingshot_type_name} with id: #{self.id.to_s}", :debug)
232
+
233
+ # Delete the document
234
+ response = Slingshot::Configuration.client.delete "#{::Mebla::Configuration.instance.url}/#{::Mebla.context.slingshot_index_name}/#{self.class.slingshot_type_name}/#{self.id.to_s}"
235
+
236
+ ::Mebla.log("Response for removing #{self.class.slingshot_type_name}: #{response.to_s}", :debug)
237
+
238
+ # Refresh the index
239
+ ::Mebla.context.refresh_index
240
+ return true
241
+ rescue => error
242
+ raise_synchronization_exception(error)
243
+
244
+ return false
245
+ end
246
+
247
+ def raise_synchronization_exception(error)
248
+ exception_message = "#{self.class.slingshot_type_name} synchronization failed with the following error: #{error.message}"
249
+ if self.class.whiny_indexing
250
+ # Whine when mebla is not able to synchronize
251
+ raise ::Mebla::Errors::MeblaSynchronizationException.new(exception_message)
252
+ else
253
+ # Whining is not allowed, silently log the exception
254
+ ::Mebla.log(exception_message, :warn)
255
+ end
256
+ end
257
+ end
258
+ end
@@ -0,0 +1,38 @@
1
+ require 'mebla'
2
+ require 'rails'
3
+
4
+ # @private
5
+ module Mebla
6
+ # @private
7
+ class Railtie < Rails::Railtie
8
+ # Configuration
9
+ initializer "mebla.set_configs" do |app|
10
+ Mebla.configure do |config|
11
+ # Open logfile
12
+ config.logger = Logger.new(
13
+ open("#{Dir.pwd}/logs/#{Rails.env}.mebla.log", "a")
14
+ )
15
+ # Setup the log level
16
+ config.logger.level = case app.config.log_level
17
+ when :info
18
+ Logger::INFO
19
+ when :warn
20
+ Logger::WARN
21
+ when :error
22
+ Logger::ERROR
23
+ when :fatal
24
+ Logger::FATAL
25
+ else
26
+ Logger::DEBUG
27
+ end
28
+
29
+ config.setup_logger
30
+ end
31
+ end
32
+
33
+ # Rake tasks
34
+ rake_tasks do
35
+ load File.expand_path('../tasks.rb', __FILE__)
36
+ end
37
+ end
38
+ end