chewy 0.0.1

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.
Files changed (58) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +17 -0
  3. data/.rspec +3 -0
  4. data/.rvmrc +1 -0
  5. data/.travis.yml +7 -0
  6. data/Gemfile +12 -0
  7. data/Guardfile +24 -0
  8. data/LICENSE.txt +22 -0
  9. data/README.md +208 -0
  10. data/Rakefile +6 -0
  11. data/chewy.gemspec +32 -0
  12. data/lib/chewy.rb +55 -0
  13. data/lib/chewy/config.rb +48 -0
  14. data/lib/chewy/fields/base.rb +49 -0
  15. data/lib/chewy/fields/default.rb +10 -0
  16. data/lib/chewy/fields/root.rb +10 -0
  17. data/lib/chewy/index.rb +71 -0
  18. data/lib/chewy/index/actions.rb +43 -0
  19. data/lib/chewy/index/client.rb +13 -0
  20. data/lib/chewy/index/search.rb +26 -0
  21. data/lib/chewy/query.rb +141 -0
  22. data/lib/chewy/query/criteria.rb +81 -0
  23. data/lib/chewy/query/loading.rb +27 -0
  24. data/lib/chewy/query/pagination.rb +39 -0
  25. data/lib/chewy/rspec.rb +1 -0
  26. data/lib/chewy/rspec/update_index.rb +121 -0
  27. data/lib/chewy/type.rb +22 -0
  28. data/lib/chewy/type/adapter/active_record.rb +27 -0
  29. data/lib/chewy/type/adapter/object.rb +22 -0
  30. data/lib/chewy/type/base.rb +41 -0
  31. data/lib/chewy/type/import.rb +67 -0
  32. data/lib/chewy/type/mapping.rb +50 -0
  33. data/lib/chewy/type/observe.rb +37 -0
  34. data/lib/chewy/type/wrapper.rb +35 -0
  35. data/lib/chewy/version.rb +3 -0
  36. data/spec/chewy/config_spec.rb +50 -0
  37. data/spec/chewy/fields/base_spec.rb +70 -0
  38. data/spec/chewy/fields/default_spec.rb +6 -0
  39. data/spec/chewy/fields/root_spec.rb +6 -0
  40. data/spec/chewy/index/actions_spec.rb +53 -0
  41. data/spec/chewy/index/client_spec.rb +18 -0
  42. data/spec/chewy/index/search_spec.rb +54 -0
  43. data/spec/chewy/index_spec.rb +65 -0
  44. data/spec/chewy/query/criteria_spec.rb +73 -0
  45. data/spec/chewy/query/loading_spec.rb +37 -0
  46. data/spec/chewy/query/pagination_spec.rb +40 -0
  47. data/spec/chewy/query_spec.rb +110 -0
  48. data/spec/chewy/rspec/update_index_spec.rb +149 -0
  49. data/spec/chewy/type/import_spec.rb +68 -0
  50. data/spec/chewy/type/mapping_spec.rb +54 -0
  51. data/spec/chewy/type/observe_spec.rb +55 -0
  52. data/spec/chewy/type/wrapper_spec.rb +35 -0
  53. data/spec/chewy/type_spec.rb +43 -0
  54. data/spec/chewy_spec.rb +36 -0
  55. data/spec/spec_helper.rb +48 -0
  56. data/spec/support/class_helpers.rb +16 -0
  57. data/spec/support/fail_helpers.rb +13 -0
  58. metadata +249 -0
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 1731bb408069ab82feb6979f9f70008a0977cb48
4
+ data.tar.gz: b676bfdc501aece147b1af8a915f99defd0f2eaa
5
+ SHA512:
6
+ metadata.gz: 7ce0646699822d696680438f2be44d12b4a9829470362e841e357e059759474c4a54fdbed4c4b5ec6946b2d50800fd7e8be3a3f31d33e7ac987705fb5622266c
7
+ data.tar.gz: 5ede42795877994577c4aac44c5898dd7abacc7d922db0a1563b0e5e1564d8c02fec987eb4f01014f7fc46c4c20811aab5cc4f17280d734be6f7f5ba73fa8c0f
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --color
2
+ --backtrace
3
+ --order random
data/.rvmrc ADDED
@@ -0,0 +1 @@
1
+ rvm use 1.9.3@chewy --create
@@ -0,0 +1,7 @@
1
+ language: ruby
2
+ rvm:
3
+ - 1.9.3
4
+ - 2.0.0
5
+ - rbx
6
+ services:
7
+ - elasticsearch
data/Gemfile ADDED
@@ -0,0 +1,12 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in chewy.gemspec
4
+ gemspec
5
+
6
+ group :test do
7
+ gem 'guard'
8
+ gem 'guard-rspec'
9
+ gem 'rb-inotify', require: false
10
+ gem 'rb-fsevent', require: false
11
+ gem 'rb-fchange', require: false
12
+ end
@@ -0,0 +1,24 @@
1
+ # A sample Guardfile
2
+ # More info at https://github.com/guard/guard#readme
3
+
4
+ guard :rspec do
5
+ watch(%r{^spec/.+_spec\.rb$})
6
+ watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" }
7
+ watch('spec/spec_helper.rb') { "spec" }
8
+
9
+ # Rails example
10
+ watch(%r{^app/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" }
11
+ watch(%r{^app/(.*)(\.erb|\.haml|\.slim)$}) { |m| "spec/#{m[1]}#{m[2]}_spec.rb" }
12
+ watch(%r{^app/controllers/(.+)_(controller)\.rb$}) { |m| ["spec/routing/#{m[1]}_routing_spec.rb", "spec/#{m[2]}s/#{m[1]}_#{m[2]}_spec.rb", "spec/acceptance/#{m[1]}_spec.rb"] }
13
+ watch(%r{^spec/support/(.+)\.rb$}) { "spec" }
14
+ watch('config/routes.rb') { "spec/routing" }
15
+ watch('app/controllers/application_controller.rb') { "spec/controllers" }
16
+
17
+ # Capybara features specs
18
+ watch(%r{^app/views/(.+)/.*\.(erb|haml|slim)$}) { |m| "spec/features/#{m[1]}_spec.rb" }
19
+
20
+ # Turnip features and steps
21
+ watch(%r{^spec/acceptance/(.+)\.feature$})
22
+ watch(%r{^spec/acceptance/steps/(.+)_steps\.rb$}) { |m| Dir[File.join("**/#{m[1]}.feature")][0] || 'spec/acceptance' }
23
+ end
24
+
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 pyromaniac
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,208 @@
1
+ # Chewy
2
+
3
+ Chewy is ODM and wrapper for official elasticsearch client (https://github.com/elasticsearch/elasticsearch-ruby)
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ gem 'chewy'
10
+
11
+ And then execute:
12
+
13
+ $ bundle
14
+
15
+ Or install it yourself as:
16
+
17
+ $ gem install chewy
18
+
19
+ ## Usage
20
+
21
+ ### Index file
22
+
23
+ 1. Create `/app/chewy/users_index.rb`
24
+
25
+ ```ruby
26
+ class UsersIndex < Chewy::Index
27
+
28
+ end
29
+ ```
30
+
31
+ 2. Add one or more types mapping
32
+
33
+ ```ruby
34
+ class UsersIndex < Chewy::Index
35
+ define_type User.active # or just model instead_of scope: define_type User
36
+ end
37
+ ```
38
+
39
+ Newly-defined index type class is accessible via `UsersIndex.user` or `UsersIndex::User`
40
+
41
+ 3. Add some type mappings
42
+
43
+ ```ruby
44
+ class UsersIndex < Chewy::Index
45
+ define_type User.active.includes(:country, :bages, :projects) do
46
+ field :first_name, :last_name # multiple fields without additional options
47
+ field :email, analyzer: 'email' # elasticsearch-related options
48
+ field :country, value: ->(user) { user.country.name } # custom value proc
49
+ field :bages, value: ->(user) { user.bages.map(&:name) } # passing array values to index
50
+ field :projects, type: 'object' do # the same syntax for `multi_field`
51
+ field :title
52
+ field :description # default data type is `string`
53
+ end
54
+ field :rating, type: 'integer' # custom data type
55
+ field :created_at, type: 'date', include_in_all: false
56
+ end
57
+ end
58
+ ```
59
+
60
+ Mapping definitions - http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/mapping.html
61
+
62
+ 4. Add some index- and type-related settings
63
+
64
+ ```ruby
65
+ class UsersIndex < Chewy::Index
66
+ settings analysis: {
67
+ analyzer: {
68
+ email: {
69
+ tokenizer: 'keyword',
70
+ filter: ['lowercase']
71
+ }
72
+ }
73
+ }
74
+
75
+ define_type User.active.includes(:country, :bages, :projects) do
76
+ root _boost: { name: :_boost, null_value: 1.0 } do # optional `root` object settings
77
+ field :first_name, :last_name # multiple fields without additional options
78
+ field :email, analyzer: 'email' # elasticsearch-related options
79
+ field :country, value: ->(user) { user.country.name } # custom value proc
80
+ field :bages, value: ->(user) { user.bages.map(&:name) } # passing array values to index
81
+ field :projects, type: 'object' do # the same syntax for `multi_field`
82
+ field :title
83
+ field :description # default data type is `string`
84
+ end
85
+ field :about_translations, type: 'object'
86
+ field :rating, type: 'integer' # custom data type
87
+ field :created_at, type: 'date', include_in_all: false
88
+ end
89
+ end
90
+ end
91
+ ```
92
+
93
+ Index settings - http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-update-settings.html
94
+ Root object settings - http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/mapping-root-object-type.html
95
+
96
+ 5. Add model observing code
97
+
98
+ ```ruby
99
+ class User < ActiveRecord::Base
100
+ update_elasticsearch('users#user') { self } # specifying index, type and backreference
101
+ # for updating after user save or destroy
102
+ end
103
+
104
+ class Country < ActiveRecord::Base
105
+ has_many :users
106
+
107
+ update_elasticsearch('users#user') { users } # return single object or collection
108
+ end
109
+
110
+ class Project < ActiveRecord::Base
111
+ update_elasticsearch('users#user') { user if user.active? } # you can return even `nil` from the backreference
112
+ end
113
+
114
+ class Bage < ActiveRecord::Base
115
+ has_and_belongs_to_many :users
116
+
117
+ update_elasticsearch('users') { users } # if index has only one type
118
+ # there is no need to specify updated type
119
+ end
120
+ ```
121
+
122
+ ### Index manipulation
123
+
124
+ ```ruby
125
+ UsersIndex.index_delete # destroy index if exists
126
+ UsersIndex.index_create! # use bang or non-bang methods
127
+ UsersIndex.import # import with 0 arguments process all the data specified in type definition
128
+ # literally, User.active.includes(:country, :bages, :projects).find_in_batches
129
+
130
+ UsersIndex.import User.where('rating > 100') # or import specified users
131
+ UsersIndex.import [1, 2, 42] # pass even ids for import, it will be handled in the most effective way
132
+ ```
133
+
134
+ Also if passed user is #destroyed? or specified id is not existing in the database, import will perform `delete` index for this it
135
+
136
+ ### Observing strategies
137
+
138
+ There are 2 strategies for index updating: updating right after save and cummulative update. The first is by default. To perform the second one, use `Chewy.atomic`:
139
+
140
+ ```ruby
141
+ Chewy.atomic do
142
+ user.each { |user| user.update_attributes(name: user.name.strip) }
143
+ end
144
+ ```
145
+
146
+ Index update will be performed once per Chewy.atomic block. This strategy is highly usable for rails actions:
147
+
148
+ ```ruby
149
+ class ApplicationController < ActionController::Base
150
+ around_action { |&block| Chewy.atomic(&block) }
151
+ end
152
+ ```
153
+
154
+ ### Index querying
155
+
156
+ ```ruby
157
+ scope = UsersIndex.search.query(term: {name: 'foo'})
158
+ .filter({numeric_range: {rating: {gte: 100}}})
159
+ .order(created_at: :desc)
160
+ .limit(20).offset(100)
161
+
162
+ scope.to_a # => will produce array of UserIndex::User or other types instances
163
+ scope.map { |user| user.email }
164
+ scope.total_count # => will return total objects count
165
+
166
+ scope.per(10).page(3) # supports kaminari pagination
167
+ scope.explain.map { |user| user._explanation }
168
+ scope.only(:id, :email) # returns ids and emails only
169
+ ```
170
+
171
+ Also, queries can be performed on a type individually
172
+
173
+ ```ruby
174
+ UsersIndex.search.query(term: {name: 'foo'}).count # will return UserIndex::User array only
175
+ ```
176
+
177
+ ### Objects loading
178
+
179
+ It is possible to load source objects from database for every search result:
180
+
181
+ ```ruby
182
+ scope = UsersIndex.search.filter({numeric_range: {rating: {gte: 100}}})
183
+
184
+ scope.load # => will return User instances array (not a scope because )
185
+ scope.load(scopes: { user: ->(_) { includes(:country) }}) # => you can also pass loading scopes for each
186
+ # possibly returned type
187
+ scope.only(:id).load # it is optimal to request ids only if you are not planning to use type objects
188
+ ```
189
+
190
+ ## TODO a.k.a coming soon:
191
+
192
+ * Dynamic templates additional DSL
193
+ * Typecasting support
194
+ * Advanced (simplyfied) query DSL: `UsersIndex.query { email == 'my@gmail.com' }` will produce term query
195
+ * Remove Index.search method, all the query DSL methods should be delegated to the Index
196
+ * Observing strategies reworking
197
+ * update_all support
198
+ * Other than ActiveRecord ORMs support (Mongoid)
199
+ * Maybe, closer ORM/ODM integration, creating index classes implicitly
200
+ * Better facets support
201
+
202
+ ## Contributing
203
+
204
+ 1. Fork it ( http://github.com/<my-github-username>/chewy/fork )
205
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
206
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
207
+ 4. Push to the branch (`git push origin my-new-feature`)
208
+ 5. Create new Pull Request
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
@@ -0,0 +1,32 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'chewy/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'chewy'
8
+ spec.version = Chewy::VERSION
9
+ spec.authors = ['pyromaniac']
10
+ spec.email = ['kinwizard@gmail.com']
11
+ spec.summary = %q{Elasticsearch ODM client wrapper}
12
+ spec.description = %q{Chewy provides functionality for Elasticsearch index handling, documents import mappings and chainable query DSL}
13
+ spec.homepage = ''
14
+ spec.license = 'MIT'
15
+
16
+ spec.files = `git ls-files`.split($/)
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ['lib']
20
+
21
+ spec.add_development_dependency 'bundler'
22
+ spec.add_development_dependency 'rake'
23
+ spec.add_development_dependency 'rspec'
24
+ spec.add_development_dependency 'sqlite3'
25
+ spec.add_development_dependency 'kaminari'
26
+ spec.add_development_dependency 'activerecord', '>= 3.2'
27
+ spec.add_development_dependency 'database_cleaner'
28
+ spec.add_development_dependency 'rubysl', '~> 2.0' if RUBY_ENGINE == 'rbx'
29
+
30
+ spec.add_dependency 'activesupport', '>= 3.2'
31
+ spec.add_dependency 'elasticsearch'
32
+ end
@@ -0,0 +1,55 @@
1
+ require 'active_support/concern'
2
+ require 'active_support/core_ext'
3
+ require 'active_support/json'
4
+ require 'singleton'
5
+
6
+ require 'elasticsearch'
7
+
8
+ require 'chewy/version'
9
+ require 'chewy/config'
10
+ require 'chewy/index'
11
+ require 'chewy/type'
12
+ require 'chewy/query'
13
+ require 'chewy/fields/base'
14
+ require 'chewy/fields/default'
15
+ require 'chewy/fields/root'
16
+
17
+ ActiveSupport.on_load(:active_record) do
18
+ extend Chewy::Type::Observe::ActiveRecordMethods
19
+ end
20
+
21
+ module Chewy
22
+ class Error < StandardError
23
+ end
24
+
25
+ class UndefinedIndex < Error
26
+ end
27
+
28
+ class UndefinedType < Error
29
+ end
30
+
31
+ class UnderivableType < Error
32
+ end
33
+
34
+ def self.derive_type name
35
+ return name if name.is_a?(Class) && name < Chewy::Type::Base
36
+
37
+ index_name, type_name = name.split('#', 2)
38
+ class_name = "#{index_name.camelize}Index"
39
+ index = class_name.safe_constantize
40
+ raise Chewy::UnderivableType.new("Can not find index named `#{class_name}`") unless index && index < Chewy::Index
41
+ type = if type_name.present?
42
+ index.types[type_name] or raise Chewy::UnderivableType.new("Index `#{class_name}` doesn`t have type named `#{type_name}`")
43
+ elsif index.types.values.one?
44
+ index.types.values.first
45
+ else
46
+ raise Chewy::UnderivableType.new("Index `#{class_name}` has more than one type, please specify type via `#{index_name}#type_name`")
47
+ end
48
+ end
49
+
50
+ def self.config
51
+ Chewy::Config.instance
52
+ end
53
+
54
+ singleton_class.delegate *Chewy::Config.delegated, to: :config
55
+ end
@@ -0,0 +1,48 @@
1
+ module Chewy
2
+ class Config
3
+ include Singleton
4
+
5
+ attr_accessor :observing_enabled, :client_options
6
+
7
+ def self.delegated
8
+ public_instance_methods - self.superclass.public_instance_methods - Singleton.public_instance_methods
9
+ end
10
+
11
+ def initialize
12
+ @observing_enabled = true
13
+ @client_options = {}
14
+ end
15
+
16
+ def client_options
17
+ yaml_options = if defined? Rails
18
+ file = Rails.root.join(*%w(config chewy.yml))
19
+ YAML.load_file(file)[Rails.env].try(:deep_symbolize_keys) if File.exists?(file)
20
+ end
21
+ @client_options.merge(yaml_options || {})
22
+ end
23
+
24
+ def atomic?
25
+ atomic_stash.any?
26
+ end
27
+
28
+ def atomic
29
+ atomic_stash.push({})
30
+ result = yield
31
+ atomic_stash.last.each { |type, ids| type.import(ids) }
32
+ result
33
+ ensure
34
+ atomic_stash.pop
35
+ end
36
+
37
+ def atomic_stash(type = nil, *ids)
38
+ if type
39
+ raise ArgumentError.new('Only Chewy::Type::Base accepted as the first argument') unless type < Chewy::Type::Base
40
+ atomic_stash.push({}) unless atomic_stash.last
41
+ atomic_stash.last[type] ||= []
42
+ atomic_stash.last[type] |= ids.flatten
43
+ else
44
+ Thread.current[:chewy_atomic] ||= []
45
+ end
46
+ end
47
+ end
48
+ end