elasticsearch-model 0.0.1 → 0.1.0.rc1

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.
Files changed (64) hide show
  1. data/.gitignore +3 -0
  2. data/LICENSE.txt +1 -1
  3. data/README.md +669 -8
  4. data/Rakefile +52 -0
  5. data/elasticsearch-model.gemspec +48 -17
  6. data/examples/activerecord_article.rb +77 -0
  7. data/examples/activerecord_associations.rb +153 -0
  8. data/examples/couchbase_article.rb +66 -0
  9. data/examples/datamapper_article.rb +71 -0
  10. data/examples/mongoid_article.rb +68 -0
  11. data/examples/ohm_article.rb +70 -0
  12. data/examples/riak_article.rb +52 -0
  13. data/gemfiles/3.gemfile +11 -0
  14. data/gemfiles/4.gemfile +11 -0
  15. data/lib/elasticsearch/model.rb +151 -1
  16. data/lib/elasticsearch/model/adapter.rb +145 -0
  17. data/lib/elasticsearch/model/adapters/active_record.rb +97 -0
  18. data/lib/elasticsearch/model/adapters/default.rb +44 -0
  19. data/lib/elasticsearch/model/adapters/mongoid.rb +90 -0
  20. data/lib/elasticsearch/model/callbacks.rb +35 -0
  21. data/lib/elasticsearch/model/client.rb +61 -0
  22. data/lib/elasticsearch/model/importing.rb +94 -0
  23. data/lib/elasticsearch/model/indexing.rb +332 -0
  24. data/lib/elasticsearch/model/naming.rb +101 -0
  25. data/lib/elasticsearch/model/proxy.rb +127 -0
  26. data/lib/elasticsearch/model/response.rb +70 -0
  27. data/lib/elasticsearch/model/response/base.rb +44 -0
  28. data/lib/elasticsearch/model/response/pagination.rb +96 -0
  29. data/lib/elasticsearch/model/response/records.rb +71 -0
  30. data/lib/elasticsearch/model/response/result.rb +50 -0
  31. data/lib/elasticsearch/model/response/results.rb +32 -0
  32. data/lib/elasticsearch/model/searching.rb +107 -0
  33. data/lib/elasticsearch/model/serializing.rb +35 -0
  34. data/lib/elasticsearch/model/support/forwardable.rb +44 -0
  35. data/lib/elasticsearch/model/version.rb +1 -1
  36. data/test/integration/active_record_associations_parent_child.rb +138 -0
  37. data/test/integration/active_record_associations_test.rb +306 -0
  38. data/test/integration/active_record_basic_test.rb +139 -0
  39. data/test/integration/active_record_import_test.rb +74 -0
  40. data/test/integration/active_record_namespaced_model_test.rb +49 -0
  41. data/test/integration/active_record_pagination_test.rb +109 -0
  42. data/test/integration/mongoid_basic_test.rb +178 -0
  43. data/test/test_helper.rb +57 -0
  44. data/test/unit/adapter_active_record_test.rb +93 -0
  45. data/test/unit/adapter_default_test.rb +31 -0
  46. data/test/unit/adapter_mongoid_test.rb +87 -0
  47. data/test/unit/adapter_test.rb +69 -0
  48. data/test/unit/callbacks_test.rb +30 -0
  49. data/test/unit/client_test.rb +27 -0
  50. data/test/unit/importing_test.rb +97 -0
  51. data/test/unit/indexing_test.rb +364 -0
  52. data/test/unit/module_test.rb +46 -0
  53. data/test/unit/naming_test.rb +76 -0
  54. data/test/unit/proxy_test.rb +88 -0
  55. data/test/unit/response_base_test.rb +40 -0
  56. data/test/unit/response_pagination_test.rb +159 -0
  57. data/test/unit/response_records_test.rb +87 -0
  58. data/test/unit/response_result_test.rb +52 -0
  59. data/test/unit/response_results_test.rb +31 -0
  60. data/test/unit/response_test.rb +57 -0
  61. data/test/unit/searching_search_request_test.rb +73 -0
  62. data/test/unit/searching_test.rb +39 -0
  63. data/test/unit/serializing_test.rb +17 -0
  64. metadata +418 -11
