elastics 0.1.1 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rspec +1 -0
- data/.travis.yml +7 -0
- data/README.md +80 -9
- data/Rakefile +3 -8
- data/elastics.gemspec +4 -1
- data/lib/elastics.rb +11 -0
- data/lib/elastics/active_record.rb +19 -4
- data/lib/elastics/active_record/helper_methods.rb +27 -26
- data/lib/elastics/active_record/instrumentation.rb +63 -7
- data/lib/elastics/active_record/model_schema.rb +22 -17
- data/lib/elastics/active_record/search_result.rb +49 -0
- data/lib/elastics/active_record/tasks_config.rb +25 -0
- data/lib/elastics/capistrano.rb +14 -0
- data/lib/elastics/client.rb +49 -22
- data/lib/elastics/client/cluster.rb +70 -0
- data/lib/elastics/railtie.rb +5 -4
- data/lib/elastics/tasks.rb +20 -19
- data/lib/elastics/tasks/config.rb +38 -0
- data/lib/elastics/tasks/indices.rb +72 -10
- data/lib/elastics/tasks/mappings.rb +11 -5
- data/lib/elastics/tasks/migrations.rb +42 -0
- data/lib/elastics/version.rb +1 -1
- data/lib/elastics/version_manager.rb +86 -0
- data/lib/tasks/elastics.rake +31 -8
- data/spec/lib/elastics/client/cluster_spec.rb +108 -0
- data/spec/spec_helper.rb +9 -0
- metadata +62 -7
- data/lib/elastics/active_record/log_subscriber.rb +0 -27
@@ -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
|
data/lib/elastics/client.rb
CHANGED
@@ -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
|
-
|
12
|
-
|
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
|
32
|
-
str = "
|
33
|
-
if index = params[:index]
|
34
|
-
str
|
35
|
-
type
|
36
|
-
str
|
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
|
45
|
+
str << "/#{path}" if path
|
40
46
|
str
|
41
47
|
end
|
42
48
|
|
43
49
|
def request(params)
|
44
|
-
|
50
|
+
method = params[:method] || :get
|
45
51
|
body = params[:data].try!(:to_json)
|
46
|
-
res =
|
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
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
raise Error
|
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
|
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
|
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
|
data/lib/elastics/railtie.rb
CHANGED
@@ -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
|
-
|
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
|
data/lib/elastics/tasks.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
18
|
-
|
19
|
-
|
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
|
-
|
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
|
-
|
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
|
26
|
-
|
27
|
-
|
28
|
-
|
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
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
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
|