elastics 0.1.1 → 0.2.0

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.
@@ -0,0 +1,49 @@
1
+ module Elastics
2
+ module ActiveRecord
3
+ class SearchResult
4
+ attr_reader :result
5
+
6
+ def initialize(model, result, options = {})
7
+ @model = model
8
+ @result = result
9
+ @options = options
10
+ end
11
+
12
+ def hits
13
+ @hits ||= @result['hits'.freeze]
14
+ end
15
+
16
+ def ids
17
+ @ids ||= hits['hits'.freeze].map { |x| x['_id'.freeze].to_i }
18
+ end
19
+
20
+ def ids_to_find
21
+ @ids_to_find ||= begin
22
+ limit = @options[:limit]
23
+ limit ? ids[0...limit] : ids
24
+ end
25
+ end
26
+
27
+ def rest_ids
28
+ limit = @options[:limit]
29
+ limit ? ids[limit..-1] : []
30
+ end
31
+
32
+ def collection
33
+ @collection ||= @model.find_all_ordered ids_to_find
34
+ end
35
+
36
+ def relation
37
+ @model.where id: ids_to_find
38
+ end
39
+
40
+ def aggregations
41
+ @aggregations ||= @result['aggregations'.freeze]
42
+ end
43
+
44
+ def total
45
+ hits['total'.freeze]
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,25 @@
1
+ module Elastics
2
+ module ActiveRecord
3
+ module TasksConfig
4
+ def base_paths
5
+ @base_paths ||= if defined?(Rails)
6
+ [File.join(Rails.root, 'db', 'elastics')]
7
+ else
8
+ super
9
+ end
10
+ end
11
+
12
+ def client
13
+ @client ||= ::ActiveRecord::Base.elastics
14
+ end
15
+
16
+ def version_manager
17
+ @version_manager ||= ::ActiveRecord::Base.elastics_version_manager
18
+ end
19
+
20
+ def config
21
+ @config ||= ::ActiveRecord::Base.elastics_config
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,14 @@
1
+ Capistrano::Configuration.instance.load do
2
+ namespace :elastics do
3
+ %w(create drop migrate reindex).each do |method|
4
+ desc "rake elastics:#{method}"
5
+ task method, roles: :elastics, only: {primary: true} do
6
+ bundle_cmd = fetch(:bundle_cmd, 'bundle')
7
+ env = fetch(:rack_env, fetch(:rails_env, 'production'))
8
+ run "cd #{current_path} && " \
9
+ "#{bundle_cmd} exec rake elastics:#{method}[#{ENV['INDICES']}] #{ENV['ES_OPTIONS']} " \
10
+ "RAILS_ENV=#{env}"
11
+ end
12
+ end
13
+ end
14
+ end
@@ -4,12 +4,18 @@ module Elastics
4
4
  class Client
5
5
  HEADERS = {'Content-Type' => 'application/json'}
6
6
 
7
+ autoload :Cluster, 'elastics/client/cluster'
8
+
7
9
  attr_writer :index, :type
8
10
  attr_reader :client
9
11
 
10
12
  def initialize(defaults = {})
11
- @host = defaults[:host] || '127.0.0.1'
12
- @port = defaults[:port] || 9200
13
+ if defaults[:host].is_a?(Array)
14
+ extend Cluster
15
+ initialize_cluster(defaults)
16
+ else
17
+ @host = defaults[:host] || '127.0.0.1:9200'
18
+ end
13
19
  @index = defaults[:index]
14
20
  @type = defaults[:type]
15
21
  @client = HTTPClient.new
@@ -28,45 +34,60 @@ module Elastics
28
34
  @type = type || nil
29
35
  end
30
36
 
31
- def uri(params)
32
- str = "http://#{@host}:#{@port}"
33
- if index = params[:index] || @index
34
- str += "/#{index}"
35
- type = params[:type] || @type
36
- str += "/#{type}" if type
37
+ def request_path(params)
38
+ str = ""
39
+ if index = params[:index] || @index
40
+ str << "/#{index}"
41
+ type = params[:type] || @type
42
+ str << "/#{type}" if type
37
43
  end