@@ -0,0 +1,97 @@
1
+ module Elasticsearch
2
+ module Model
3
+ module Adapter
4
+
5
+ # An adapter for ActiveRecord-based models
6
+ #
7
+ module ActiveRecord
8
+
9
+ Adapter.register self,
10
+ lambda { |klass| !!defined?(::ActiveRecord::Base) && klass.ancestors.include?(::ActiveRecord::Base) }
11
+
12
+ module Records
13
+ # Returns an `ActiveRecord::Relation` instance
14
+ #
15
+ def records
16
+ sql_records = klass.where(id: ids)
17
+
18
+ # Re-order records based on the order from Elasticsearch hits
19
+ # by redefining `to_a`, unless the user has called `order()`
20
+ #
21
+ sql_records.instance_exec(response.response['hits']['hits']) do |hits|
22
+ define_singleton_method :to_a do
23
+ if defined?(::ActiveRecord) && ::ActiveRecord::VERSION::MAJOR >= 4
24
+ self.load
25
+ else
26
+ self.__send__(:exec_queries)
27
+ end
28
+ @records.sort_by { |record| hits.index { |hit| hit['_id'].to_s == record.id.to_s } }
29
+ end
30
+ end
31
+
32
+ sql_records
33
+ end
34
+
35
+ # Prevent clash with `ActiveSupport::Dependencies::Loadable`
36
+ #
37
+ def load
38
+ records.load
39
+ end
40
+
41
+ # Intercept call to the `order` method, so we can ignore the order from Elasticsearch
42
+ #
43
+ def order(*args)
44
+ sql_records = records.__send__ :order, *args
45
+
46
+ # Redefine the `to_a` method to the original one
47
+ #
48
+ sql_records.instance_exec do
49
+ define_singleton_method(:to_a) do
50
+ if defined?(::ActiveRecord) && ::ActiveRecord::VERSION::MAJOR >= 4
51
+ self.load
52
+ else
53
+ self.__send__(:exec_queries)
54
+ end
55
+ @records
56
+ end
57
+ end
58
+
59
+ sql_records
60
+ end
61
+ end
62
+
63
+ module Callbacks
64
+
65
+ # Handle index updates (creating, updating or deleting documents)
66
+ # when the model changes, by hooking into the lifecycle
67
+ #
68
+ # @see http://guides.rubyonrails.org/active_record_callbacks.html
69
+ #
70
+ def self.included(base)
71
+ base.class_eval do
72
+ after_commit lambda { __elasticsearch__.index_document }, on: :create
73
+ after_commit lambda { __elasticsearch__.update_document }, on: :update
74
+ after_commit lambda { __elasticsearch__.delete_document }, on: :destroy
75
+ end
76
+ end
77
+ end
78
+
79
+ module Importing
80
+
81
+ # Fetch batches of records from the database
82
+ #
83
+ # @see http://api.rubyonrails.org/classes/ActiveRecord/Batches.html ActiveRecord::Batches.find_in_batches
84
+ #
85
+ def __find_in_batches(options={}, &block)
86
+ find_in_batches(options) do |batch|
87
+ batch_for_bulk = batch.map { |a| { index: { _id: a.id, data: a.__elasticsearch__.as_indexed_json } } }
88
+ yield batch_for_bulk
89
+ end
90
+ end
91
+ end
92
+
93
+ end
94
+
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,44 @@
1
+ module Elasticsearch
2
+ module Model
3
+ module Adapter
4
+
5
+ # The default adapter for models which haven't one registered
6
+ #
7
+ module Default
8
+
9
+ # Module for implementing methods and logic related to fetching records from the database
10
+ #
11
+ module Records
12
+
13
+ # Return the collection of records fetched from the database
14
+ #
15
+ # By default uses `MyModel#find[1, 2, 3]`
16
+ #
17
+ def records
18
+ klass.find(@ids)
19
+ end
20
+ end
21
+
22
+ # Module for implementing methods and logic related to hooking into model lifecycle
23
+ # (e.g. to perform automatic index updates)
24
+ #
25
+ # @see http://api.rubyonrails.org/classes/ActiveModel/Callbacks.html
26
+ module Callbacks
27
+ # noop
28
+ end
29
+
30
+ # Module for efficiently fetching records from the database to import them into the index
31
+ #
32
+ module Importing
33
+
34
+ # @abstract Implement this method in your adapter
35
+ #
36
+ def __find_in_batches(options={}, &block)
37
+ raise NotImplemented, "Method not implemented for default adapter"
38
+ end
39
+ end
40
+
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,90 @@
1
+ module Elasticsearch
2
+ module Model
3
+ module Adapter
4
+
5
+ # An adapter for Mongoid-based models
6
+ #
7
+ # @see http://mongoid.org
8
+ #
9
+ module Mongoid
10
+
11
+ Adapter.register self,
12
+ lambda { |klass| !!defined?(::Mongoid::Document) && klass.ancestors.include?(::Mongoid::Document) }
13
+
14
+ module Records
15
+
16
+ # Return a `Mongoid::Criteria` instance
17
+ #
18
+ def records
19
+ criteria = klass.where(:id.in => ids)
20
+
21
+ criteria.instance_exec(response.response['hits']['hits']) do |hits|
22
+ define_singleton_method :to_a do
23
+ self.entries.sort_by { |e| hits.index { |hit| hit['_id'].to_s == e.id.to_s } }
24
+ end
25
+ end
26
+
27
+ criteria
28
+ end
29
+
30
+ # Intercept call to sorting methods, so we can ignore the order from Elasticsearch
31
+ #
32
+ %w| asc desc order_by |.each do |name|
33
+ define_method name do |*args|
34
+ criteria = records.__send__ name, *args
35
+ criteria.instance_exec do
36
+ define_singleton_method(:to_a) { self.entries }
37
+ end
38
+
39
+ criteria
40
+ end
41
+ end
42
+ end
43
+
44
+ module Callbacks
45
+
46
+ # Handle index updates (creating, updating or deleting documents)
47
+ # when the model changes, by hooking into the lifecycle
48
+ #
49
+ # @see http://mongoid.org/en/mongoid/docs/callbacks.html
50
+ #
51
+ def self.included(base)
52
+ base.after_create { |document| document.__elasticsearch__.index_document }
53
+ base.after_update { |document| document.__elasticsearch__.update_document }
54
+ base.after_destroy { |document| document.__elasticsearch__.delete_document }
55
+ end
56
+ end
57
+
58
+ module Importing
59
+
60
+ # Fetch batches of records from the database
61
+ #
62
+ # @see https://github.com/mongoid/mongoid/issues/1334
63
+ # @see https://github.com/karmi/retire/pull/724
64
+ #
65
+ def __find_in_batches(options={}, &block)
66
+ options[:batch_size] ||= 1_000
67
+ items = []
68
+
69
+ all.each do |item|
70
+ items << item
71
+
72
+ if items.length % options[:batch_size] == 0
73
+ batch_for_bulk = items.map { |a| { index: { _id: a.id.to_s, data: a.as_indexed_json } } }
74
+ yield batch_for_bulk
75
+ items = []
76
+ end
77
+ end
78
+
79
+ unless items.empty?
80
+ batch_for_bulk = items.map { |a| { index: { _id: a.id.to_s, data: a.as_indexed_json } } }
81
+ yield batch_for_bulk
82
+ end
83
+ end
84
+ end
85
+
86
+ end
87
+
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,35 @@
1
+ module Elasticsearch
2
+ module Model
3
+
4
+ # Allows to automatically update index based on model changes,
5
+ # by hooking into the model lifecycle.
6
+ #
7
+ # @note A blocking HTTP request is done during the update process.
8
+ # If you need a more performant/resilient way of updating the index,
9
+ # consider adapting the callbacks behaviour, and use a background
10
+ # processing solution such as [Sidekiq](http://sidekiq.org)
11
+ # or [Resque](https://github.com/resque/resque).
12
+ #
13
+ module Callbacks
14
+
15
+ # When included in a model, automatically injects the callback subscribers (`after_save`, etc)
16
+ #
17
+ # @example Automatically update Elasticsearch index when the model changes
18
+ #
19
+ # class Article
20
+ # include Elasticsearch::Model
21
+ # include Elasticsearch::Model::Callbacks
22
+ # end
23
+ #
24
+ # Article.first.update_attribute :title, 'Updated'
25
+ # # SQL (0.3ms) UPDATE "articles" SET "title" = ...
26
+ # # 2013-11-20 15:08:52 +0100: POST http://localhost:9200/articles/article/1/_update ...
27
+ #
28
+ def self.included(base)
29
+ adapter = Adapter.from_class(base)
30
+ base.__send__ :include, adapter.callbacks_mixin
31
+ end
32
+
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,61 @@
1
+ module Elasticsearch
2
+ module Model
3
+
4
+ # Contains an `Elasticsearch::Client` instance
5
+ #
6
+ module Client
7
+
8
+ module ClassMethods
9
+
10
+ # Get the client for a specific model class
11
+ #
12
+ # @example Get the client for `Article` and perform API request
13
+ #
14
+ # Article.client.cluster.health
15
+ # # => { "cluster_name" => "elasticsearch" ... }
16
+ #
17
+ def client client=nil
18
+ @client ||= Elasticsearch::Model.client
19
+ end
20
+
21
+ # Set the client for a specific model class
22
+ #
23
+ # @example Configure the client for the `Article` model
24
+ #
25
+ # Article.client = Elasticsearch::Client.new host: 'http://api.server:8080'
26
+ # Article.search ...
27
+ #
28
+ def client=(client)
29
+ @client = client
30
+ end
31
+ end
32
+
33
+ module InstanceMethods
34
+
35
+ # Get or set the client for a specific model instance
36
+ #
37
+ # @example Get the client for a specific record and perform API request
38
+ #
39
+ # @article = Article.first
40
+ # @article.client.info
41
+ # # => { "name" => "Node-1", ... }
42
+ #
43
+ def client
44
+ @client ||= self.class.client
45
+ end
46
+
47
+ # Set the client for a specific model instance
48
+ #
49
+ # @example Set the client for a specific record
50
+ #
51
+ # @article = Article.first
52
+ # @article.client = Elasticsearch::Client.new host: 'http://api.server:8080'
53
+ #
54
+ def client=(client)
55
+ @client = client
56
+ end
57
+ end
58
+
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,94 @@
1
+ module Elasticsearch
2
+ module Model
3
+
4
+ # Provides support for easily and efficiently importing large amounts of
5
+ # records from the including class into the index.
6
+ #
7
+ # @see ClassMethods#import
8
+ #
9
+ module Importing
10
+
11
+ # When included in a model, adds the importing methods.
12
+ #
13
+ # @example Import all records from the `Article` model
14
+ #
15
+ # Article.import
16
+ #
17
+ # @see #import
18
+ #
19
+ def self.included(base)
20
+ base.__send__ :extend, ClassMethods
21
+
22
+ adapter = Adapter.from_class(base)
23
+ base.__send__ :include, adapter.importing_mixin
24
+ base.__send__ :extend, adapter.importing_mixin
25
+ end
26
+
27
+ module ClassMethods
28
+
29
+ # Import all model records into the index
30
+ #
31
+ # The method will pick up correct strategy based on the `Importing` module
32
+ # defined in the corresponding adapter.
33
+ #
34
+ # @param options [Hash] Options passed to the underlying `__find_in_batches`method
35
+ # @param block [Proc] Optional block to evaluate for each batch
36
+ #
37
+ # @yield [Hash] Gives the Hash with the Elasticsearch response to the block
38
+ #
39
+ # @return [Fixnum] Number of errors encountered during importing
40
+ #
41
+ # @example Import all records into the index
42
+ #
43
+ # Article.import
44
+ #
45
+ # @example Set the batch size to 100
46
+ #
47
+ # Article.import batch_size: 100
48
+ #
49
+ # @example Process the response from Elasticsearch
50
+ #
51
+ # Article.import do |response|
52
+ # puts "Got " + response['items'].select { |i| i['index']['error'] }.size.to_s + " errors"
53
+ # end
54
+ #
55
+ # @example Delete and create the index with appropriate settings and mappings
56
+ #
57
+ # Article.import force: true
58
+ #
59
+ # @example Refresh the index after importing all batches
60
+ #
61
+ # Article.import refresh: true
62
+ #
63
+ #
64
+ def import(options={}, &block)
65
+ errors = 0
66
+
67
+ if options.delete(:force)
68
+ self.create_index! force: true
69
+ end
70
+
71
+ refresh = options.delete(:refresh) || false
72
+
73
+ __find_in_batches(options) do |batch|
74
+ response = client.bulk \
75
+ index: index_name,
76
+ type: document_type,
77
+ body: batch
78
+
79
+ yield response if block_given?
80
+
81
+ errors += response['items'].map { |k, v| k.values.first['error'] }.compact.length
82
+ end
83
+
84
+ self.refresh_index! if refresh
85
+
86
+ return errors
87
+ end
88
+
89
+ end
90
+
91
+ end
92
+
93
+ end
94
+ end
@@ -0,0 +1,332 @@
1
+ module Elasticsearch
2
+ module Model
3
+
4
+ # Provides the necessary support to set up index options (mappings, settings)
5
+ # as well as instance methods to create, update or delete documents in the index.
6
+ #
7
+ # @see ClassMethods#settings
8
+ # @see ClassMethods#mapping
9
+ #
10
+ # @see InstanceMethods#index_document
11
+ # @see InstanceMethods#update_document
12
+ # @see InstanceMethods#delete_document
13
+ #
14
+ module Indexing
15
+
16
+ # Wraps the [index settings](http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/setup-configuration.html#configuration-index-settings)
17
+ #
18
+ class Settings
19
+ attr_accessor :settings
20
+
21
+ def initialize(settings={})
22
+ @settings = settings
23
+ end
24
+
25
+ def to_hash
26
+ @settings
27
+ end
28
+
29
+ def as_json(options={})
30
+ to_hash
31
+ end
32
+ end
33
+
34
+ # Wraps the [index mappings](http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/mapping.html)
35
+ #
36
+ class Mappings
37
+ attr_accessor :options
38
+
39
+ def initialize(type, options={})
40
+ @type = type
41
+ @options = options
42
+ @mapping = {}
43
+ end
44
+
45
+ def indexes(name, options = {}, &block)
46
+ @mapping[name] = options
47
+
48
+ if block_given?
49
+ @mapping[name][:type] ||= 'object'
50
+ properties = @mapping[name][:type] == 'multi_field' ? :fields : :properties
51
+
52
+ @mapping[name][properties] ||= {}
53
+
54
+ previous = @mapping
55
+ begin
56
+ @mapping = @mapping[name][properties]
57
+ self.instance_eval(&block)
58
+ ensure
59
+ @mapping = previous
60
+ end
61
+ end
62
+
63
+ # Set the type to `string` by default
64
+ #
65
+ @mapping[name][:type] ||= 'string'
66
+
67
+ self
68
+ end
69
+
70
+ def to_hash
71
+ { @type.to_sym => @options.merge( properties: @mapping ) }
72
+ end
73
+
74
+ def as_json(options={})
75
+ to_hash
76
+ end
77
+ end
78
+
79
+ module ClassMethods
80
+
81
+ # Defines mappings for the index
82
+ #
83
+ # @example Define mapping for model
84
+ #
85
+ # class Article
86
+ # mapping dynamic: 'strict' do
87
+ # indexes :foo do
88
+ # indexes :bar
89
+ # end
90
+ # indexes :baz
91
+ # end
92
+ # end
93
+ #
94
+ # Article.mapping.to_hash
95
+ #
96
+ # # => { :article =>
97
+ # # { :dynamic => "strict",
98
+ # # :properties=>
99
+ # # { :foo => {
100
+ # # :type=>"object",
101
+ # # :properties => {
102
+ # # :bar => { :type => "string" }
103
+ # # }
104
+ # # }
105
+ # # },
106
+ # # :baz => { :type=> "string" }
107
+ # # }
108
+ # # }
109
+ #
110
+ # @example Define index settings and mappings
111
+ #
112
+ # class Article
113
+ # settings number_of_shards: 1 do
114
+ # mappings do
115
+ # indexes :foo
116
+ # end
117
+ # end
118
+ # end
119
+ #
120
+ # @example Call the mapping method directly
121
+ #
122
+ # Article.mapping(dynamic: 'strict') { indexes :foo, type: 'long' }
123
+ #
124
+ # Article.mapping.to_hash
125
+ #
126
+ # # => {:article=>{:dynamic=>"strict", :properties=>{:foo=>{:type=>"long"}}}}
127
+ #
128
+ # The `mappings` and `settings` methods are accessible directly on the model class,
129
+ # when it doesn't already defines them. Use the `__elasticsearch__` proxy otherwise.
130
+ #
131
+ def mapping(options={}, &block)
132
+ @mapping ||= Mappings.new(document_type, options)
133
+
134
+ if block_given?
135
+ @mapping.options.update(options)
136
+
137
+ @mapping.instance_eval(&block)
138
+ return self
139
+ else
140
+ @mapping
141
+ end
142
+ end; alias_method :mappings, :mapping
143
+
144
+ # Define settings for the index
145
+ #
146
+ # @example Define index settings
147
+ #
148
+ # Article.settings(index: { number_of_shards: 1 })
149
+ #
150
+ # Article.settings.to_hash
151
+ #
152
+ # # => {:index=>{:number_of_shards=>1}}
153
+ #
154
+ def settings(settings={}, &block)
155
+ @settings ||= Settings.new(settings)
156
+
157
+ @settings.settings.update(settings) unless settings.empty?
158
+
159
+ if block_given?
160
+ self.instance_eval(&block)
161
+ return self
162
+ else
163
+ @settings
164
+ end
165
+ end
166
+
167
+ # Creates an index with correct name, automatically passing
168
+ # `settings` and `mappings` defined in the model
169
+ #
170
+ # @example Create an index for the `Article` model
171
+ #
172
+ # Article.__elasticsearch__.create_index!
173
+ #
174
+ # @example Forcefully create (delete first) an index for the `Article` model
175
+ #
176
+ # Article.__elasticsearch__.create_index! force: true
177
+ #
178
+ def create_index!(options={})
179
+ delete_index!(options) if options[:force]
180
+
181
+ unless ( self.client.indices.exists(index: self.index_name) rescue false )
182
+ begin
183
+ self.client.indices.create index: self.index_name,
184
+ body: {
185
+ settings: self.settings.to_hash,
186
+ mappings: self.mappings.to_hash }
187
+ rescue Exception => e
188
+ unless e.class.to_s =~ /NotFound/ && options[:force]
189
+ STDERR.puts "[!!!] Error when creating the index: #{e.class}", "#{e.message}"
190
+ end
191
+ end
192
+ else
193
+ end
194
+ end
195
+
196
+ # Deletes the index with corresponding name
197
+ #
198
+ # @example Delete the index for the `Article` model
199
+ #
200
+ # Article.__elasticsearch__.delete_index!
201
+ #
202
+ def delete_index!(options={})
203
+ begin
204
+ self.client.indices.delete index: self.index_name
205
+ rescue Exception => e
206
+ unless e.class.to_s =~ /NotFound/ && options[:force]
207
+ STDERR.puts "[!!!] Error when deleting the index: #{e.class}", "#{e.message}"
208
+ end
209
+ end
210
+ end
211
+
212
+ # Performs the "refresh" operation for the index (useful e.g. in tests)
213
+ #
214
+ # @example Refresh the index for the `Article` model
215
+ #
216
+ # Article.__elasticsearch__.refresh_index!
217
+ #
218
+ # @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-refresh.html
219
+ #
220
+ def refresh_index!(options={})
221
+ begin
222
+ self.client.indices.refresh index: self.index_name
223
+ rescue Exception => e
224
+ unless e.class.to_s =~ /NotFound/ && options[:force]
225
+ STDERR.puts "[!!!] Error when refreshing the index: #{e.class}", "#{e.message}"
226
+ end
227
+ end
228
+ end
229
+ end
230
+
231
+ module InstanceMethods
232
+
233
+ def self.included(base)
234
+ # Register callback for storing changed attributes for models
235
+ # which implement `before_save` and `changed_attributes` methods
236
+ #
237
+ # @note This is typically triggered only when the module would be
238
+ # included in the model directly, not within the proxy.
239
+ #
240
+ # @see #update_document
241
+ #
242
+ base.before_save do |instance|
243
+ instance.instance_variable_set(:@__changed_attributes,
244
+ Hash[ instance.changes.map { |key, value| [key, value.last] } ])
245
+ end if base.respond_to?(:before_save) && base.instance_methods.include?(:changed_attributes)
246
+ end
247
+
248
+ # Serializes the model instance into JSON (by calling `as_indexed_json`),
249
+ # and saves the document into the Elasticsearch index.
250
+ #
251
+ # @param options [Hash] Optional arguments for passing to the client
252
+ #
253
+ # @example Index a record
254
+ #
255
+ # @article.__elasticsearch__.index_document
256
+ # 2013-11-20 16:25:57 +0100: PUT http://localhost:9200/articles/article/1 ...
257
+ #
258
+ # @return [Hash] The response from Elasticsearch
259
+ #
260
+ # @see http://rubydoc.info/gems/elasticsearch-api/Elasticsearch/API/Actions:index
261
+ #
262
+ def index_document(options={})
263
+ document = self.as_indexed_json
264
+
265
+ client.index(
266
+ { index: index_name,
267
+ type: document_type,
268
+ id: self.id,
269
+ body: document }.merge(options)
270
+ )
271
+ end
272
+
273
+ # Deletes the model instance from the index
274
+ #
275
+ # @param options [Hash] Optional arguments for passing to the client
276
+ #
277
+ # @example Delete a record
278
+ #
279
+ # @article.__elasticsearch__.delete_document
280
+ # 2013-11-20 16:27:00 +0100: DELETE http://localhost:9200/articles/article/1
281
+ #
282
+ # @return [Hash] The response from Elasticsearch
283
+ #
284
+ # @see http://rubydoc.info/gems/elasticsearch-api/Elasticsearch/API/Actions:delete
285
+ #
286
+ def delete_document(options={})
287
+ client.delete(
288
+ { index: index_name,
289
+ type: document_type,
290
+ id: self.id }.merge(options)
291
+ )
292
+ end
293
+
294
+ # Tries to gather the changed attributes of a model instance
295
+ # (via [ActiveModel::Dirty](http://api.rubyonrails.org/classes/ActiveModel/Dirty.html)),
296
+ # performing a _partial_ update of the document.
297
+ #
298
+ # When the changed attributes are not available, performs full re-index of the record.
299
+ #
300
+ # @param options [Hash] Optional arguments for passing to the client
301
+ #
302
+ # @example Update a document corresponding to the record
303
+ #
304
+ # @article = Article.first
305
+ # @article.update_attribute :title, 'Updated'
306
+ # # SQL (0.3ms) UPDATE "articles" SET "title" = ?...
307
+ #
308
+ # @article.__elasticsearch__.update_document
309
+ # # 2013-11-20 17:00:05 +0100: POST http://localhost:9200/articles/article/1/_update ...
310
+ # # 2013-11-20 17:00:05 +0100: > {"doc":{"title":"Updated"}}
311
+ #
312
+ # @return [Hash] The response from Elasticsearch
313
+ #
314
+ # @see http://rubydoc.info/gems/elasticsearch-api/Elasticsearch/API/Actions:update
315
+ #
316
+ def update_document(options={})
317
+ if changed_attributes = self.instance_variable_get(:@__changed_attributes)
318
+ client.update(
319
+ { index: index_name,
320
+ type: document_type,
321
+ id: self.id,
322
+ body: { doc: changed_attributes } }.merge(options)
323
+ )
324
+ else
325
+ index_document(options)
326
+ end
327
+ end
328
+ end
329
+
330
+ end
331
+ end
332
+ end