elasticsearch-model-queryable 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +20 -0
- data/CHANGELOG.md +26 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +13 -0
- data/README.md +695 -0
- data/Rakefile +59 -0
- data/elasticsearch-model.gemspec +57 -0
- data/examples/activerecord_article.rb +77 -0
- data/examples/activerecord_associations.rb +162 -0
- data/examples/couchbase_article.rb +66 -0
- data/examples/datamapper_article.rb +71 -0
- data/examples/mongoid_article.rb +68 -0
- data/examples/ohm_article.rb +70 -0
- data/examples/riak_article.rb +52 -0
- data/gemfiles/3.0.gemfile +12 -0
- data/gemfiles/4.0.gemfile +11 -0
- data/lib/elasticsearch/model/adapter.rb +145 -0
- data/lib/elasticsearch/model/adapters/active_record.rb +104 -0
- data/lib/elasticsearch/model/adapters/default.rb +50 -0
- data/lib/elasticsearch/model/adapters/mongoid.rb +92 -0
- data/lib/elasticsearch/model/callbacks.rb +35 -0
- data/lib/elasticsearch/model/client.rb +61 -0
- data/lib/elasticsearch/model/ext/active_record.rb +14 -0
- data/lib/elasticsearch/model/hash_wrapper.rb +15 -0
- data/lib/elasticsearch/model/importing.rb +144 -0
- data/lib/elasticsearch/model/indexing.rb +472 -0
- data/lib/elasticsearch/model/naming.rb +101 -0
- data/lib/elasticsearch/model/proxy.rb +127 -0
- data/lib/elasticsearch/model/response/base.rb +44 -0
- data/lib/elasticsearch/model/response/pagination.rb +173 -0
- data/lib/elasticsearch/model/response/records.rb +69 -0
- data/lib/elasticsearch/model/response/result.rb +63 -0
- data/lib/elasticsearch/model/response/results.rb +31 -0
- data/lib/elasticsearch/model/response.rb +71 -0
- data/lib/elasticsearch/model/searching.rb +107 -0
- data/lib/elasticsearch/model/serializing.rb +35 -0
- data/lib/elasticsearch/model/version.rb +5 -0
- data/lib/elasticsearch/model.rb +157 -0
- data/test/integration/active_record_associations_parent_child.rb +139 -0
- data/test/integration/active_record_associations_test.rb +307 -0
- data/test/integration/active_record_basic_test.rb +179 -0
- data/test/integration/active_record_custom_serialization_test.rb +62 -0
- data/test/integration/active_record_import_test.rb +100 -0
- data/test/integration/active_record_namespaced_model_test.rb +49 -0
- data/test/integration/active_record_pagination_test.rb +132 -0
- data/test/integration/mongoid_basic_test.rb +193 -0
- data/test/test_helper.rb +63 -0
- data/test/unit/adapter_active_record_test.rb +140 -0
- data/test/unit/adapter_default_test.rb +41 -0
- data/test/unit/adapter_mongoid_test.rb +102 -0
- data/test/unit/adapter_test.rb +69 -0
- data/test/unit/callbacks_test.rb +31 -0
- data/test/unit/client_test.rb +27 -0
- data/test/unit/importing_test.rb +176 -0
- data/test/unit/indexing_test.rb +478 -0
- data/test/unit/module_test.rb +57 -0
- data/test/unit/naming_test.rb +76 -0
- data/test/unit/proxy_test.rb +89 -0
- data/test/unit/response_base_test.rb +40 -0
- data/test/unit/response_pagination_kaminari_test.rb +189 -0
- data/test/unit/response_pagination_will_paginate_test.rb +208 -0
- data/test/unit/response_records_test.rb +91 -0
- data/test/unit/response_result_test.rb +90 -0
- data/test/unit/response_results_test.rb +31 -0
- data/test/unit/response_test.rb +67 -0
- data/test/unit/searching_search_request_test.rb +78 -0
- data/test/unit/searching_test.rb +41 -0
- data/test/unit/serializing_test.rb +17 -0
- metadata +466 -0
@@ -0,0 +1,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
|