elastics 0.3.0 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: c5c6528a83756461e3f88a64c932e6f6cd4a572e
4
- data.tar.gz: 329c63ad74333cee9dc57581ca8ac626973dea3b
3
+ metadata.gz: ca7add0bf62591b992bdcbb03cf08461cf208204
4
+ data.tar.gz: 5f2b845c2cf49a89e6f4414dfad2dd505ed56784
5
5
  SHA512:
6
- metadata.gz: aba73c791bb368035b51ed5d93994fa27648c442f77ed7992749b5bb5ee5ee823a17e33d5449d0d8a1f293dd830ff01501405287ca1dc563df644c3bb3da68ba
7
- data.tar.gz: d687176c8013f6196d0ebc0340aaa6e194e5719b700fdbb688159ca2ca6ffda3694e144e0b55c064fcb63b629299288121b9dbd25ba61876318092089360cafa
6
+ metadata.gz: da3920ea1f157297b6c3e5fea2b7ead3620493fb887ee34bd6191b8428ab74763fb324246be193fbd5e2fc32268f7f61331680a02f087d6ce3c619d9d7da1820
7
+ data.tar.gz: 262e2e0efec270bed5eac991e706298220bf67f5024efd6ae99782d94efb86380c95bf40bcc9dc2779e423205d6c00441a9b72968921894d3bd5fb3869034cab
@@ -3,6 +3,8 @@ rvm:
3
3
  - 1.9.3
4
4
  - 2.0.0
5
5
  - 2.1.4
6
+ - jruby-19mode
7
+ - jruby-head
6
8
  services:
7
9
  - elasticsearch
8
10
  notifications:
data/Gemfile CHANGED
@@ -1,3 +1,9 @@
1
1
  source 'https://rubygems.org'
2
2
 
3
3
  gemspec
4
+
5
+ group :development do
6
+ gem 'pry', '~> 0.10.1'
7
+ gem 'pry-byebug', '~> 2.0.0', platforms: [:mri_20, :mri_21]
8
+ gem 'pry-coolline', '~> 0.2.5'
9
+ end
data/README.md CHANGED
@@ -32,8 +32,6 @@ client = Elastics::Client.new(options)
32
32
  # :host - hostname with port or array with hosts (default 127.0.0.1:9200)
33
33
  # :index - (default index)
34
34
  # :type - (default type)
35
- # :connect_timeout - timeout to mark the host as dead in cluster-mode (default 10)
36
- # :resurrect_timeout - timeout to mark dead host as alive in cluster-mode (default 10)
37
35
 
38
36
  # basic request
39
37
  client.request(params)
@@ -70,7 +68,21 @@ client.bulk(params) do |bulk|
70
68
  end
71
69
  ```
72
70
 
73
- When using cluster-mode you should also install `gem 'thread_safe'`.
71
+ ### Models
72
+ Most of ActiveRecord integration functionality is available for plain ruby.
73
+
74
+ ```ruby
75
+ class User
76
+ include Elastics::Model
77
+
78
+ # set connection config, check 'configure section' for available options
79
+ self.elastics_config = {host: 'hostname:port'}
80
+ # set index base and version manager will manage aliases
81
+ self.elastics_index_base = 'user'
82
+ self.elastics_type_name = 'user'
83
+ end
84
+ ```
85
+ Check out available [HelperMethods](https://github.com/printercu/elastics-rb/blob/master/lib/elastics/model/helper_methods.rb).
74
86
 
75
87
  ### ActiveRecord
76
88
 
@@ -91,8 +103,13 @@ User.elastics_params # hash with index & type values for the model
91
103
  User.request_elastics(params) # performs request merging params with elastics_params
92
104
  User.search_elastics(data)
93
105
  # Returns Elastics::ActiveRecord::SearchResult object with some useful methods
106
+
107
+ # Indexing on create/update can be skipped with skip_elastics
108
+ User.skip_elastics { users.each { |x| x.update_attributes(smth: 'not indexed') } }
109
+ user.skip_elastics { user.update_attributes(smth: 'not indexed') }
94
110
  ```
