elasticsearch-model-queryable 0.1.5

Sign up to get free protection for your applications and to get access to all the features.
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