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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: f7b4e6384b29ab3105fb8619706a05afdce54a0e
4
- data.tar.gz: d5eb3b6133e0682e5470943e5ec4d9ffcc077c7c
3
+ metadata.gz: 39da83ca0918ecd5e5bced988a20a0f43c4a0651
4
+ data.tar.gz: 1b71e46b7873678e169586cf4f16204c3f01d01c
5
5
  SHA512:
6
- metadata.gz: a2bf37ecf37ed6734e2f3ba4e95a70db13c8c5fd9fd50722a9e81842a9ed241d9d6dbce51a628763770358eb80dc132ecd39424ff2a017e3bf38def5976d5ec0
7
- data.tar.gz: b3c316df5e6ce8113a0e41b5879049775adcb127593b8a970487e479198577c281e8611bbe57a13820c1bec795d366e3ea14af6b41db7ddec7d30f5f48355e54
6
+ metadata.gz: fc66215c7c93d9209828494f50aa2ed7a39ec4477924728955641ee6d8ed4cfa0fe09ed7388e7d6c599923c3eef28b729c1c3fd92f0c370a9f02b88bf16d2240
7
+ data.tar.gz: b03818b919cb671ec17c259f30527d18d994a5dc3c05377b1fbacd87bcbb73b68f4de5bdff16af9f43500f465f950d795be53a82562b7ec0eec86f03b7defe6c
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --color
data/.travis.yml ADDED
@@ -0,0 +1,7 @@
1
+ language: ruby
2
+ rvm:
3
+ - 1.9.3
4
+ - 2.0.0
5
+ - 2.1.4
6
+ notifications:
7
+ email: false
data/README.md CHANGED
@@ -1,13 +1,21 @@
1
1
  # elastics