95
- Check out [HelperMethods](https://github.com/printercu/elastics-rb/blob/master/lib/elastics/active_record/helper_methods.rb)
111
+ Check out [Model::HelperMethods](https://github.com/printercu/elastics-rb/blob/master/lib/elastics/model/helper_methods.rb)
112
+ and [AR::HelperMethods](https://github.com/printercu/elastics-rb/blob/master/lib/elastics/active_record/helper_methods.rb)
96
113
  for more information.
97
114
 
98
115
  #### Configure
@@ -171,8 +188,7 @@ task :environment do
171
188
  end
172
189
  ```
173
190
 
174
- Also you need to install `active_support` & require
175
- `active_support/core_ext/object` to be able to run tasks.
191
+ Also you need to install `active_support` to be able to run tasks.
176
192
 
177
193
  ### Auto refresh index
178
194
  Add `Elastics::AutoRefresh.enable!` to your test helper,
@@ -189,6 +205,48 @@ Elastics::AutoRefresh.disable! { Model.reindex_elastics }
189
205
  Model.refresh_elastics
190
206
  ```
191
207
 
208
+ ### Instrumentation
209
+ Instrumentation works out of box in rails. For pure ruby there is only
210
+ ActiveSupport version (remember to install gem).
211
+
212
+ ```ruby
213
+ # Activate instrumentation (no need for rails)
214
+ Elastics::Instrumentation::ActiveSupport.install
215
+
216
+ # Activate body prettifier (off by default)
217
+ Elastics::Instrumentation.body_prettifier = true
218
+ # can be
219
+ # true - JSON.pretty_generate
220
+ # :ap - awesome_print
221
+ # :pp - pretty_print
222
+ # Proc - ->(str) { your_prettifier(str) }
223
+ ```
224
+
225
+ ### Connecting to cluster
226
+ When you pass array in `:host` option to initializer, client will work in cluster mode.
227
+ There are some options for this mode:
228
+ - `:connect_timeout` - timeout to mark the host as dead in cluster-mode (default 10)
229
+ - `:resurrect_timeout` - timeout to mark dead host as alive in cluster-mode (default 60)
230
+ - `:discover` - enable nodes discovering
231
+
232
+ In plain ruby you should also install `thread_safe` gem.
233
+
234
+ ##### Note about nodes discovering
235
+ Client will perform requests to discover nodes with enabled http module. After
236
+ discovering hosts will be overwritten with discovered ones.
237
+
238
+ `discover: true` will keep you from editing config too frequently,
239
+ but keep in mind that you should set enough hosts explicitly, so that discover
240
+ will be able to continue if some of hosts are down.
241
+ Also this is performed automaticaly only once, when client is initialized.
242
+ It will not track nodes that go online after client was instantiated. Anyway you
243
+ still can call `.discover_cluster` whenever you want, or just restart app when
244
+ you add more nodes.
245
+
246
+ ### Thread-safety
247
+ Elastics designed to be thread-safe. It should be ok to have single client instance
248
+ for the whole application.
249
+
192
250
  ### Use with capistrano
193
251
  Add following lines to your `deploy.rb` and all rake tasks will be available in cap.
194
252
 
@@ -4,21 +4,13 @@ module Elastics
4
4
 
5
5
  require 'elastics/client'
6
6
 
7
- autoload :AutoRefresh, 'elastics/auto_refresh'
8
- autoload :Tasks, 'elastics/tasks'
9
- autoload :QueryHelper, 'elastics/query_helper'
10
- autoload :Result, 'elastics/result'
11
- autoload :VersionManager, 'elastics/version_manager'
12
-
13
- class << self
14
- attr_reader :models
15
-
16
- def reset_models
17
- @models = []
18
- end
19
- end
20
-
21
- reset_models
7
+ autoload :AutoRefresh, 'elastics/auto_refresh'
8
+ autoload :Instrumentation, 'elastics/instrumentation'
9
+ autoload :Model, 'elastics/model'
10
+ autoload :QueryHelper, 'elastics/query_helper'
11
+ autoload :Result, 'elastics/result'
12
+ autoload :Tasks, 'elastics/tasks'
13
+ autoload :VersionManager, 'elastics/version_manager'
22
14
  end
23
15
 
24
16
  require 'elastics/railtie' if defined?(Rails)
@@ -5,33 +5,21 @@ module Elastics
5
5
  autoload :SearchResult
6
6
  autoload :ModelSchema
7
7
  autoload :HelperMethods
8
- autoload :Instrumentation
9
- autoload :LogSubscriber, 'elastics/active_record/instrumentation'
10
8
 
11
9
  class << self
12
10
  def install
13
11
  ::ActiveRecord::Base.extend self
14
- Instrumentation.install
12
+ Instrumentation::ActiveSupport.install
15
13
  end
16
14
  end
17
15
 
16
+ include Model::Connection
17
+
18
18
  def elastics_config
19
19
  @elastics_config ||= connection_config[:elastics].try!(:with_indifferent_access) ||
20
20
  raise('No elastics configuration in database.yml')
21
21
  end
22
22
 
23
- def elastics
24
- @elastics ||= Client.new elastics_config.slice(:host, :port)
25
- end
26
-
27
- # Don't memoize to GC it after initialization
28
- def elastics_version_manager
29
- VersionManager.new(elastics, elastics_config.slice(
30
- :service_index,
31
- :index_prefix,
32
- ))
33
- end
34
-
35
23
  def indexed_with_elastics(options = {})
36
24
  options = {
37
25
  hooks: [:update, :destroy],
@@ -39,13 +27,22 @@ module Elastics
39
27
 
40
28
  extend ModelSchema
41
29
  include HelperMethods
30
+ extend Model::Tracking
42
31
 
43
32
  self.elastics_index_base = options[:index] if options[:index]
44
33
  self.elastics_type_name = options[:type] if options[:type]
45
34
 
46
- hooks = options[:hooks]
47
- after_commit :index_elastics, on: [:create, :update] if hooks.include?(:update)
48
- after_commit :delete_elastics, on: [:destroy] if hooks.include?(:destroy)
35
+ install_elastics_hooks(options[:hooks])
49
36
  end
37
+
38
+ private
39
+ def install_elastics_hooks(hooks)
40
+ if hooks.include?(:update)
41
+ after_commit :index_elastics, on: [:create, :update], unless: :skip_elastics?
42
+ end
43
+ if hooks.include?(:destroy)
44
+ after_commit :delete_elastics, on: [:destroy], unless: :skip_elastics?
45
+ end
46
+ end
50
47
  end
51
48
  end
@@ -3,27 +3,13 @@ module Elastics
3
3
  module HelperMethods
4
4
  extend ActiveSupport::Concern
5
5
 
6
- included do
7
- alias_method :to_elastics, :as_json unless instance_methods.include?(:to_elastics)
6
+ def self.append_features(base)
7
+ base.send :include, Model::HelperMethods
8
+ base.send :include, Model::Skipping
9
+ super
8
10
  end
9
11
 
10
12
  module ClassMethods
11
- def elastics_params
12
- {
13
- index: elastics_index_name,
14
- type: elastics_type_name,
15
- model: self,
16
- }
17
- end
18
-
19
- def request_elastics(params)
20
- elastics.request(elastics_params.merge!(params))
21
- end
22
-
23
- def bulk_elastics(params = {}, &block)
24
- elastics.bulk(elastics_params.merge!(params), &block)
25
- end
26
-
27
13
  def search_elastics(data = {}, options = {})
28
14
  request = {
29
15
  id: :_search,
@@ -46,11 +32,7 @@ module Elastics
46
32
 
47
33
  def index_all_elastics(*args)
48
34
  find_in_batches(*args) do |batch|
49
- bulk_elastics do |bulk|
50
- batch.each do |record|
51
- bulk.index record.id, record.to_elastics
52
- end
53
- end
35
+ index_batch_elastics(batch)
54
36
  end
55
37
  end
56
38
 
@@ -71,27 +53,6 @@ module Elastics
71
53
  end
72
54
  scope.index_all_elastics(options)
73
55
  end
74
-
75
- def refresh_elastics
76
- request_elastics(method: :post, type: nil, id: :_refresh)
77
- end
78
- end
79
-
80
- def index_elastics
81
- self.class.request_elastics(method: :post, id: id, body: to_elastics)
82
- end
83
-
84
- def update_elastics(data)
85
- self.class.request_elastics(method: :post, id: "#{id}/_update", body: data)
86
- end
87
-
88
- def update_elastics_doc(fields)
89
- update_elastics(doc: fields)
90
- end
91
-
92
- def delete_elastics
93
- self.class.request_elastics(method: :delete, id: id)
94
- rescue NotFound
95
56
  end
96
57
  end
97
58
  end
@@ -1,22 +1,7 @@
1
1
  module Elastics
2
2
  module ActiveRecord
3
3
  module ModelSchema
4
- class << self
5
- def track_model(model)
6
- Elastics.models << model unless model.abstract_class?
7
- end
8
-
9
- def extended(base)
10
- track_model(base)
11
- end
12
- end
13
-
14
- attr_writer :elastics_index_base, :elastics_type_name
15
-
16
- def elastics_index_name
17
- reset_elastics_index_name unless defined?(@elastics_index_name)
18
- @elastics_index_name
19
- end
4
+ include Model::Schema
20
5
 
21
6
  def elastics_type_name
22
7
  @elastics_type_name ||= model_name.to_s.demodulize.underscore.singularize
@@ -28,16 +13,8 @@ module Elastics
28
13
  end
29
14
  end
30
15
 
31
- def compute_elastics_index_name
32
- elastics_version_manager.index_name(elastics_index_base)
33
- end
34
-
35
- def elastics_index_base
36
- @elastics_index_base || elastics_config[:index] || elastics_type_name
37
- end
38
-
39
- def inherited(base)
40
- super.tap { ::Elastics::ActiveRecord::ModelSchema.track_model(base) }
16
+ def track_elastics_model?
17
+ !abstract_class?
41
18
  end
42
19
  end
43
20
  end
@@ -14,15 +14,15 @@ module Elastics
14
14
  attr_reader :client
15
15
 
16
16
  def initialize(defaults = {})
17
+ @index = defaults[:index]
18
+ @type = defaults[:type]
19
+ @client = HTTPClient.new
17
20
  if defaults[:host].is_a?(Array)
18
21
  extend Cluster
19
22
  initialize_cluster(defaults)
20
23
  else
21
24
  @host = defaults[:host] || '127.0.0.1:9200'
22
25
  end
23
- @index = defaults[:index]
24
- @type = defaults[:type]
25
- @client = HTTPClient.new
26
26
  end
27
27
 
28
28
  def debug!(dev = STDOUT)
@@ -133,7 +133,7 @@ module Elastics
133
133
  # You probably don't want to use this method directly.
134
134
  def http_request(method, path, query, body, params = nil, host = @host)
135
135
  uri = "http://#{host}#{path}"
136
- @client.request(method, uri, query, body, HEADERS)
136
+ client.request(method, uri, query, body, HEADERS)
137
137
  end
138
138
  end
139
139
  end
@@ -7,14 +7,27 @@ module Elastics
7
7
  module Cluster
8
8
  class NoAliveHosts < Error; end
9
9
 
10
+ def discover_cluster
11
+ # `nothing` allows not to fetch all unnecessary data
12
+ discovered = request(index: '_nodes', type: '_all', id: 'nothing')['nodes'].
13
+ map { |id, node|
14
+ match = node['http_address'].match(/inet\[.*?\/([\da-f.:]+)/i)
15
+ match && match[1]
16
+ }.compact
17
+ @cluster_mutex.synchronize do
18
+ @hosts.clear.concat(discovered - @dead_hosts.keys)
19
+ end
20
+ end
21
+
10
22
  private
11
23
  def initialize_cluster(defaults)
12
24
  @hosts = ThreadSafe::Array.new defaults[:host]
13
25
  @dead_hosts = ThreadSafe::Hash.new
14
26
  @connect_timeout = defaults[:connect_timeout] || 10
15
- @resurrect_timeout = defaults[:resurrect_timeout] || 10
27
+ @resurrect_timeout = defaults[:resurrect_timeout] || 60
16
28
  @current_host_n = 0
17
29
  @cluster_mutex = Mutex.new
30
+ discover_cluster if defaults[:discover]
18
31
  end
19
32
 
20
33
  def http_request(method, path, query, body, params = nil)
@@ -27,44 +40,46 @@ module Elastics
27
40
  retry
28
41
  end
29
42
 
30
- # TODO: check Enumerable#cycle for thread-safety and use it if possible
31
- def next_cluster_host
32
- if @resurrect_at
33
- time = Time.now.to_i
34
- resurrect_cluster(time) if @resurrect_at <= time
35
- end
36
- host_n = @current_host_n
37
- loop do
38
- host = @hosts[host_n]
39
- if !host
40
- raise NoAliveHosts if host_n == 0
41
- host_n = 0
42
- else
43
- @current_host_n = host_n + 1
44
- return host
43
+ # Very simple implementation. It may skip some hosts on current cycle
44
+ # when other is marked as dead. This should not be a problem.
45
+ # TODO: check Enumerable#cycle for thread-safety and use it if possible
46
+ def next_cluster_host
47
+ if @resurrect_at
48
+ time = Time.now.to_i
49
+ resurrect_cluster(time) if @resurrect_at <= time
50
+ end
51
+ host_n = @current_host_n
52
+ loop do
53
+ host = @hosts[host_n]
54
+ if !host
55
+ raise NoAliveHosts if host_n == 0
56
+ host_n = 0
57
+ else
58
+ @current_host_n = host_n + 1
59
+ return host
60
+ end
45
61
  end
46
62
  end
47
- end
48
63
 
49
- def resurrect_cluster(time = Time.now.to_i)
50
- @cluster_mutex.synchronize do
51
- @dead_hosts.delete_if do |host, resurrect_at|
52
- # skip the rest because values are sorted
53
- if time < resurrect_at
54
- @resurrect_at = resurrect_at
55
- break
64
+ def resurrect_cluster(time = Time.now.to_i)
65
+ @cluster_mutex.synchronize do
66
+ @dead_hosts.delete_if do |host, resurrect_at|
67
+ # skip the rest because values are sorted
68
+ if time < resurrect_at
69
+ @resurrect_at = resurrect_at
70
+ break
71
+ end
72
+ @hosts << host
56
73
  end
57
- @hosts << host
58
74
  end
59
75
  end
60
- end
61
76
 
62
- def add_dead_host(host, resurrect_at = nil)
63
- resurrect_at ||= Time.now.to_i + @resurrect_timeout
64
- @hosts.delete(host)
65
- @dead_hosts[host] = resurrect_at
66
- @resurrect_at ||= resurrect_at
67
- end
77
+ def add_dead_host(host, resurrect_at = nil)
78
+ resurrect_at ||= Time.now.to_i + @resurrect_timeout
79
+ @hosts.delete(host)
80
+ @dead_hosts[host] = resurrect_at
81
+ @resurrect_at ||= resurrect_at
82
+ end
68
83
  end
69
84
  end
70
85
  end
@@ -0,0 +1,38 @@
1
+ module Elastics
2
+ module Instrumentation
3
+ autoload :ActiveSupport, 'elastics/instrumentation/active_support'
4
+
5
+ PRETTIFIERS = {
6
+ ap: ->(str) { prettify_json(str, &:awesome_inspect) },
7
+ pp: ->(str) { prettify_json(str, &:pretty_inspect) },
8
+ true => ->(str) { prettify_json(str, &JSON.method(:pretty_generate)) },
9
+ }
10
+
11
+ class << self
12
+ def body_prettifier=(value)
13
+ @body_prettifier = case value
14
+ when Proc, nil, false then value
15
+ else PRETTIFIERS[value] or raise 'Invalid prettifier'
16
+ end
17
+ end
18
+
19
+ def prettify_body(str)
20
+ if @body_prettifier
21
+ @body_prettifier.call(str)
22
+ else
23
+ str
24
+ end
25
+ end
26
+
27
+ def prettify_json(str, &block)
28
+ data = [JSON.parse(str)] rescue nil
29
+ data ||= str.split("\n").map { |x| JSON.parse(x) } rescue nil
30
+ if data
31
+ data.map(&block).join("\n")
32
+ else
33
+ str
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,70 @@
1
+ require 'active_support/core_ext/object/blank'
2
+ require 'active_support/log_subscriber'
3
+ require 'active_support/notifications'
4
+
5
+ module Elastics
6
+ module Instrumentation
7
+ module ActiveSupport
8
+ class << self
9
+ def install
10
+ if Client.respond_to?(:prepend)
11
+ Client.prepend self
12
+ else
13
+ Client.send :include, Ruby19Fallback
14
+ end
15
+ LogSubscriber.attach_to :elastics
16
+ end
17
+ end
18
+
19
+ def http_request(*args)
20
+ ::ActiveSupport::Notifications.instrument 'http_request.elastics', args: args do
21
+ super
22
+ end
23
+ end
24
+
25
+ # old rubies support
26
+ module Ruby19Fallback
27
+ def self.included(base)
28
+ base.alias_method_chain :http_request, :as_instrumentation
29
+ end
30
+
31
+ def http_request_with_as_instrumentation(*args)
32
+ ::ActiveSupport::Notifications.instrument 'http_request.elastics', args: args do
33
+ http_request_without_as_instrumentation(*args)
34
+ end
35
+ end
36
+ end
37
+
38
+ class LogSubscriber < ::ActiveSupport::LogSubscriber
39
+ def http_request(event)
40
+ return unless logger.debug?
41
+
42
+ payload = event.payload[:args]
43
+ method, path, query, body, params = payload
44
+ path = '/' if path.blank?
45
+ path << "?#{query.to_param}" if query.present?
46
+ model = params[:model]
47
+
48
+ name = ""
49
+ name << "#{model.name} " if model
50
+ name << "elastics (#{event.duration.round(1)}ms)"
51
+ request = "#{method.to_s.upcase} #{path}"
52
+ request << " #{Instrumentation.prettify_body(body)}" if body.present?
53
+
54
+ if odd?
55
+ name = color(name, ::ActiveSupport::LogSubscriber::CYAN, true)
56
+ request = color(request, nil, true)
57
+ else
58
+ name = color(name, ::ActiveSupport::LogSubscriber::MAGENTA, true)
59
+ end
60
+
61
+ debug " #{name} #{request}"
62
+ end
63
+
64
+ def odd?
65
+ @odd = !@odd
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,18 @@
1
+ module Elastics
2
+ module Model
3
+ autoload :Connection, 'elastics/model/connection'
4
+ autoload :HelperMethods, 'elastics/model/helper_methods'
5
+ autoload :Schema, 'elastics/model/schema'
6
+ autoload :Skipping, 'elastics/model/skipping'
7
+
8
+ require 'elastics/model/tracking'
9
+
10
+ def self.included(base)
11
+ base.extend Connection
12
+ base.extend Schema
13
+ base.extend Tracking
14
+ base.send :include, HelperMethods
15
+ base.send :include, Skipping
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,19 @@
1
+ module Elastics
2
+ module Model
3
+ module Connection
4
+ attr_accessor :elastics_config
5
+
6
+ def elastics
7
+ @elastics ||= Client.new elastics_config.slice(:host)
8
+ end
9
+
10
+ # Don't memoize to GC it after initialization
11
+ def elastics_version_manager
12
+ VersionManager.new(elastics, elastics_config.slice(
13
+ :service_index,
14
+ :index_prefix,
15
+ ))
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,67 @@
1
+ module Elastics
2
+ module Model
3
+ module HelperMethods
4
+ def self.included(base)
5
+ base.class_eval do
6
+ extend ClassMethods
7
+
8
+ # don't override to_elastics method, if it already exists
9
+ if !instance_methods.include?(:to_elastics) && instance_methods.include?(:as_json)
10
+ alias_method :to_elastics, :as_json
11
+ end
12
+ end
13
+ end
14
+
15
+ module ClassMethods
16
+ def elastics_params
17
+ {
18
+ index: elastics_index_name,
19
+ type: elastics_type_name,
20
+ model: self,
21
+ }
22
+ end
23
+
24
+ def request_elastics(params)
25
+ elastics.request(elastics_params.merge!(params))
26
+ end
27
+
28
+ def bulk_elastics(params = {}, &block)
29
+ elastics.bulk(elastics_params.merge!(params), &block)
30
+ end
31
+
32
+ def refresh_elastics
33
+ request_elastics(method: :post, type: nil, id: :_refresh)
34
+ end
35
+
36
+ def index_batch_elastics(batch)
37
+ bulk_elastics do |bulk|
38
+ batch.each do |record|
39
+ bulk.index record.id, record.to_elastics
40
+ end
41
+ end
42
+ end
43
+
44
+ def reindex_elastics(options = {})
45
+ raise 'Not implemented'
46
+ end
47
+ end
48
+
49
+ def index_elastics
50
+ self.class.request_elastics(method: :post, id: id, body: to_elastics)
51
+ end
52
+
53
+ def update_elastics(data)
54
+ self.class.request_elastics(method: :post, id: "#{id}/_update", body: data)
55
+ end
56
+
57
+ def update_elastics_doc(fields)
58
+ update_elastics(doc: fields)
59
+ end
60
+
61
+ def delete_elastics
62
+ self.class.request_elastics(method: :delete, id: id)
63
+ rescue NotFound
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,32 @@
1
+ module Elastics
2
+ module Model
3
+ module Schema
4
+ attr_writer :elastics_index_base, :elastics_type_name
5
+
6
+ def elastics_index_name
7
+ reset_elastics_index_name unless defined?(@elastics_index_name)
8
+ @elastics_index_name
9
+ end
10
+
11
+ def elastics_type_name
12
+ @elastics_type_name ||= name.split('::').last.downcase
13
+ end
14
+
15
+ def reset_elastics_index_name
16
+ @elastics_index_name = if superclass.respond_to?(:elastics_index_name)
17
+ superclass.elastics_index_name
18
+ else
19
+ compute_elastics_index_name
20
+ end
21
+ end
22
+
23
+ def compute_elastics_index_name
24
+ elastics_version_manager.index_name(elastics_index_base)
25
+ end
26
+
27
+ def elastics_index_base
28
+ @elastics_index_base || elastics_config[:index] || elastics_type_name
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,42 @@
1
+ module Elastics
2
+ module Model
3
+ module Skipping
4
+ class << self
5
+ def models
6
+ Thread.current[:elastics_skip_models] ||= {}
7
+ end
8
+
9
+ def included(base)
10
+ base.extend self
11
+ base.extend ClassMethods
12
+ base.send :include, InstanceMethods
13
+ end
14
+ end
15
+
16
+ def skip_elastics
17
+ self.skip_elastics = true
18
+ yield
19
+ ensure
20
+ self.skip_elastics = false
21
+ end
22
+
23
+ module ClassMethods
24
+ def skip_elastics?
25
+ Skipping.models[self]
26
+ end
27
+
28
+ def skip_elastics=(val)
29
+ Skipping.models[self] = val
30
+ end
31
+ end
32
+
33
+ module InstanceMethods
34
+ attr_writer :skip_elastics
35
+
36
+ def skip_elastics?
37
+ @skip_elastics || self.class.skip_elastics?
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,29 @@
1
+ module Elastics
2
+ module Model
3
+ class << self
4
+ attr_reader :list
5
+
6
+ def reset_list
7
+ @list = []
8
+ end
9
+
10
+ def track(model)
11
+ if !model.respond_to?(:track_elastics_model?) || model.track_elastics_model?
12
+ list << model
13
+ end
14
+ end
15
+ end
16
+
17
+ reset_list
18
+
19
+ module Tracking
20
+ def self.extended(base)
21
+ Model.track(base)
22
+ end
23
+
24
+ def inherited(base)
25
+ super.tap { Model.track(base) }
26
+ end
27
+ end
28
+ end
29
+ end
@@ -11,7 +11,7 @@ module Elastics
11
11
  end
12
12
 
13
13
  config.to_prepare do
14
- Elastics.reset_models
14
+ Model.reset_list
15
15
  end
16
16
  end
17
17
  end
@@ -10,7 +10,7 @@ module Elastics
10
10
  end
11
11
 
12
12
  def client
13
- @client ||= Client.new config.slice(:host, :port)
13
+ @client ||= Client.new config.slice(:host)
14
14
  end
15
15
 
16
16
  def client=(val)
@@ -41,7 +41,7 @@ module Elastics
41
41
  def models_to_reindex(options = {})
42
42
  indices = options[:indices].try!(:map, &:to_s)
43
43
  types = options[:types].try!(:map, &:to_s)
44
- models = Elastics.models.select do |model|
44
+ models = Model.list.select do |model|
45
45
  next if indices && !indices.include?(model.elastics_index_base)
46
46
  next if types && !types.include?(model.elastics_type_name)
47
47
  true
@@ -1,5 +1,5 @@
1
1
  module Elastics
2
- VERSION = '0.3.0'
2
+ VERSION = '0.4.0'
3
3
 
4
4
  def self.gem_version
5
5
  Gem::Version.new VERSION
@@ -8,7 +8,7 @@ module Elastics
8
8
 
9
9
  def default_version=(version)
10
10
  @default_version = version
11
- Elastics.models.each &:reset_elastics_index_name
11
+ Model.list.each &:reset_elastics_index_name
12
12
  end
13
13
 
14
14
  def use_version(version)
@@ -21,14 +21,14 @@ describe Elastics::AutoRefresh do
21
21
  it 'invokes refresh after each request' do
22
22
  expect(client).to receive(:refresh).exactly(3).times
23
23
  client.post id: 1
24
- client.put_mapping type: {fields: {}}
24
+ client.put_mapping type: :test, body: {fields: {}}
25
25
  client.delete id: 2
26
26
  end
27
27
 
28
28
  it 'invokes refresh after each request, not wrapped in .disable! block' do
29
29
  expect(client).to receive(:refresh).exactly(2).times
30
30
  client.post id: 1
31
- described_class.disable! { client.put_mapping type: {fields: {}} }
31
+ described_class.disable! { client.put_mapping type: :test, body: {fields: {}} }
32
32
  client.delete id: 2
33
33
  end
34
34
  end
@@ -105,4 +105,63 @@ describe Elastics::Client do
105
105
  it { should raise_error(Elastics::Client::Cluster::NoAliveHosts) }
106
106
  end
107
107
  end
108
+
109
+ describe '#discover_cluster' do
110
+ let(:instance) { described_class.new(host: initial_host, discover: true).tap { |x|
111
+ x.singleton_class.send :attr_reader, :hosts
112
+ } }
113
+
114
+ let(:initial_host) { ['10.0.0.1:9200', '10.0.0.2:9200', '10.0.0.3:9200']}
115
+
116
+ let(:successful_response) do
117
+ {"cluster_name"=>"elasticsearch_max",
118
+ "nodes"=>
119
+ {"one"=>{"http_address"=>"inet[/10.0.0.2:9200]"},
120
+ "other"=>{"http_address"=>"inet[/10.0.0.1:9200]"}}}
121
+ end
122
+
123
+ context 'when nodes discover request fails' do
124
+ before do
125
+ expect_any_instance_of(described_class).to receive(:request) { raise Elastics::Error }
126
+ end
127
+
128
+ it 'fails on initialization' do
129
+ expect { instance }.to raise_error(Elastics::Error)
130
+ end
131
+ end
132
+
133
+ context 'when request is performed on first node' do
134
+ before do
135
+ expect_any_instance_of(described_class).to receive(:request) { successful_response }
136
+ end
137
+
138
+ it 'overwrites settings' do
139
+ expect(instance.hosts).to eq ['10.0.0.2:9200', '10.0.0.1:9200']
140
+ end
141
+ end
142
+
143
+ context 'when first is not available' do
144
+ before do
145
+ client = Object.new
146
+
147
+ expect(client).to receive(:request).
148
+ with(:get, "http://#{initial_host[0]}/_nodes/_all/nothing", nil, nil, Elastics::Client::HEADERS) do
149
+ raise Timeout::Error
150
+ end
151
+
152
+ expect(client).to receive(:request).
153
+ with(:get, "http://#{initial_host[2]}/_nodes/_all/nothing", nil, nil, Elastics::Client::HEADERS) do
154
+ OpenStruct.new(status: 200, body: successful_response.to_json)
155
+ end
156
+
157
+ allow_any_instance_of(described_class).to receive(:client) { client }
158
+ end
159
+
160
+ it 'keeps first node in @dead_hosts' do
161
+ expect(instance.hosts).to eq ['10.0.0.2:9200']
162
+ instance.send(:resurrect_cluster, 1.minute.from_now.to_i)
163
+ expect(instance.hosts).to eq ['10.0.0.2:9200', '10.0.0.1:9200']
164
+ end
165
+ end
166
+ end
108
167
  end
@@ -0,0 +1,71 @@
1
+ require 'spec_helper'
2
+
3
+ describe Elastics::Instrumentation do
4
+ describe '.prettify_json' do
5
+ context 'when string is json' do
6
+ it 'parses it and yields hash into block' do
7
+ result = described_class.prettify_json('{"a":1}') { |x| x.keys[0] }
8
+ expect(result).to eq 'a'
9
+ end
10
+ end
11
+
12
+ context 'when string is multiple jsons joined with new line (bulk request)' do
13
+ it 'parses it into array and yields hashes into block one by one' do
14
+ result = described_class.prettify_json("{\"a\":1}\n{\"b\":2}") do |x|
15
+ x.keys[0]
16
+ end
17
+ expect(result).to eq "a\nb"
18
+ end
19
+ end
20
+
21
+ context 'when string is not json' do
22
+ it 'returns original string' do
23
+ [
24
+ "{\"a\":1",
25
+ "{\"a\":1}\n{b\":2}",
26
+ "{\"a\":1}\n{\"b\":2}\n{",
27
+ 'not json'
28
+ ].each do |str|
29
+ expect(described_class.prettify_json(str) { raise }).to eq(str)
30
+ end
31
+ end
32
+ end
33
+ end
34
+
35
+ describe '.prettify_body' do
36
+ context 'when body_prettifier is true' do
37
+ before { described_class.body_prettifier = true }
38
+
39
+ it 'uses JSON.pretty_generate to prettify' do
40
+ expect(JSON).to receive(:pretty_generate).with('a' => 1).and_return(1)
41
+ expect(JSON).to receive(:pretty_generate).with('b' => 2).and_return(2)
42
+ expect(described_class.prettify_body("{\"a\":1}\n{\"b\":2}")).to eq "1\n2"
43
+ end
44
+ end
45
+ end
46
+
47
+ describe '.body_prettifier=' do
48
+ context 'when new value is false/nil, Proc, or valid prettifier id' do
49
+ it 'accepts it' do
50
+ [
51
+ :ap,
52
+ true,
53
+ ->(str) { str * 2 },
54
+ false,
55
+ ].each { |value| described_class.body_prettifier = value }
56
+ end
57
+ end
58
+
59
+ context 'when new value is invalid' do
60
+ it 'raises error' do
61
+ [
62
+ 1,
63
+ :invalid,
64
+ Object.new,
65
+ ].each do |value|
66
+ expect { described_class.body_prettifier = value }.to raise_error
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,24 @@
1
+ require 'spec_helper'
2
+
3
+ describe Elastics::Model::Schema do
4
+ let(:model) { Class.new.tap { |x| x.extend described_class } }
5
+
6
+ describe '#elastics_type_name' do
7
+ subject { model.elastics_type_name }
8
+
9
+ context 'when type was manually set' do
10
+ before { model.elastics_type_name = :test }
11
+ it { should be :test }
12
+ end
13
+
14
+ context 'when class name does not include modules' do
15
+ before { expect(model).to receive(:name) { 'Test' } }
16
+ it { should eq 'test' }
17
+ end
18
+
19
+ context 'when class name includes modules' do
20
+ before { expect(model).to receive(:name) { 'Module::Other::Index' } }
21
+ it { should eq 'index' }
22
+ end
23
+ end
24
+ end
@@ -1,5 +1,6 @@
1
1
  require 'bundler/setup'
2
2
  Bundler.setup
3
+ require 'pry'
3
4
 
4
5
  require 'elastics'
5
6
  require 'active_support/all'
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: elastics
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Max Melentiev
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-11-11 00:00:00.000000000 Z
11
+ date: 2014-12-31 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: httpclient
@@ -111,7 +111,6 @@ files:
111
111
  - lib/elastics.rb
112
112
  - lib/elastics/active_record.rb
113
113
  - lib/elastics/active_record/helper_methods.rb
114
- - lib/elastics/active_record/instrumentation.rb
115
114
  - lib/elastics/active_record/model_schema.rb
116
115
  - lib/elastics/active_record/search_result.rb
117
116
  - lib/elastics/active_record/tasks_config.rb
@@ -120,6 +119,14 @@ files:
120
119
  - lib/elastics/client.rb
121
120
  - lib/elastics/client/bulk.rb
122
121
  - lib/elastics/client/cluster.rb
122
+ - lib/elastics/instrumentation.rb
123
+ - lib/elastics/instrumentation/active_support.rb
124
+ - lib/elastics/model.rb
125
+ - lib/elastics/model/connection.rb
126
+ - lib/elastics/model/helper_methods.rb
127
+ - lib/elastics/model/schema.rb
128
+ - lib/elastics/model/skipping.rb
129
+ - lib/elastics/model/tracking.rb
123
130
  - lib/elastics/query_helper.rb
124
131
  - lib/elastics/railtie.rb
125
132
  - lib/elastics/result.rb
@@ -136,6 +143,8 @@ files:
136
143
  - spec/lib/elastics/client/bulk_spec.rb
137
144
  - spec/lib/elastics/client/cluster_spec.rb
138
145
  - spec/lib/elastics/client_spec.rb
146
+ - spec/lib/elastics/instrumentation_spec.rb
147
+ - spec/lib/elastics/model/schema_spec.rb
139
148
  - spec/spec_helper.rb
140
149
  homepage: http://github.com/printercu/elastics-rb
141
150
  licenses:
@@ -166,4 +175,7 @@ test_files:
166
175
  - spec/lib/elastics/client/bulk_spec.rb
167
176
  - spec/lib/elastics/client/cluster_spec.rb
168
177
  - spec/lib/elastics/client_spec.rb
178
+ - spec/lib/elastics/instrumentation_spec.rb
179
+ - spec/lib/elastics/model/schema_spec.rb
169
180
  - spec/spec_helper.rb
181
+ has_rdoc:
@@ -1,71 +0,0 @@
1
- module Elastics
2
- module ActiveRecord
3
- # To be included in `Elastics::Client`
4
- module Instrumentation
5
- class << self
6
- def install
7
- if Client.respond_to?(:prepend)
8
- Client.prepend self
9
- else
10
- Client.send :include, Fallback
11
- end
12
- unless ::ActiveRecord::LogSubscriber < LogSubscriber
13
- ::ActiveRecord::LogSubscriber.send :include, LogSubscriber
14
- end
15
- end
16
- end
17
-
18
- def http_request(*args)
19
- ActiveSupport::Notifications.instrument 'request_elastics.active_record', args: args do
20
- super
21
- end
22
- end
23
-
24
- # old rubies support
25
- module Fallback
26
- extend ActiveSupport::Concern
27
-
28
- included do
29
- alias_method_chain :http_request, :instrumentation
30
- end
31
-
32
- def http_request_with_instrumentation(*args)
33
- ActiveSupport::Notifications.instrument 'request_elastics.active_record', args: args do
34
- http_request_without_instrumentation(*args)
35
- end
36
- end
37
- end
38
- end
39
-
40
- module LogSubscriber
41
- def self.included(base)
42
- instance_methods.each { |method| base.method_added(method) }
43
- end
44
-
45
- def request_elastics(event)
46
- return unless logger.debug?
47
-
48
- payload = event.payload[:args]
49
- method, path, query, body, params = payload
50
- path = '/' if path.blank?
51
- path << "?#{query.to_param}" if query.present?
52
- model = params[:model]
53
-
54
- name = ""
55
- name << "#{model.name} " if model
56
- name << "elastics (#{event.duration.round(1)}ms)"
57
- request = "#{method.to_s.upcase} #{path}"
58
- request << " #{body}" if body.present?
59
-
60
- if odd?
61
- name = color(name, ActiveSupport::LogSubscriber::CYAN, true)
62
- request = color(request, nil, true)
63
- else
64
- name = color(name, ActiveSupport::LogSubscriber::MAGENTA, true)
65
- end
66
-
67
- debug " #{name} #{request}"
68
- end
69
- end
70
- end
71
- end