38
44
  path = params[:id]
39
- str += "/#{path}" if path
45
+ str << "/#{path}" if path
40
46
  str
41
47
  end
42
48
 
43
49
  def request(params)
44
- http_method = params[:method] || :get
50
+ method = params[:method] || :get
45
51
  body = params[:data].try!(:to_json)
46
- res = @client.request(http_method, uri(params), params[:query], body, HEADERS)
52
+ res = http_request(method, request_path(params), params[:query], body, params)
47
53
  status = res.status
48
54
  return JSON.parse(res.body) if 300 > status
49
- if 404 == status
50
- message = JSON.parse(res.body)['error'] rescue 'Not found'
51
- raise NotFound, message
52
- end
53
- raise Error.new(res.reason)
55
+ result = JSON.parse(res.body) rescue nil
56
+ err_msg = "#{res.reason}: #{result && result['error'] || '-'}"
57
+ # NotFound is raised only for valid responses from ElasticSearch
58
+ raise NotFound, err_msg if 404 == status && result
59
+ raise Error, err_msg
54
60
  end
55
61
 
56
62
  # shortcuts
57
- [:put, :post, :delete].each do |method|
63
+ [:put, :post].each do |method|
58
64
  define_method(method) do |params|
59
65
  params[:method] = method
60
66
  request params
61
67
  end
62
68
  end
63
69
 
64
- def get(params)
70
+ def delete!(params)
71
+ params[:method] = :delete
72
+ request params
73
+ end
74
+
75
+ def delete(params)
76
+ delete!(params)
77
+ rescue NotFound
78
+ end
79
+
80
+ def get!(params)
65
81
  params = {id: params} unless params.is_a?(Hash)
66
82
  params[:method] = :get
67
83
  request(params)
68
84
  end
69
85
 
86
+ def get(params)
87
+ get!(params)
88
+ rescue NotFound
89
+ end
90
+
70
91
  def set(id, data)
71
92
  request(id: id, data: data, method: :put)
72
93
  end
@@ -88,10 +109,16 @@ module Elastics
88
109
  end
89
110
 
90
111
  def index_exists?(index)
91
- get(index: index, type: nil, id: :_mapping)
92
- true
93
- rescue NotFound
94
- false
112
+ !!get(index: index, type: nil, id: :_mapping)
95
113
  end
114
+
115
+ private
116
+ # Endpoint for low-level request. For easy host highjacking & instrumentation.
117
+ # Params are not used directly but kept for instrumentation purpose.
118
+ # You probably don't want to use this method directly.
119
+ def http_request(method, path, query, body, params = nil, host = @host)
120
+ uri = "http://#{host}#{path}"
121
+ @client.request(method, uri, query, body, HEADERS)
122
+ end
96
123
  end
97
124
  end
@@ -0,0 +1,70 @@
1
+ require 'timeout'
2
+ require 'thread'
3
+ require 'thread_safe'
4
+
5
+ module Elastics
6
+ class Client
7
+ module Cluster
8
+ class NoAliveHosts < Error; end
9
+
10
+ private
11
+ def initialize_cluster(defaults)
12
+ @hosts = ThreadSafe::Array.new defaults[:host]
13
+ @dead_hosts = ThreadSafe::Hash.new
14
+ @connect_timeout = defaults[:connect_timeout] || 10
15
+ @resurrect_timeout = defaults[:resurrect_timeout] || 10
16
+ @current_host_n = 0
17
+ @cluster_mutex = Mutex.new
18
+ end
19
+
20
+ def http_request(method, path, query, body, params = nil)
21
+ host = next_cluster_host
22
+ Timeout.timeout(@connect_timeout) do
23
+ super(method, path, query, body, params, host)
24
+ end
25
+ rescue Timeout::Error, HTTPClient::ConnectTimeoutError
26
+ add_dead_host(host)
27
+ retry
28
+ end
29
+
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
45
+ end
46
+ end
47
+ end
48
+
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
56
+ end
57
+ @hosts << host
58
+ end
59
+ end
60
+ end
61
+
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
68
+ end
69
+ end
70
+ end
@@ -3,14 +3,15 @@ require 'elastics/active_record'
3
3
  module Elastics
