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.
Files changed (70) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +20 -0
  3. data/CHANGELOG.md +26 -0
  4. data/Gemfile +4 -0
  5. data/LICENSE.txt +13 -0
  6. data/README.md +695 -0
  7. data/Rakefile +59 -0
  8. data/elasticsearch-model.gemspec +57 -0
  9. data/examples/activerecord_article.rb +77 -0
  10. data/examples/activerecord_associations.rb +162 -0
  11. data/examples/couchbase_article.rb +66 -0
  12. data/examples/datamapper_article.rb +71 -0
  13. data/examples/mongoid_article.rb +68 -0
  14. data/examples/ohm_article.rb +70 -0
  15. data/examples/riak_article.rb +52 -0
  16. data/gemfiles/3.0.gemfile +12 -0
  17. data/gemfiles/4.0.gemfile +11 -0
  18. data/lib/elasticsearch/model/adapter.rb +145 -0
  19. data/lib/elasticsearch/model/adapters/active_record.rb +104 -0
  20. data/lib/elasticsearch/model/adapters/default.rb +50 -0
  21. data/lib/elasticsearch/model/adapters/mongoid.rb +92 -0
  22. data/lib/elasticsearch/model/callbacks.rb +35 -0
  23. data/lib/elasticsearch/model/client.rb +61 -0
  24. data/lib/elasticsearch/model/ext/active_record.rb +14 -0
  25. data/lib/elasticsearch/model/hash_wrapper.rb +15 -0
  26. data/lib/elasticsearch/model/importing.rb +144 -0
  27. data/lib/elasticsearch/model/indexing.rb +472 -0
  28. data/lib/elasticsearch/model/naming.rb +101 -0
  29. data/lib/elasticsearch/model/proxy.rb +127 -0
  30. data/lib/elasticsearch/model/response/base.rb +44 -0
  31. data/lib/elasticsearch/model/response/pagination.rb +173 -0
  32. data/lib/elasticsearch/model/response/records.rb +69 -0
  33. data/lib/elasticsearch/model/response/result.rb +63 -0
  34. data/lib/elasticsearch/model/response/results.rb +31 -0
  35. data/lib/elasticsearch/model/response.rb +71 -0
  36. data/lib/elasticsearch/model/searching.rb +107 -0
  37. data/lib/elasticsearch/model/serializing.rb +35 -0
  38. data/lib/elasticsearch/model/version.rb +5 -0
  39. data/lib/elasticsearch/model.rb +157 -0
  40. data/test/integration/active_record_associations_parent_child.rb +139 -0
  41. data/test/integration/active_record_associations_test.rb +307 -0
  42. data/test/integration/active_record_basic_test.rb +179 -0
  43. data/test/integration/active_record_custom_serialization_test.rb +62 -0
  44. data/test/integration/active_record_import_test.rb +100 -0
  45. data/test/integration/active_record_namespaced_model_test.rb +49 -0
  46. data/test/integration/active_record_pagination_test.rb +132 -0
  47. data/test/integration/mongoid_basic_test.rb +193 -0
  48. data/test/test_helper.rb +63 -0
  49. data/test/unit/adapter_active_record_test.rb +140 -0
  50. data/test/unit/adapter_default_test.rb +41 -0
  51. data/test/unit/adapter_mongoid_test.rb +102 -0
  52. data/test/unit/adapter_test.rb +69 -0
  53. data/test/unit/callbacks_test.rb +31 -0
  54. data/test/unit/client_test.rb +27 -0
  55. data/test/unit/importing_test.rb +176 -0
  56. data/test/unit/indexing_test.rb +478 -0
  57. data/test/unit/module_test.rb +57 -0
  58. data/test/unit/naming_test.rb +76 -0
  59. data/test/unit/proxy_test.rb +89 -0
  60. data/test/unit/response_base_test.rb +40 -0
  61. data/test/unit/response_pagination_kaminari_test.rb +189 -0
  62. data/test/unit/response_pagination_will_paginate_test.rb +208 -0
  63. data/test/unit/response_records_test.rb +91 -0
  64. data/test/unit/response_result_test.rb +90 -0
  65. data/test/unit/response_results_test.rb +31 -0
  66. data/test/unit/response_test.rb +67 -0
  67. data/test/unit/searching_search_request_test.rb +78 -0
  68. data/test/unit/searching_test.rb +41 -0
  69. data/test/unit/serializing_test.rb +17 -0
  70. metadata +466 -0
