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