elastics 0.1.1 → 0.2.0

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