@@ -0,0 +1,11 @@
1
+ # Usage:
2
+ #
3
+ # $ BUNDLE_GEMFILE=./gemfiles/4.0.gemfile bundle install
4
+ # $ BUNDLE_GEMFILE=./gemfiles/4.0.gemfile bundle exec rake test:integration
5
+
6
+ source 'https://rubygems.org'
7
+
8
+ gemspec path: '../'
9
+
10
+ gem 'activemodel', '~> 4'
11
+ gem 'activerecord', '~> 4'
@@ -0,0 +1,145 @@
1
+ module Elasticsearch
2
+ module Model
3
+
4
+ # Contains an adapter which provides OxM-specific implementations for common behaviour:
5
+ #
6
+ # * {Adapter::Adapter#records_mixin Fetching records from the database}
7
+ # * {Adapter::Adapter#callbacks_mixin Model callbacks for automatic index updates}
8
+ # * {Adapter::Adapter#importing_mixin Efficient bulk loading from the database}
9
+ #
10
+ # @see Elasticsearch::Model::Adapter::Default
11
+ # @see Elasticsearch::Model::Adapter::ActiveRecord
12
+ # @see Elasticsearch::Model::Adapter::Mongoid
13
+ #
14
+ module Adapter
15
+
16
+ # Returns an adapter based on the Ruby class passed
17
+ #
18
+ # @example Create an adapter for an ActiveRecord-based model
19
+ #
20
+ # class Article < ActiveRecord::Base; end
21
+ #
22
+ # myadapter = Elasticsearch::Model::Adapter.from_class(Article)
23
+ # myadapter.adapter
24
+ # # => Elasticsearch::Model::Adapter::ActiveRecord
25
+ #
26
+ # @see Adapter.adapters The list of included adapters
27
+ # @see Adapter.register Register a custom adapter
28
+ #
29
+ def from_class(klass)
30
+ Adapter.new(klass)
31
+ end; module_function :from_class
32
+
33
+ # Returns registered adapters
34
+ #
35
+ # @see ::Elasticsearch::Model::Adapter::Adapter.adapters
36
+ #
37
+ def adapters
38
+ Adapter.adapters
39
+ end; module_function :adapters
40
+
41
+ # Registers an adapter
42
+ #
43
+ # @see ::Elasticsearch::Model::Adapter::Adapter.register
44
+ #
45
+ def register(name, condition)
46
+ Adapter.register(name, condition)
47
+ end; module_function :register
48
+
49
+ # Contains an adapter for specific OxM or architecture.
50
+ #
51
+ class Adapter
52
+ attr_reader :klass
53
+
54
+ def initialize(klass)
55
+ @klass = klass
56
+ end
57
+
58
+ # Registers an adapter for specific condition
59
+ #
60
+ # @param name [Module] The module containing the implemented interface
61
+ # @param condition [Proc] An object with a `call` method which is evaluated in {.adapter}
62
+ #
63
+ # @example Register an adapter for DataMapper
64
+ #
65
+ # module DataMapperAdapter
66
+ #
67
+ # # Implement the interface for fetching records
68
+ # #
69
+ # module Records
70
+ # def records
71
+ # klass.all(id: @ids)
72
+ # end
73
+ #
74
+ # # ...
75
+ # end
76
+ # end
77
+ #
78
+ # # Register the adapter
79
+ # #
80
+ # Elasticsearch::Model::Adapter.register(
81
+ # DataMapperAdapter,
82
+ # lambda { |klass|
83
+ # defined?(::DataMapper::Resource) and klass.ancestors.include?(::DataMapper::Resource)
84
+ # }
85
+ # )
86
+ #
87
+ def self.register(name, condition)
88
+ self.adapters[name] = condition
89
+ end
90
+
91
+ # Return the collection of registered adapters
92
+ #
93
+ # @example Return the currently registered adapters
94
+ #
95
+ # Elasticsearch::Model::Adapter.adapters
96
+ # # => {
97
+ # # Elasticsearch::Model::Adapter::ActiveRecord => #<Proc:0x007...(lambda)>,
98
+ # # Elasticsearch::Model::Adapter::Mongoid => #<Proc:0x007... (lambda)>,
99
+ # # }
100
+ #
101
+ # @return [Hash] The collection of adapters
102
+ #
103
+ def self.adapters
104
+ @adapters ||= {}
105
+ end
106
+
107
+ # Return the module with {Default::Records} interface implementation
108
+ #
109
+ # @api private
110
+ #
111
+ def records_mixin
112
+ adapter.const_get(:Records)
113
+ end
114
+
115
+ # Return the module with {Default::Callbacks} interface implementation
116
+ #
117
+ # @api private
118
+ #
119
+ def callbacks_mixin
120
+ adapter.const_get(:Callbacks)
121
+ end
122
+
123
+ # Return the module with {Default::Importing} interface implementation
124
+ #
125
+ # @api private
126
+ #
127
+ def importing_mixin
128
+ adapter.const_get(:Importing)
129
+ end
130
+
131
+ # Returns the adapter module
132
+ #
133
+ # @api private
134
+ #
135
+ def adapter
136
+ @adapter ||= begin
137
+ self.class.adapters.find( lambda {[]} ) { |name, condition| condition.call(klass) }.first \
138
+ || Elasticsearch::Model::Adapter::Default
139
+ end
140
+ end
141
+
142
+ end
143
+ end
144
+ end
145
+ end
@@ -0,0 +1,104 @@
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(klass.primary_key => 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 (used by the import method)
82
+ #
83
+ #
84
+ # @see http://api.rubyonrails.org/classes/ActiveRecord/Batches.html ActiveRecord::Batches.find_in_batches
85
+ #
86
+ def __find_in_batches(options={}, &block)
87
+ named_scope = options.delete(:scope)
88
+ preprocess = options.delete(:preprocess)
89
+
90
+ scope = named_scope ? self.__send__(named_scope) : self
91
+
92
+ scope.find_in_batches(options) do |batch|
93
+ yield (preprocess ? self.__send__(preprocess, batch) : batch)
94
+ end
95
+ end
96
+
97
+ def __transform
98
+ lambda { |model| { index: { _id: model.id, data: model.__elasticsearch__.as_indexed_json } } }
99
+ end
100
+ end
101
+ end
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,50 @@
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
+
40
+ # @abstract Implement this method in your adapter
41
+ #
42
+ def __transform
43
+ raise NotImplemented, "Method not implemented for default adapter"
44
+ end
45
+ end
46
+
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,92 @@
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
+ yield items
74
+ items = []
75
+ end
76
+ end
77
+
78
+ unless items.empty?
79
+ yield items
80
+ end
81
+ end
82
+
83
+ def __transform
84
+ lambda {|a| { index: { _id: a.id.to_s, data: a.as_indexed_json } }}
85
+ end
86
+ end
87
+
88
+ end
89
+
90
+ end
91
+ end
92
+ 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,14 @@
1
+ # Prevent `MyModel.inspect` failing with `ActiveRecord::ConnectionNotEstablished`
2
+ # (triggered by elasticsearch-model/lib/elasticsearch/model.rb:79:in `included')
3
+ #
4
+ ActiveRecord::Base.instance_eval do
5
+ class << self
6
+ def inspect_with_rescue
7
+ inspect_without_rescue
8
+ rescue ActiveRecord::ConnectionNotEstablished
9
+ "#{self}(no database connection)"
10
+ end
11
+
12
+ alias_method_chain :inspect, :rescue
13
+ end
14
+ end if defined?(ActiveRecord) && ActiveRecord::VERSION::STRING < '4'
@@ -0,0 +1,15 @@
1
+ module Elasticsearch
2
+ module Model
3
+
4
+ # Subclass of `Hashie::Mash` to wrap Hash-like structures
5
+ # (responses from Elasticsearch, search definitions, etc)
6
+ #
7
+ # The primary goal of the subclass is to disable the
8
+ # warning being printed by Hashie for re-defined
9
+ # methods, such as `sort`.
10
+ #
11
+ class HashWrapper < ::Hashie::Mash
12
+ disable_warnings if respond_to?(:disable_warnings)
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,144 @@
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
+ # @example Import the records into a different index/type than the default one
64
+ #
65
+ # Article.import index: 'my-new-index', type: 'my-other-type'
66
+ #
67
+ # @example Pass an ActiveRecord scope to limit the imported records
68
+ #
69
+ # Article.import scope: 'published'
70
+ #
71
+ # @example Transform records during the import with a lambda
72
+ #
73
+ # transform = lambda do |a|
74
+ # {index: {_id: a.id, _parent: a.author_id, data: a.__elasticsearch__.as_indexed_json}}
75
+ # end
76
+ #
77
+ # Article.import transform: transform
78
+ #
79
+ # @example Update the batch before yielding it
80
+ #
81
+ # class Article
82
+ # # ...
83
+ # def enrich(batch)
84
+ # batch.each do |item|
85
+ # item.metadata = MyAPI.get_metadata(item.id)
86
+ # end
87
+ # batch
88
+ # end
89
+ # end
90
+ #
91
+ # Article.import preprocess: enrich
92
+ #
93
+ # @example Return an array of error elements instead of the number of errors, eg.
94
+ # to try importing these records again
95
+ #
96
+ # Article.import return: 'errors'
97
+ #
98
+ def import(options={}, &block)
99
+ errors = []
100
+ refresh = options.delete(:refresh) || false
101
+ target_index = options.delete(:index) || index_name
102
+ target_type = options.delete(:type) || document_type
103
+ transform = options.delete(:transform) || __transform
104
+ return_value = options.delete(:return) || 'count'
105
+
106
+ unless transform.respond_to?(:call)
107
+ raise ArgumentError,
108
+ "Pass an object responding to `call` as the :transform option, #{transform.class} given"
109
+ end
110
+
111
+ if options.delete(:force)
112
+ self.create_index! force: true, index: target_index
113
+ end
114
+
115
+ __find_in_batches(options) do |batch|
116
+ response = client.bulk \
117
+ index: target_index,
118
+ type: target_type,
119
+ body: __batch_to_bulk(batch, transform)
120
+
121
+ yield response if block_given?
122
+
123
+ errors += response['items'].select { |k, v| k.values.first['error'] }
124
+ end
125
+
126
+ self.refresh_index! if refresh
127
+
128
+ case return_value
129
+ when 'errors'
130
+ errors
131
+ else
132
+ errors.size
133
+ end
134
+ end
135
+
136
+ def __batch_to_bulk(batch, transform)
137
+ batch.map { |model| transform.call(model) }
138
+ end
139
+ end
140
+
141
+ end
142
+
143
+ end
144
+ end