2
+ [![Gem Version](https://badge.fury.io/rb/elastics.svg)](http://badge.fury.io/rb/elastics)
3
+ [![Code Climate](https://codeclimate.com/github/printercu/elastics-rb/badges/gpa.svg)](https://codeclimate.com/github/printercu/elastics-rb)
4
+ [![Build Status](https://travis-ci.org/printercu/elastics-rb.svg)](https://travis-ci.org/printercu/elastics-rb)
2
5
 
3
6
  Simple ElasticSearch client.
7
+ - basic API only
8
+ - transparent aliases management & zero-downtime migrations
9
+ - capistrano integration
4
10
 
5
- Fast and thread-safe [httpclient](https://github.com/nahi/httpclient) under the hood.
11
+ Fast and thread-safe [httpclient](https://github.com/nahi/httpclient) is under the hood.
6
12
 
7
13
  ## Install
8
14
 
9
15
  ```ruby
10
16
  # Gemfile
17
+ gem 'elastics', '~> 0.2' # use version from the badge above
18
+ # or
11
19
  gem 'elastics', github: 'printercu/elastics-rb'
12
20
  ```
13
21
 
@@ -19,10 +27,11 @@ gem 'elastics', github: 'printercu/elastics-rb'
19
27
  # initialize client with
20
28
  client = Elastics::Client.new(options)
21
29
  # options is hash with
22
- # :host
23
- # :port
30
+ # :host - hostname with port or array with hosts (default 127.0.0.1:9200)
24
31
  # :index - (default index)
25
32
  # :type - (default type)
33
+ # :connect_timeout - timeout to mark the host as dead in cluster-mode (default 10)
34
+ # :resurrect_timeout - timeout to mark dead host as alive in cluster-mode (default 10)
26
35
 
27
36
  # basic request
28
37
  client.request(options)
@@ -49,6 +58,8 @@ client.index(params) # PUT if :id is set, otherwise POST
49
58
  client.index_exists?(name)
50
59
  ```
51
60
 
61
+ When using cluster-mode you should also install `gem 'thread_safe'`.
62
+
52
63
  ### ActiveRecord
53
64
 
54
65
  ```ruby
@@ -63,6 +74,7 @@ class User < ActiveRecord::Base
63
74
  end
64
75
 
65
76
  User.search_elastics(data)
77
+ # Returns Elastics::ActiveRecord::SearchResult object with some useful methods
66
78
  ```
67
79
 
68
80
  #### Configure
@@ -78,26 +90,85 @@ development:
78
90
 
79
91
  production:
80
92
  elastics:
81
- host: 10.0.0.1
82
- port: 1234
93
+ host: 10.0.0.1:1234
94
+ # or
95
+ host:
96
+ - 10.0.0.1:1234
97
+ - 10.0.0.2:1234
83
98
 
84
99
  index: app
85
100
  # or
86
101
  index_prefix: app_
87
102
  ```
88
103
 
104
+ #### Create mappings & import data
105
+ ```
106
+ $ rake elastics:migrate elastics:reindex
107
+ ```
108
+
89
109
  #### Mappings & index settings
90
110
  Mappings & index settings `.yml` files are placed in
91
111
  `db/elastics/mappings` & `db/elastics/indices`.
92
112
  For now this files are not related to models and only used by rake tasks.
93
113
 
94
- - `rake elastics:create` (or `Elastics::Tasks.create_indices`)
114
+ ### Index management
115
+ When index is created elastics transparently manages aliases for it.
116
+ Instead of creating `index1` it creates `index1-v0` and create `index1` alias for it.
117
+ When you perform normal migration, mappings are applied to the current version.
118
+ Later when you perform full migration `index1-v1` is created, after reindexing
119
+ aliases are changed and `index-v0` is droped.
120
+
121
+ Versions of indices are stored in ElasticSearch in `.elastics` index.
122
+
123
+ ### Rake tasks
124
+ All rake tasks except `purge` accepts list of indices to process
125
+ (`rake elastics:create[index1,index2]`).
126
+ Also you can specify index version like this `rake elastics:migrate version=next`.
127
+ Version can be set to `next` or `current` (default).
128
+
129
+ Rake tasks are just frontend for `Elastics::Tasks`'s methods.
130
+ For complex migrations, when you need partially reindex data,
131
+ you may want to write custom scripts using this methods.
132
+
133
+ - `rake elastics:create` (`.create_indices`)
95
134
  creates index with settings for each file from `indices` folder.
96
- For single index it only processes file with index name.
97
- For multiple indices each index name is `#{index_prefix}#{file.basename}`
98
135
 
99
- - `rake elastics:migrate` (or `Elastics::Tasks.migrate`)
136
+ - `rake elastics:migrate` (`.migrate`)
100
137
  puts mappings from `mappings` folder.
101
138
 
139
+ - `rake elastics:migrate full=true` (`.migrate!`)
140
+ performs full migration.
141
+
142
+ - `rake elastics:reindex` (`.reindex`)
143
+ reindexes data.
144
+
145
+ #### Using without Rails
146
+ You need to setup `Elastics::Tasks` yourself. This can be done in `environment` or
147
+ `db:load_config` rake tasks.
148
+
149
+ ```ruby
150
+ task :environment do
151
+ Elastics::Tasks.base_paths = '/path/to/your/elastics/folder'
152
+ Elastics::Tasks.config = your_configuration
153
+ end
154
+ ```
155
+
156
+ Also you need to install `active_support` & require
157
+ `active_support/core_ext/object` to be able to run tasks.
158
+
159
+ ### Use with capistrano
160
+ Add following lines to your `deploy.rb` and all rake tasks will be available in cap.
161
+
162
+ ```ruby
163
+ role :elastics, '%HOSTNAME%', primary: true
164
+
165
+ require 'elastics/capistrano'
166
+ ```
167
+
168
+ Indices & rake options can be passed like this:
169
+ ```
170
+ cap --dry-run elastics:migrate INDICES=index1,index2 ES_OPTIONS='full=true no_drop=true'
171
+ ```
172
+
102
173
  ## License
103
174
  MIT
data/Rakefile CHANGED
@@ -1,11 +1,6 @@
1
1
  require 'bundler/gem_tasks'
2
- require 'rake/testtask'
2
+ require 'rspec/core/rake_task'
3
3
 
4
- Rake::TestTask.new do |t|
5
- t.libs << 'lib/elastics'
6
- t.libs << 'test'
7
- t.test_files = FileList['test/lib/elastics/*_test.rb']
8
- t.verbose = true
9
- end
4
+ RSpec::Core::RakeTask.new(:spec)
10
5
 
11
- task default: :test
6
+ task default: :spec
data/elastics.gemspec CHANGED
@@ -21,5 +21,8 @@ Gem::Specification.new do |spec|
21
21
  spec.add_runtime_dependency 'httpclient', '~> 2.4.0'
22
22
 
23
23
  spec.add_development_dependency 'bundler', '~> 1.5'
24
- spec.add_development_dependency 'rake', '~> 10'
24
+ spec.add_development_dependency 'rake', '~> 10.0'
25
+ spec.add_development_dependency 'rspec', '~> 3.1.0'
26
+ spec.add_development_dependency 'thread_safe', '~> 0.3.4'
27
+ spec.add_development_dependency 'activesupport', '~> 4.1.6'
25
28
  end
data/lib/elastics.rb CHANGED
@@ -3,11 +3,22 @@ module Elastics
3
3
  class NotFound < Error; end
4
4
 
5
5
  require 'elastics/client'
6
+ require 'elastics/version_manager'
6
7
  require 'elastics/query_helper'
7
8
 
8
9
  autoload :Tasks, 'elastics/tasks'
9
10
 
10
11
  extend QueryHelper
12
+
13
+ class << self
14
+ attr_reader :models
15
+
16
+ def reset_models
17
+ @models = []
18
+ end
19
+ end
20
+
21
+ reset_models
11
22
  end
12
23
 
13
24
  require 'elastics/railtie' if defined?(Rails)
@@ -2,10 +2,18 @@ module Elastics
2
2
  module ActiveRecord
3
3
  extend ActiveSupport::Autoload
4
4
 
5
+ autoload :SearchResult
5
6
  autoload :ModelSchema
6
7
  autoload :HelperMethods
7
8
  autoload :Instrumentation
8
- autoload :LogSubscriber
9
+ autoload :LogSubscriber, 'elastics/active_record/instrumentation'
10
+
11
+ class << self
12
+ def install
13
+ ::ActiveRecord::Base.extend self
14
+ Instrumentation.install
15
+ end
16
+ end
9
17
 
10
18
  def elastics_config
11
19
  @elastics_config ||= connection_config[:elastics].try!(:with_indifferent_access) ||
@@ -16,16 +24,23 @@ module Elastics
16
24
  @elastics ||= Client.new elastics_config.slice(:host, :port)
17
25
  end
18
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
+
19
35
  def indexed_with_elastics(options = {})
20
36
  options = {
21
37
  hooks: [:update, :destroy],
22
- }.merge(options)
38
+ }.merge!(options)
23
39
 
24
40
  extend ModelSchema
25
41
  include HelperMethods
26
- extend Instrumentation
27
42
 
28
- self.elastics_index_name = options[:index] if options[:index]
43
+ self.elastics_index_base = options[:index] if options[:index]
29
44
  self.elastics_type_name = options[:type] if options[:type]
30
45
 
31
46
  hooks = options[:hooks]
@@ -3,42 +3,41 @@ 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)
8
+ end
9
+
6
10
  module ClassMethods
7
- def search(data = {}, routing = nil)
8
- es_results = search_elastics(data, routing)
9
- ids = es_results['hits'.freeze]['hits'.freeze].map { |x| x['_id'.freeze].to_i }
10
- relation = where(id: ids)
11
- items_by_id = relation.index_by(&:id)
12
- collection = ids.map { |i| items_by_id[i] }
13
- {
14
- collection: collection,
15
- relation: relation,
16
- search: es_results,
17
- }
11
+ def request_elastics(params)
12
+ request = {
13
+ index: elastics_index_name,
14
+ type: elastics_type_name,
15
+ model: self,
16
+ }.merge!(params)
17
+ elastics.request(request)
18
18
  end
19
19
 
20
- def search_elastics(data = {}, routing = nil)
20
+ def search_elastics(data = {}, options = {})
21
21
  request = {
22
- id: :_search,
22
+ id: :_search,
23
23
  data: data,
24
24
  }
25
- request[:query] = {routing: routing} if routing
26
- request_elastics(request)
25
+ if routing = options[:routing]
26
+ request[:query] = {routing: routing}
27
+ end
28
+ SearchResult.new self, request_elastics(request), options
27
29
  end
28
30
 
29
- def request_elastics(params)
30
- request = {
31
- index: elastics_index_name,
32
- type: elastics_type_name,
33
- }.merge!(params)
34
- elastics.request(request)
31
+ def find_all_ordered(ids)
32
+ items_by_id = where(id: ids).index_by(&:id)
33
+ ids.map { |i| items_by_id[i] }
35
34
  end
36
35
 
37
36
  def elastics_mapping
38
37
  request_elastics(method: :get, id: :_mapping)
39
38
  end
40
39
 
41
- def reindex(*args)
40
+ def reindex_elastics(*args)
42
41
  find_each(*args, &:index_elastics)
43
42
  end
44
43
  end
@@ -47,12 +46,14 @@ module Elastics
47
46
  self.class.request_elastics(method: :post, id: id, data: to_elastics)
48
47
  end
49
48
 
50
- def delete_elastics
51
- self.class.request_elastics(method: :delete, id: id)
49
+ def update_elastics(fields)
50
+ self.class.request_elastics(method: :post, id: "#{id}/_update", data: {
51
+ doc: fields
52
+ })
52
53
  end
53
54
 
54
- def to_elastics
55
- as_json
55
+ def delete_elastics
56
+ self.class.request_elastics(method: :delete, id: id)
56
57
  end
57
58
  end
58
59
  end
@@ -1,15 +1,71 @@
1
1
  module Elastics
2
2
  module ActiveRecord
3
+ # To be included in `Elastics::Client`
3
4
  module Instrumentation
4
- def request_elastics(params = {})
5
- data = {
6
- name: name,
7
- request: params,
8
- }
9
- ActiveSupport::Notifications.instrument 'request_elastics.active_record', data do
10
- super(params)
5
+ class << self
6
+ def install
7
+ if Client.respond_to?(:prepend)
8
+ Client.prepend self
9
+ else
10
+ Client.include Fallback
11
+ end
12
+ unless ::ActiveRecord::LogSubscriber < LogSubscriber
13
+ ::ActiveRecord::LogSubscriber.send :include, LogSubscriber
14
+ end
11
15
  end
12
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
13
69
  end
14
70
  end
15
71
  end
@@ -1,7 +1,17 @@
1
1
  module Elastics
2
2
  module ActiveRecord
3
3
  module ModelSchema
4
- attr_writer :elastics_index_name, :elastics_type_name
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
5
15
 
6
16
  def elastics_index_name
7
17
  reset_elastics_index_name unless defined?(@elastics_index_name)
@@ -9,30 +19,25 @@ module Elastics
9
19
  end
10
20
 
11
21
  def elastics_type_name
12
- reset_elastics_index_name unless defined?(@elastics_type_name)
13
- @elastics_type_name
22
+ @elastics_type_name ||= model_name.to_s.demodulize.underscore.singularize
14
23
  end
15
24
 
16
25
  def reset_elastics_index_name
17
- superclass_responds = superclass.respond_to?(:elastics_index_name)
18
- index = if abstract_class? && superclass_responds
19
- superclass == ::ActiveRecord::Base ? nil : superclass.elastics_index_name
20
- elsif superclass.abstract_class? && superclass_responds
21
- superclass.elastics_index_name || compute_elastics_index_name
22
- else
23
- compute_elastics_index_name
26
+ @elastics_index_name = if self != ::ActiveRecord::Base && !abstract_class?
27
+ superclass.try(:elastics_index_name) || compute_elastics_index_name
24
28
  end
25
- @elastics_index_name = index
26
- @elastics_type_name = compute_elastics_type_name
27
29
  end
28
30
 
29
- def compute_elastics_index_name(name = nil)
30
- elastics_config[:index] ||
31
- "#{elastics_config[:index_prefix]}#{name || table_name.singularize}"
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
32
37
  end
33
38
 
34
- def compute_elastics_type_name
35
- model_name.to_s.demodulize.underscore.singularize
39
+ def inherited(base)
40
+ super.tap { ::Elastics::ActiveRecord::ModelSchema.track_model(base) }
36
41
  end
37
42
  end
38
43
  end