4
4
  class Railtie < Rails::Railtie
5
5
  initializer 'elastics.configure_rails_initialization' do
6
- ::ActiveRecord::Base.extend Elastics::ActiveRecord
7
- unless ::ActiveRecord::LogSubscriber < ActiveRecord::LogSubscriber
8
- ::ActiveRecord::LogSubscriber.send :include, ActiveRecord::LogSubscriber
9
- end
6
+ ActiveRecord.install
10
7
  end
11
8
 
12
9
  rake_tasks do
13
10
  load 'tasks/elastics.rake'
14
11
  end
12
+
13
+ config.to_prepare do
14
+ Elastics.reset_models
15
+ end
15
16
  end
16
17
  end
@@ -1,35 +1,36 @@
1
+ require 'active_support'
2
+ require 'active_support/core_ext'
3
+
1
4
  module Elastics
2
5
  module Tasks
6
+ require 'elastics/tasks/config'
7
+ include Config
8
+
3
9
  require 'elastics/tasks/indices'
4
10
  include Indices
5
11
 
6
12
  require 'elastics/tasks/mappings'
7
13
  include Mappings
8
14
 
9
- extend self
10
-
11
- attr_writer :base_paths, :client, :config
12
-
13
- def base_paths
14
- @base_paths ||= [File.join(Rails.root, 'db', 'elastics')]
15
- end
15
+ require 'elastics/tasks/migrations'
16
+ include Migrations
16
17
 
17
- def migrate(options = {})
18
- delete_indices if options[:flush]
19
- create_indices
20
- put_mappings
18
+ if defined?(::ActiveRecord) && defined?(::Elastics::ActiveRecord)
19
+ require 'elastics/active_record/tasks_config'
20
+ include ActiveRecord::TasksConfig
21
21
  end
22
22
 
23
- def client
24
- @client ||= ::ActiveRecord::Base.elastics
25
- end
26
-
27
- def config
28
- @config ||= ::ActiveRecord::Base.elastics_config
29
- end
23
+ extend self
30
24
 
31
25
  def log(*args)
32
- Rails.logger.info(*args)
26
+ puts(*args)
33
27
  end
28
+
29
+ private
30
+ def each_filtered(collection, filter, &block)
31
+ filter = filter && filter.map(&:to_s)
32
+ collection = collection.select { |x| filter.include?(x) } if filter
33
+ collection.each &block
34
+ end
34
35
  end
35
36
  end
@@ -0,0 +1,38 @@
1
+ module Elastics
2
+ module Tasks
3
+ # Module contains basic configuration methods.
4
+ # You should setup Elastics::Task yourself unless you you use ActiveRecord.
5
+ module Config
6
+ attr_writer :base_paths
7
+
8
+ def base_paths
9
+ @base_paths ||= Dir.pwd
10
+ end
11
+
12
+ def client
13
+ @client ||= Client.new config.slice(:host, :port)
14
+ end
15
+
16
+ def client=(val)
17
+ @version_manager = nil
18
+ @client = val
19
+ end
20
+
21
+ def version_manager
22
+ @version_manager ||= VersionManager.new(client, config.slice(
23
+ :service_index,
24
+ :index_prefix,
25
+ ))
26
+ end
27
+
28
+ def config
29
+ @config ||= {}
30
+ end
31
+
32
+ def config=(val)
33
+ @version_manager = nil
34
+ @config = val
35
+ end
36
+ end
37
+ end
38
+ end
@@ -1,5 +1,9 @@
1
1
  module Elastics
