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