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.
- 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
|