2
2
  module Tasks
3
+ # Most of methods accepts `options` hash with:
4
+ # - `:indices` - array of indices to perform action on
5
+ # - `:version` - mapping version to use in method
6
+ #
3
7
  module Indices
4
8
  attr_writer :indices_path
5
9
 
@@ -13,7 +17,6 @@ module Elastics
13
17
  each_with_object({}) do |file, hash|
14
18
  name = File.basename file, '.yml'
15
19
  data = YAML.load_file(file)
16
- name = "#{config[:index_prefix]}#{name}"
17
20
  hash[name] = data[Rails.env] || data
18
21
  end
19
22
  end
@@ -22,20 +25,79 @@ module Elastics
22
25
  @indices ||= config[:index] ? [config[:index]] : indices_settings.keys
23
26
  end
24
27
 
25
- def delete_indices
26
- indices.each do |index|
27
- log "Delete index #{index}"
28
- client.delete index: index rescue NotFound
28
+ def versioned_index_name(*args)
29
+ version_manager.index_name *args
30
+ end
31
+
32
+ def purge(keep_data = false)
33
+ unless keep_data
34
+ drop_indices
35
+ drop_indices version: :next
36
+ end
37
+ index = version_manager.service_index
38
+ log "Deleting index #{index}"
39
+ version_manager.reset
40
+ client.delete index: index
41
+ end
42
+
43
+ def drop_indices(options = {})
44
+ version = options.fetch :version, :current
45
+ each_filtered(indices, options[:indices]) do |index|
46
+ versioned_index = versioned_index_name(index, version)
47
+ log "Deleting index #{index} (#{versioned_index})"
48
+ client.delete index: versioned_index
29
49
  end
30
50
  end
31
51
 
32
- def create_indices
33
- indices.each do |index|
34
- log "Create index #{index}"
35
- unless client.index_exists?(index)
36
- client.put(index: index, data: indices_settings[index])
52
+ def create_indices(options = {})
53
+ version = options.fetch :version, :current
54
+ each_filtered(indices, options[:indices]) do |index|
55
+ versioned_index = versioned_index_name(index, version)
56
+ exists = client.index_exists?(versioned_index)
57
+ log_msg = "Creating index #{index} (#{versioned_index})"
58
+ log_msg << ' - Skipping: exists' if exists
59
+ log log_msg
60
+ unless exists
61
+ client.put(index: versioned_index, data: indices_settings[index])
37
62
  end
38
63
  end
64
+ manage_aliases :add, options if version.to_s == 'current'
65
+ end
66
+
67
+ # Action can be :add or :remove.
68
+ def manage_aliases(action, options = {})
69
+ version = options.fetch :version, :current
70
+ post_aliases(options) do |index|
71
+ alias_action(action, index, version)
72
+ end
73
+ end
74
+
75
+ def forward_aliases(options = {})
76
+ new_versions = {}
77
+ post_aliases options do |index|
78
+ new_versions[index] = version_manager.next_version index
79
+ [
80
+ alias_action(:remove, index, :current),
81
+ alias_action(:add, index, :next),
82
+ ]
83
+ end
84
+ drop_indices(options.merge version: :current) if options.fetch(:drop, true)
85
+ new_versions.each do |index, version|
86
+ version_manager.set index, current: version
87
+ end
88
+ end
89
+
90
+ def alias_action(action, index, version)
91
+ {action => {
92
+ index: versioned_index_name(index, version),
93
+ alias: versioned_index_name(index, :alias),
94
+ }}
95
+ end
96
+
97
+ def post_aliases(options = {}, &block)
98
+ actions = each_filtered(indices, options[:indices]).map(&block).flatten
99
+ log "Posting aliases: #{actions.inspect}"
100
+ client.post id: :_aliases, data: {actions: actions} if actions.any?
39
101
  end
40
102
  end
41
103
  end