elasticsearch-model-queryable 0.1.5
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.
- checksums.yaml +7 -0
- data/.gitignore +20 -0
- data/CHANGELOG.md +26 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +13 -0
- data/README.md +695 -0
- data/Rakefile +59 -0
- data/elasticsearch-model.gemspec +57 -0
- data/examples/activerecord_article.rb +77 -0
- data/examples/activerecord_associations.rb +162 -0
- data/examples/couchbase_article.rb +66 -0
- data/examples/datamapper_article.rb +71 -0
- data/examples/mongoid_article.rb +68 -0
- data/examples/ohm_article.rb +70 -0
- data/examples/riak_article.rb +52 -0
- data/gemfiles/3.0.gemfile +12 -0
- data/gemfiles/4.0.gemfile +11 -0
- data/lib/elasticsearch/model/adapter.rb +145 -0
- data/lib/elasticsearch/model/adapters/active_record.rb +104 -0
- data/lib/elasticsearch/model/adapters/default.rb +50 -0
- data/lib/elasticsearch/model/adapters/mongoid.rb +92 -0
- data/lib/elasticsearch/model/callbacks.rb +35 -0
- data/lib/elasticsearch/model/client.rb +61 -0
- data/lib/elasticsearch/model/ext/active_record.rb +14 -0
- data/lib/elasticsearch/model/hash_wrapper.rb +15 -0
- data/lib/elasticsearch/model/importing.rb +144 -0
- data/lib/elasticsearch/model/indexing.rb +472 -0
- data/lib/elasticsearch/model/naming.rb +101 -0
- data/lib/elasticsearch/model/proxy.rb +127 -0
- data/lib/elasticsearch/model/response/base.rb +44 -0
- data/lib/elasticsearch/model/response/pagination.rb +173 -0
- data/lib/elasticsearch/model/response/records.rb +69 -0
- data/lib/elasticsearch/model/response/result.rb +63 -0
- data/lib/elasticsearch/model/response/results.rb +31 -0
- data/lib/elasticsearch/model/response.rb +71 -0
- data/lib/elasticsearch/model/searching.rb +107 -0
- data/lib/elasticsearch/model/serializing.rb +35 -0
- data/lib/elasticsearch/model/version.rb +5 -0
- data/lib/elasticsearch/model.rb +157 -0
- data/test/integration/active_record_associations_parent_child.rb +139 -0
- data/test/integration/active_record_associations_test.rb +307 -0
- data/test/integration/active_record_basic_test.rb +179 -0
- data/test/integration/active_record_custom_serialization_test.rb +62 -0
- data/test/integration/active_record_import_test.rb +100 -0
- data/test/integration/active_record_namespaced_model_test.rb +49 -0
- data/test/integration/active_record_pagination_test.rb +132 -0
- data/test/integration/mongoid_basic_test.rb +193 -0
- data/test/test_helper.rb +63 -0
- data/test/unit/adapter_active_record_test.rb +140 -0
- data/test/unit/adapter_default_test.rb +41 -0
- data/test/unit/adapter_mongoid_test.rb +102 -0
- data/test/unit/adapter_test.rb +69 -0
- data/test/unit/callbacks_test.rb +31 -0
- data/test/unit/client_test.rb +27 -0
- data/test/unit/importing_test.rb +176 -0
- data/test/unit/indexing_test.rb +478 -0
- data/test/unit/module_test.rb +57 -0
- data/test/unit/naming_test.rb +76 -0
- data/test/unit/proxy_test.rb +89 -0
- data/test/unit/response_base_test.rb +40 -0
- data/test/unit/response_pagination_kaminari_test.rb +189 -0
- data/test/unit/response_pagination_will_paginate_test.rb +208 -0
- data/test/unit/response_records_test.rb +91 -0
- data/test/unit/response_result_test.rb +90 -0
- data/test/unit/response_results_test.rb +31 -0
- data/test/unit/response_test.rb +67 -0
- data/test/unit/searching_search_request_test.rb +78 -0
- data/test/unit/searching_test.rb +41 -0
- data/test/unit/serializing_test.rb +17 -0
- metadata +466 -0
@@ -0,0 +1,472 @@
|
|
1
|
+
# Licensed to Elasticsearch B.V. under one or more contributor
|
2
|
+
# license agreements. See the NOTICE file distributed with
|
3
|
+
# this work for additional information regarding copyright
|
4
|
+
# ownership. Elasticsearch B.V. licenses this file to you under
|
5
|
+
# the Apache License, Version 2.0 (the "License"); you may
|
6
|
+
# not use this file except in compliance with the License.
|
7
|
+
# You may obtain a copy of the License at
|
8
|
+
#
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
10
|
+
#
|
11
|
+
# Unless required by applicable law or agreed to in writing,
|
12
|
+
# software distributed under the License is distributed on an
|
13
|
+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
14
|
+
# KIND, either express or implied. See the License for the
|
15
|
+
# specific language governing permissions and limitations
|
16
|
+
# under the License.
|
17
|
+
|
18
|
+
module Elasticsearch
|
19
|
+
module Model
|
20
|
+
|
21
|
+
# Provides the necessary support to set up index options (mappings, settings)
|
22
|
+
# as well as instance methods to create, update or delete documents in the index.
|
23
|
+
#
|
24
|
+
# @see ClassMethods#settings
|
25
|
+
# @see ClassMethods#mapping
|
26
|
+
#
|
27
|
+
# @see InstanceMethods#index_document
|
28
|
+
# @see InstanceMethods#update_document
|
29
|
+
# @see InstanceMethods#delete_document
|
30
|
+
#
|
31
|
+
module Indexing
|
32
|
+
|
33
|
+
# Wraps the [index settings](https://www.elastic.co/guide/en/elasticsearch/reference/current/index.html)
|
34
|
+
#
|
35
|
+
class Settings
|
36
|
+
attr_accessor :settings
|
37
|
+
|
38
|
+
def initialize(settings = {})
|
39
|
+
@settings = settings
|
40
|
+
end
|
41
|
+
|
42
|
+
def to_hash
|
43
|
+
@settings
|
44
|
+
end
|
45
|
+
|
46
|
+
def as_json(options = {})
|
47
|
+
to_hash
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
# Wraps the [index mappings](https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping.html)
|
52
|
+
#
|
53
|
+
class Mappings
|
54
|
+
attr_accessor :options, :type
|
55
|
+
|
56
|
+
# @private
|
57
|
+
TYPES_WITH_EMBEDDED_PROPERTIES = %w(object nested)
|
58
|
+
|
59
|
+
def initialize(type = nil, options = {})
|
60
|
+
@type = type
|
61
|
+
@options = options
|
62
|
+
@mapping = {}
|
63
|
+
end
|
64
|
+
|
65
|
+
def indexes(name, options = {}, &block)
|
66
|
+
@mapping[name] = options
|
67
|
+
|
68
|
+
if block_given?
|
69
|
+
@mapping[name][:type] ||= "object"
|
70
|
+
properties = TYPES_WITH_EMBEDDED_PROPERTIES.include?(@mapping[name][:type].to_s) ? :properties : :fields
|
71
|
+
|
72
|
+
@mapping[name][properties] ||= {}
|
73
|
+
|
74
|
+
previous = @mapping
|
75
|
+
begin
|
76
|
+
@mapping = @mapping[name][properties]
|
77
|
+
self.instance_eval(&block)
|
78
|
+
ensure
|
79
|
+
@mapping = previous
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
# Set the type to `text` by default
|
84
|
+
@mapping[name][:type] ||= "text"
|
85
|
+
|
86
|
+
self
|
87
|
+
end
|
88
|
+
|
89
|
+
def to_hash
|
90
|
+
# if @type
|
91
|
+
# { @type.to_sym => @options.merge(properties: @mapping) }
|
92
|
+
# else
|
93
|
+
@options.merge(properties: @mapping)
|
94
|
+
# end
|
95
|
+
end
|
96
|
+
|
97
|
+
def as_json(options = {})
|
98
|
+
to_hash
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
module ClassMethods
|
103
|
+
|
104
|
+
# Defines mappings for the index
|
105
|
+
#
|
106
|
+
# @example Define mapping for model
|
107
|
+
#
|
108
|
+
# class Article
|
109
|
+
# mapping dynamic: 'strict' do
|
110
|
+
# indexes :foo do
|
111
|
+
# indexes :bar
|
112
|
+
# end
|
113
|
+
# indexes :baz
|
114
|
+
# end
|
115
|
+
# end
|
116
|
+
#
|
117
|
+
# Article.mapping.to_hash
|
118
|
+
#
|
119
|
+
# # => { :article =>
|
120
|
+
# # { :dynamic => "strict",
|
121
|
+
# # :properties=>
|
122
|
+
# # { :foo => {
|
123
|
+
# # :type=>"object",
|
124
|
+
# # :properties => {
|
125
|
+
# # :bar => { :type => "string" }
|
126
|
+
# # }
|
127
|
+
# # }
|
128
|
+
# # },
|
129
|
+
# # :baz => { :type=> "string" }
|
130
|
+
# # }
|
131
|
+
# # }
|
132
|
+
#
|
133
|
+
# @example Define index settings and mappings
|
134
|
+
#
|
135
|
+
# class Article
|
136
|
+
# settings number_of_shards: 1 do
|
137
|
+
# mappings do
|
138
|
+
# indexes :foo
|
139
|
+
# end
|
140
|
+
# end
|
141
|
+
# end
|
142
|
+
#
|
143
|
+
# @example Call the mapping method directly
|
144
|
+
#
|
145
|
+
# Article.mapping(dynamic: 'strict') { indexes :foo, type: 'long' }
|
146
|
+
#
|
147
|
+
# Article.mapping.to_hash
|
148
|
+
#
|
149
|
+
# # => {:article=>{:dynamic=>"strict", :properties=>{:foo=>{:type=>"long"}}}}
|
150
|
+
#
|
151
|
+
# The `mappings` and `settings` methods are accessible directly on the model class,
|
152
|
+
# when it doesn't already define them. Use the `__elasticsearch__` proxy otherwise.
|
153
|
+
#
|
154
|
+
def mapping(options = {}, &block)
|
155
|
+
@mapping ||= Mappings.new(document_type, options)
|
156
|
+
|
157
|
+
@mapping.options.update(options) unless options.empty?
|
158
|
+
|
159
|
+
if block_given?
|
160
|
+
@mapping.instance_eval(&block)
|
161
|
+
return self
|
162
|
+
else
|
163
|
+
@mapping
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
alias_method :mappings, :mapping
|
168
|
+
|
169
|
+
# Define settings for the index
|
170
|
+
#
|
171
|
+
# @example Define index settings
|
172
|
+
#
|
173
|
+
# Article.settings(index: { number_of_shards: 1 })
|
174
|
+
#
|
175
|
+
# Article.settings.to_hash
|
176
|
+
#
|
177
|
+
# # => {:index=>{:number_of_shards=>1}}
|
178
|
+
#
|
179
|
+
# You can read settings from any object that responds to :read
|
180
|
+
# as long as its return value can be parsed as either YAML or JSON.
|
181
|
+
#
|
182
|
+
# @example Define index settings from YAML file
|
183
|
+
#
|
184
|
+
# # config/elasticsearch/articles.yml:
|
185
|
+
# #
|
186
|
+
# # index:
|
187
|
+
# # number_of_shards: 1
|
188
|
+
# #
|
189
|
+
#
|
190
|
+
# Article.settings File.open("config/elasticsearch/articles.yml")
|
191
|
+
#
|
192
|
+
# Article.settings.to_hash
|
193
|
+
#
|
194
|
+
# # => { "index" => { "number_of_shards" => 1 } }
|
195
|
+
#
|
196
|
+
#
|
197
|
+
# @example Define index settings from JSON file
|
198
|
+
#
|
199
|
+
# # config/elasticsearch/articles.json:
|
200
|
+
# #
|
201
|
+
# # { "index": { "number_of_shards": 1 } }
|
202
|
+
# #
|
203
|
+
#
|
204
|
+
# Article.settings File.open("config/elasticsearch/articles.json")
|
205
|
+
#
|
206
|
+
# Article.settings.to_hash
|
207
|
+
#
|
208
|
+
# # => { "index" => { "number_of_shards" => 1 } }
|
209
|
+
#
|
210
|
+
def settings(settings = {}, &block)
|
211
|
+
settings = YAML.load(settings.read) if settings.respond_to?(:read)
|
212
|
+
@settings ||= Settings.new(settings)
|
213
|
+
|
214
|
+
@settings.settings.update(settings) unless settings.empty?
|
215
|
+
|
216
|
+
if block_given?
|
217
|
+
self.instance_eval(&block)
|
218
|
+
return self
|
219
|
+
else
|
220
|
+
@settings
|
221
|
+
end
|
222
|
+
end
|
223
|
+
|
224
|
+
def load_settings_from_io(settings)
|
225
|
+
YAML.load(settings.read)
|
226
|
+
end
|
227
|
+
|
228
|
+
# Creates an index with correct name, automatically passing
|
229
|
+
# `settings` and `mappings` defined in the model
|
230
|
+
#
|
231
|
+
# @example Create an index for the `Article` model
|
232
|
+
#
|
233
|
+
# Article.__elasticsearch__.create_index!
|
234
|
+
#
|
235
|
+
# @example Forcefully create (delete first) an index for the `Article` model
|
236
|
+
#
|
237
|
+
# Article.__elasticsearch__.create_index! force: true
|
238
|
+
#
|
239
|
+
# @example Pass a specific index name
|
240
|
+
#
|
241
|
+
# Article.__elasticsearch__.create_index! index: 'my-index'
|
242
|
+
#
|
243
|
+
def create_index!(options = {})
|
244
|
+
options = options.clone
|
245
|
+
|
246
|
+
target_index = options.delete(:index) || self.index_name
|
247
|
+
settings = options.delete(:settings) || self.settings.to_hash
|
248
|
+
mappings = options.delete(:mappings) || self.mappings.to_hash
|
249
|
+
|
250
|
+
delete_index!(options.merge index: target_index) if options[:force]
|
251
|
+
|
252
|
+
unless index_exists?(index: target_index)
|
253
|
+
options.delete(:force)
|
254
|
+
self.client.indices.create({ index: target_index,
|
255
|
+
body: {
|
256
|
+
settings: settings,
|
257
|
+
mappings: mappings,
|
258
|
+
} }.merge(options))
|
259
|
+
end
|
260
|
+
end
|
261
|
+
|
262
|
+
# Returns true if the index exists
|
263
|
+
#
|
264
|
+
# @example Check whether the model's index exists
|
265
|
+
#
|
266
|
+
# Article.__elasticsearch__.index_exists?
|
267
|
+
#
|
268
|
+
# @example Check whether a specific index exists
|
269
|
+
#
|
270
|
+
# Article.__elasticsearch__.index_exists? index: 'my-index'
|
271
|
+
#
|
272
|
+
def index_exists?(options = {})
|
273
|
+
target_index = options[:index] || self.index_name
|
274
|
+
|
275
|
+
self.client.indices.exists(index: target_index, ignore: 404)
|
276
|
+
end
|
277
|
+
|
278
|
+
# Deletes the index with corresponding name
|
279
|
+
#
|
280
|
+
# @example Delete the index for the `Article` model
|
281
|
+
#
|
282
|
+
# Article.__elasticsearch__.delete_index!
|
283
|
+
#
|
284
|
+
# @example Pass a specific index name
|
285
|
+
#
|
286
|
+
# Article.__elasticsearch__.delete_index! index: 'my-index'
|
287
|
+
#
|
288
|
+
def delete_index!(options = {})
|
289
|
+
target_index = options.delete(:index) || self.index_name
|
290
|
+
|
291
|
+
begin
|
292
|
+
self.client.indices.delete index: target_index
|
293
|
+
rescue Exception => e
|
294
|
+
if e.class.to_s =~ /NotFound/ && options[:force]
|
295
|
+
client.transport.transport.logger.debug("[!!!] Index does not exist (#{e.class})") if client.transport.transport.logger
|
296
|
+
nil
|
297
|
+
else
|
298
|
+
raise e
|
299
|
+
end
|
300
|
+
end
|
301
|
+
end
|
302
|
+
|
303
|
+
# Performs the "refresh" operation for the index (useful e.g. in tests)
|
304
|
+
#
|
305
|
+
# @example Refresh the index for the `Article` model
|
306
|
+
#
|
307
|
+
# Article.__elasticsearch__.refresh_index!
|
308
|
+
#
|
309
|
+
# @example Pass a specific index name
|
310
|
+
#
|
311
|
+
# Article.__elasticsearch__.refresh_index! index: 'my-index'
|
312
|
+
#
|
313
|
+
# @see https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-refresh.html
|
314
|
+
#
|
315
|
+
def refresh_index!(options = {})
|
316
|
+
target_index = options.delete(:index) || self.index_name
|
317
|
+
|
318
|
+
begin
|
319
|
+
self.client.indices.refresh index: target_index
|
320
|
+
rescue Exception => e
|
321
|
+
if e.class.to_s =~ /NotFound/ && options[:force]
|
322
|
+
client.transport.transport.logger.debug("[!!!] Index does not exist (#{e.class})") if client.transport.transport.logger
|
323
|
+
nil
|
324
|
+
else
|
325
|
+
raise e
|
326
|
+
end
|
327
|
+
end
|
328
|
+
end
|
329
|
+
end
|
330
|
+
|
331
|
+
module InstanceMethods
|
332
|
+
def self.included(base)
|
333
|
+
# Register callback for storing changed attributes for models
|
334
|
+
# which implement `before_save` and return changed attributes
|
335
|
+
# (ie. when `Elasticsearch::Model` is included)
|
336
|
+
#
|
337
|
+
# @note This is typically triggered only when the module would be
|
338
|
+
# included in the model directly, not within the proxy.
|
339
|
+
#
|
340
|
+
# @see #update_document
|
341
|
+
#
|
342
|
+
base.before_save do |obj|
|
343
|
+
if obj.respond_to?(:changes_to_save) # Rails 5.1
|
344
|
+
changes_to_save = obj.changes_to_save
|
345
|
+
elsif obj.respond_to?(:changes)
|
346
|
+
changes_to_save = obj.changes
|
347
|
+
end
|
348
|
+
|
349
|
+
if changes_to_save
|
350
|
+
attrs = obj.instance_variable_get(:@__changed_model_attributes) || {}
|
351
|
+
latest_changes = changes_to_save.inject({}) { |latest_changes, (k, v)| latest_changes.merge!(k => v.last) }
|
352
|
+
obj.instance_variable_set(:@__changed_model_attributes, attrs.merge(latest_changes))
|
353
|
+
end
|
354
|
+
end if base.respond_to?(:before_save)
|
355
|
+
end
|
356
|
+
|
357
|
+
# Serializes the model instance into JSON (by calling `as_indexed_json`),
|
358
|
+
# and saves the document into the Elasticsearch index.
|
359
|
+
#
|
360
|
+
# @param options [Hash] Optional arguments for passing to the client
|
361
|
+
#
|
362
|
+
# @example Index a record
|
363
|
+
#
|
364
|
+
# @article.__elasticsearch__.index_document
|
365
|
+
# 2013-11-20 16:25:57 +0100: PUT http://localhost:9200/articles/article/1 ...
|
366
|
+
#
|
367
|
+
# @return [Hash] The response from Elasticsearch
|
368
|
+
#
|
369
|
+
# @see http://rubydoc.info/gems/elasticsearch-api/Elasticsearch/API/Actions:index
|
370
|
+
#
|
371
|
+
def index_document(options = {})
|
372
|
+
document = as_indexed_json
|
373
|
+
request = { index: index_name,
|
374
|
+
id: id,
|
375
|
+
body: document }
|
376
|
+
request.merge!(type: document_type) if document_type
|
377
|
+
|
378
|
+
client.index(request.merge!(options))
|
379
|
+
end
|
380
|
+
|
381
|
+
# Deletes the model instance from the index
|
382
|
+
#
|
383
|
+
# @param options [Hash] Optional arguments for passing to the client
|
384
|
+
#
|
385
|
+
# @example Delete a record
|
386
|
+
#
|
387
|
+
# @article.__elasticsearch__.delete_document
|
388
|
+
# 2013-11-20 16:27:00 +0100: DELETE http://localhost:9200/articles/article/1
|
389
|
+
#
|
390
|
+
# @return [Hash] The response from Elasticsearch
|
391
|
+
#
|
392
|
+
# @see http://rubydoc.info/gems/elasticsearch-api/Elasticsearch/API/Actions:delete
|
393
|
+
#
|
394
|
+
def delete_document(options = {})
|
395
|
+
request = { index: index_name,
|
396
|
+
id: self.id }
|
397
|
+
request.merge!(type: document_type) if document_type
|
398
|
+
|
399
|
+
client.delete(request.merge!(options))
|
400
|
+
end
|
401
|
+
|
402
|
+
# Tries to gather the changed attributes of a model instance
|
403
|
+
# (via [ActiveModel::Dirty](http://api.rubyonrails.org/classes/ActiveModel/Dirty.html)),
|
404
|
+
# performing a _partial_ update of the document.
|
405
|
+
#
|
406
|
+
# When the changed attributes are not available, performs full re-index of the record.
|
407
|
+
#
|
408
|
+
# See the {#update_document_attributes} method for updating specific attributes directly.
|
409
|
+
#
|
410
|
+
# @param options [Hash] Optional arguments for passing to the client
|
411
|
+
#
|
412
|
+
# @example Update a document corresponding to the record
|
413
|
+
#
|
414
|
+
# @article = Article.first
|
415
|
+
# @article.update_attribute :title, 'Updated'
|
416
|
+
# # SQL (0.3ms) UPDATE "articles" SET "title" = ?...
|
417
|
+
#
|
418
|
+
# @article.__elasticsearch__.update_document
|
419
|
+
# # 2013-11-20 17:00:05 +0100: POST http://localhost:9200/articles/article/1/_update ...
|
420
|
+
# # 2013-11-20 17:00:05 +0100: > {"doc":{"title":"Updated"}}
|
421
|
+
#
|
422
|
+
# @return [Hash] The response from Elasticsearch
|
423
|
+
#
|
424
|
+
# @see http://rubydoc.info/gems/elasticsearch-api/Elasticsearch/API/Actions:update
|
425
|
+
#
|
426
|
+
def update_document(options = {})
|
427
|
+
if attributes_in_database = self.instance_variable_get(:@__changed_model_attributes).presence
|
428
|
+
attributes = if respond_to?(:as_indexed_json)
|
429
|
+
self.as_indexed_json.select { |k, v| attributes_in_database.keys.map(&:to_s).include? k.to_s }
|
430
|
+
else
|
431
|
+
attributes_in_database
|
432
|
+
end
|
433
|
+
|
434
|
+
unless attributes.empty?
|
435
|
+
request = { index: index_name,
|
436
|
+
id: self.id,
|
437
|
+
body: { doc: attributes } }
|
438
|
+
request.merge!(type: document_type) if document_type
|
439
|
+
|
440
|
+
client.update(request.merge!(options))
|
441
|
+
end
|
442
|
+
else
|
443
|
+
index_document(options)
|
444
|
+
end
|
445
|
+
end
|
446
|
+
|
447
|
+
# Perform a _partial_ update of specific document attributes
|
448
|
+
# (without consideration for changed attributes as in {#update_document})
|
449
|
+
#
|
450
|
+
# @param attributes [Hash] Attributes to be updated
|
451
|
+
# @param options [Hash] Optional arguments for passing to the client
|
452
|
+
#
|
453
|
+
# @example Update the `title` attribute
|
454
|
+
#
|
455
|
+
# @article = Article.first
|
456
|
+
# @article.title = "New title"
|
457
|
+
# @article.__elasticsearch__.update_document_attributes title: "New title"
|
458
|
+
#
|
459
|
+
# @return [Hash] The response from Elasticsearch
|
460
|
+
#
|
461
|
+
def update_document_attributes(attributes, options = {})
|
462
|
+
request = { index: index_name,
|
463
|
+
id: self.id,
|
464
|
+
body: { doc: attributes } }
|
465
|
+
request.merge!(type: document_type) if document_type
|
466
|
+
|
467
|
+
client.update(request.merge!(options))
|
468
|
+
end
|
469
|
+
end
|
470
|
+
end
|
471
|
+
end
|
472
|
+
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
module Elasticsearch
|
2
|
+
module Model
|
3
|
+
|
4
|
+
# Provides methods for getting and setting index name and document type for the model
|
5
|
+
#
|
6
|
+
module Naming
|
7
|
+
|
8
|
+
module ClassMethods
|
9
|
+
|
10
|
+
# Get or set the name of the index
|
11
|
+
#
|
12
|
+
# @example Set the index name for the `Article` model
|
13
|
+
#
|
14
|
+
# class Article
|
15
|
+
# index_name "articles-#{Rails.env}"
|
16
|
+
# end
|
17
|
+
#
|
18
|
+
# @example Directly set the index name for the `Article` model
|
19
|
+
#
|
20
|
+
# Article.index_name "articles-#{Rails.env}"
|
21
|
+
#
|
22
|
+
# TODO: Dynamic names a la Tire -- `Article.index_name { "articles-#{Time.now.year}" }`
|
23
|
+
#
|
24
|
+
def index_name name=nil
|
25
|
+
@index_name = name || @index_name || self.model_name.collection.gsub(/\//, '-')
|
26
|
+
end
|
27
|
+
|
28
|
+
# Set the index name
|
29
|
+
#
|
30
|
+
# @see index_name
|
31
|
+
def index_name=(name)
|
32
|
+
@index_name = name
|
33
|
+
end
|
34
|
+
|
35
|
+
# Get or set the document type
|
36
|
+
#
|
37
|
+
# @example Set the document type for the `Article` model
|
38
|
+
#
|
39
|
+
# class Article
|
40
|
+
# document_type "my-article"
|
41
|
+
# end
|
42
|
+
#
|
43
|
+
# @example Directly set the document type for the `Article` model
|
44
|
+
#
|
45
|
+
# Article.document_type "my-article"
|
46
|
+
#
|
47
|
+
def document_type name=nil
|
48
|
+
@document_type = name || @document_type || self.model_name.element
|
49
|
+
end
|
50
|
+
|
51
|
+
|
52
|
+
# Set the document type
|
53
|
+
#
|
54
|
+
# @see document_type
|
55
|
+
#
|
56
|
+
def document_type=(name)
|
57
|
+
@document_type = name
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
module InstanceMethods
|
62
|
+
|
63
|
+
# Get or set the index name for the model instance
|
64
|
+
#
|
65
|
+
# @example Set the index name for an instance of the `Article` model
|
66
|
+
#
|
67
|
+
# @article.index_name "articles-#{@article.user_id}"
|
68
|
+
# @article.__elasticsearch__.update_document
|
69
|
+
#
|
70
|
+
def index_name name=nil
|
71
|
+
@index_name = name || @index_name || self.class.index_name
|
72
|
+
end
|
73
|
+
|
74
|
+
# Set the index name
|
75
|
+
#
|
76
|
+
# @see index_name
|
77
|
+
def index_name=(name)
|
78
|
+
@index_name = name
|
79
|
+
end
|
80
|
+
|
81
|
+
# @example Set the document type for an instance of the `Article` model
|
82
|
+
#
|
83
|
+
# @article.document_type "my-article"
|
84
|
+
# @article.__elasticsearch__.update_document
|
85
|
+
#
|
86
|
+
def document_type name=nil
|
87
|
+
@document_type = name || @document_type || self.class.document_type
|
88
|
+
end
|
89
|
+
|
90
|
+
# Set the document type
|
91
|
+
#
|
92
|
+
# @see document_type
|
93
|
+
#
|
94
|
+
def document_type=(name)
|
95
|
+
@document_type = name
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
@@ -0,0 +1,127 @@
|
|
1
|
+
module Elasticsearch
|
2
|
+
module Model
|
3
|
+
|
4
|
+
# This module provides a proxy interfacing between the including class and
|
5
|
+
# {Elasticsearch::Model}, preventing the pollution of the including class namespace.
|
6
|
+
#
|
7
|
+
# The only "gateway" between the model and Elasticsearch::Model is the
|
8
|
+
# `__elasticsearch__` class and instance method.
|
9
|
+
#
|
10
|
+
# The including class must be compatible with
|
11
|
+
# [ActiveModel](https://github.com/rails/rails/tree/master/activemodel).
|
12
|
+
#
|
13
|
+
# @example Include the {Elasticsearch::Model} module into an `Article` model
|
14
|
+
#
|
15
|
+
# class Article < ActiveRecord::Base
|
16
|
+
# include Elasticsearch::Model
|
17
|
+
# end
|
18
|
+
#
|
19
|
+
# Article.__elasticsearch__.respond_to?(:search)
|
20
|
+
# # => true
|
21
|
+
#
|
22
|
+
# article = Article.first
|
23
|
+
#
|
24
|
+
# article.respond_to? :index_document
|
25
|
+
# # => false
|
26
|
+
#
|
27
|
+
# article.__elasticsearch__.respond_to?(:index_document)
|
28
|
+
# # => true
|
29
|
+
#
|
30
|
+
module Proxy
|
31
|
+
|
32
|
+
# Define the `__elasticsearch__` class and instance methods in the including class
|
33
|
+
# and register a callback for intercepting changes in the model.
|
34
|
+
#
|
35
|
+
# @note The callback is triggered only when `Elasticsearch::Model` is included in the
|
36
|
+
# module and the functionality is accessible via the proxy.
|
37
|
+
#
|
38
|
+
def self.included(base)
|
39
|
+
base.class_eval do
|
40
|
+
# {ClassMethodsProxy} instance, accessed as `MyModel.__elasticsearch__`
|
41
|
+
#
|
42
|
+
def self.__elasticsearch__ &block
|
43
|
+
@__elasticsearch__ ||= ClassMethodsProxy.new(self)
|
44
|
+
@__elasticsearch__.instance_eval(&block) if block_given?
|
45
|
+
@__elasticsearch__
|
46
|
+
end
|
47
|
+
|
48
|
+
# {InstanceMethodsProxy}, accessed as `@mymodel.__elasticsearch__`
|
49
|
+
#
|
50
|
+
def __elasticsearch__ &block
|
51
|
+
@__elasticsearch__ ||= InstanceMethodsProxy.new(self)
|
52
|
+
@__elasticsearch__.instance_eval(&block) if block_given?
|
53
|
+
@__elasticsearch__
|
54
|
+
end
|
55
|
+
|
56
|
+
# Register a callback for storing changed attributes for models which implement
|
57
|
+
# `before_save` and `changed_attributes` methods (when `Elasticsearch::Model` is included)
|
58
|
+
#
|
59
|
+
# @see http://api.rubyonrails.org/classes/ActiveModel/Dirty.html
|
60
|
+
#
|
61
|
+
before_save do |i|
|
62
|
+
i.__elasticsearch__.instance_variable_set(:@__changed_attributes,
|
63
|
+
Hash[ i.changes.map { |key, value| [key, value.last] } ])
|
64
|
+
end if respond_to?(:before_save) && instance_methods.include?(:changed_attributes)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
# Common module for the proxy classes
|
69
|
+
#
|
70
|
+
module Base
|
71
|
+
attr_reader :target
|
72
|
+
|
73
|
+
def initialize(target)
|
74
|
+
@target = target
|
75
|
+
end
|
76
|
+
|
77
|
+
# Delegate methods to `@target`
|
78
|
+
#
|
79
|
+
def method_missing(method_name, *arguments, &block)
|
80
|
+
target.respond_to?(method_name) ? target.__send__(method_name, *arguments, &block) : super
|
81
|
+
end
|
82
|
+
|
83
|
+
# Respond to methods from `@target`
|
84
|
+
#
|
85
|
+
def respond_to?(method_name, include_private = false)
|
86
|
+
target.respond_to?(method_name) || super
|
87
|
+
end
|
88
|
+
|
89
|
+
def inspect
|
90
|
+
"[PROXY] #{target.inspect}"
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
# A proxy interfacing between Elasticsearch::Model class methods and model class methods
|
95
|
+
#
|
96
|
+
# TODO: Inherit from BasicObject and make Pry's `ls` command behave?
|
97
|
+
#
|
98
|
+
class ClassMethodsProxy
|
99
|
+
include Base
|
100
|
+
end
|
101
|
+
|
102
|
+
# A proxy interfacing between Elasticsearch::Model instance methods and model instance methods
|
103
|
+
#
|
104
|
+
# TODO: Inherit from BasicObject and make Pry's `ls` command behave?
|
105
|
+
#
|
106
|
+
class InstanceMethodsProxy
|
107
|
+
include Base
|
108
|
+
|
109
|
+
def klass
|
110
|
+
target.class
|
111
|
+
end
|
112
|
+
|
113
|
+
def class
|
114
|
+
klass.__elasticsearch__
|
115
|
+
end
|
116
|
+
|
117
|
+
# Need to redefine `as_json` because we're not inheriting from `BasicObject`;
|
118
|
+
# see TODO note above.
|
119
|
+
#
|
120
|
+
def as_json(options={})
|
121
|
+
target.as_json(options)
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|