elasticsearch-persistence 0.1.3 → 0.1.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (53) hide show
  1. checksums.yaml +8 -8
  2. data/CHANGELOG.md +4 -0
  3. data/README.md +238 -7
  4. data/elasticsearch-persistence.gemspec +4 -1
  5. data/examples/music/album.rb +34 -0
  6. data/examples/music/artist.rb +50 -0
  7. data/examples/music/artists/_form.html.erb +8 -0
  8. data/examples/music/artists/artists_controller.rb +67 -0
  9. data/examples/music/artists/artists_controller_test.rb +53 -0
  10. data/examples/music/artists/index.html.erb +57 -0
  11. data/examples/music/artists/show.html.erb +51 -0
  12. data/examples/music/assets/application.css +226 -0
  13. data/examples/music/assets/autocomplete.css +48 -0
  14. data/examples/music/assets/blank_cover.png +0 -0
  15. data/examples/music/assets/form.css +113 -0
  16. data/examples/music/index_manager.rb +60 -0
  17. data/examples/music/search/index.html.erb +93 -0
  18. data/examples/music/search/search_controller.rb +41 -0
  19. data/examples/music/search/search_controller_test.rb +9 -0
  20. data/examples/music/search/search_helper.rb +15 -0
  21. data/examples/music/suggester.rb +45 -0
  22. data/examples/music/template.rb +392 -0
  23. data/examples/music/vendor/assets/jquery-ui-1.10.4.custom.min.css +7 -0
  24. data/examples/music/vendor/assets/jquery-ui-1.10.4.custom.min.js +6 -0
  25. data/examples/{sinatra → notes}/.gitignore +0 -0
  26. data/examples/{sinatra → notes}/Gemfile +0 -0
  27. data/examples/{sinatra → notes}/README.markdown +0 -0
  28. data/examples/{sinatra → notes}/application.rb +0 -0
  29. data/examples/{sinatra → notes}/config.ru +0 -0
  30. data/examples/{sinatra → notes}/test.rb +0 -0
  31. data/lib/elasticsearch/persistence.rb +19 -0
  32. data/lib/elasticsearch/persistence/model.rb +129 -0
  33. data/lib/elasticsearch/persistence/model/base.rb +75 -0
  34. data/lib/elasticsearch/persistence/model/errors.rb +8 -0
  35. data/lib/elasticsearch/persistence/model/find.rb +171 -0
  36. data/lib/elasticsearch/persistence/model/rails.rb +39 -0
  37. data/lib/elasticsearch/persistence/model/store.rb +239 -0
  38. data/lib/elasticsearch/persistence/model/utils.rb +0 -0
  39. data/lib/elasticsearch/persistence/repository.rb +3 -1
  40. data/lib/elasticsearch/persistence/repository/search.rb +25 -0
  41. data/lib/elasticsearch/persistence/version.rb +1 -1
  42. data/lib/rails/generators/elasticsearch/model/model_generator.rb +21 -0
  43. data/lib/rails/generators/elasticsearch/model/templates/model.rb.tt +9 -0
  44. data/lib/rails/generators/elasticsearch_generator.rb +2 -0
  45. data/test/integration/model/model_basic_test.rb +157 -0
  46. data/test/integration/repository/default_class_test.rb +6 -0
  47. data/test/unit/model_base_test.rb +40 -0
  48. data/test/unit/model_find_test.rb +147 -0
  49. data/test/unit/model_gateway_test.rb +99 -0
  50. data/test/unit/model_rails_test.rb +88 -0
  51. data/test/unit/model_store_test.rb +493 -0
  52. data/test/unit/repository_search_test.rb +17 -0
  53. metadata +79 -9
@@ -0,0 +1,239 @@
1
+ module Elasticsearch
2
+ module Persistence
3
+ module Model
4
+
5
+ # This module contains the storage related features of {Elasticsearch::Persistence::Model}
6
+ #
7
+ module Store
8
+ module ClassMethods #:nodoc:
9
+
10
+ # Creates a class instance, saves it, if validations pass, and returns it
11
+ #
12
+ # @example Create a new person
13
+ #
14
+ # Person.create name: 'John Smith'
15
+ # # => #<Person:0x007f889e302b30 ... @id="bG7yQDAXRhCi3ZfVcx6oAA", @name="John Smith" ...>
16
+ #
17
+ # @return [Object] The model instance
18
+ #
19
+ def create(attributes, options={})
20
+ object = self.new(attributes)
21
+ object.run_callbacks :create do
22
+ object.save(options)
23
+ object
24
+ end
25
+ end
26
+ end
27
+
28
+ module InstanceMethods
29
+
30
+ # Saves the model (if validations pass) and returns the response (or `false`)
31
+ #
32
+ # @example Save a valid model instance
33
+ #
34
+ # p = Person.new(name: 'John')
35
+ # p.save
36
+ # => {"_index"=>"people", ... "_id"=>"RzFSXFR0R8u1CZIWNs2Gvg", "_version"=>1, "created"=>true}
37
+ #
38
+ # @example Save an invalid model instance
39
+ #
40
+ # p = Person.new(name: nil)
41
+ # p.save
42
+ # # => false
43
+ #
44
+ # @return [Hash,FalseClass] The Elasticsearch response as a Hash or `false`
45
+ #
46
+ def save(options={})
47
+ return false unless valid?
48
+ run_callbacks :save do
49
+ options.update id: self.id
50
+ options.update index: self._index if self._index
51
+ options.update type: self._type if self._type
52
+
53
+ response = self.class.gateway.save(self, options)
54
+
55
+ self[:updated_at] = Time.now.utc
56
+
57
+ @_id = response['_id']
58
+ @_index = response['_index']
59
+ @_type = response['_type']
60
+ @_version = response['_version']
61
+ @persisted = true
62
+
63
+ response
64
+ end
65
+ end
66
+
67
+ # Deletes the model from Elasticsearch (if it's persisted), freezes it, and returns the response
68
+ #
69
+ # @example Delete a model instance
70
+ #
71
+ # p.destroy
72
+ # => {"_index"=>"people", ... "_id"=>"RzFSXFR0R8u1CZIWNs2Gvg", "_version"=>2 ...}
73
+ #
74
+ # @return [Hash] The Elasticsearch response as a Hash
75
+ #
76
+ def destroy(options={})
77
+ raise DocumentNotPersisted, "Object not persisted: #{self.inspect}" unless persisted?
78
+
79
+ run_callbacks :destroy do
80
+ options.update index: self._index if self._index
81
+ options.update type: self._type if self._type
82
+
83
+ response = self.class.gateway.delete(self.id, options)
84
+
85
+ @destroyed = true
86
+ @persisted = false
87
+ self.freeze
88
+ response
89
+ end
90
+ end; alias :delete :destroy
91
+
92
+ # Updates the model (via Elasticsearch's "Update" API) and returns the response
93
+ #
94
+ # @example Update a model with partial attributes
95
+ #
96
+ # p.update name: 'UPDATED'
97
+ # => {"_index"=>"people", ... "_version"=>2}
98
+ #
99
+ # @return [Hash] The Elasticsearch response as a Hash
100
+ #
101
+ def update(attributes={}, options={})
102
+ raise DocumentNotPersisted, "Object not persisted: #{self.inspect}" unless persisted?
103
+
104
+ run_callbacks :update do
105
+ options.update index: self._index if self._index
106
+ options.update type: self._type if self._type
107
+
108
+ attributes.update( { updated_at: Time.now.utc } )
109
+
110
+ response = self.class.gateway.update(self.id, { doc: attributes}.merge(options))
111
+
112
+ self.attributes = self.attributes.merge(attributes)
113
+ @_index = response['_index']
114
+ @_type = response['_type']
115
+ @_version = response['_version']
116
+
117
+ response
118
+ end
119
+ end; alias :update_attributes :update
120
+
121
+ # Increments a numeric attribute (via Elasticsearch's "Update" API) and returns the response
122
+ #
123
+ # @example Increment the `salary` attribute by 1
124
+ #
125
+ # p.increment :salary
126
+ #
127
+ # @example Increment the `salary` attribute by 100
128
+ #
129
+ # p.increment :salary, 100
130
+ #
131
+ # @return [Hash] The Elasticsearch response as a Hash
132
+ #
133
+ def increment(attribute, value=1, options={})
134
+ raise DocumentNotPersisted, "Object not persisted: #{self.inspect}" unless persisted?
135
+
136
+ options.update index: self._index if self._index
137
+ options.update type: self._type if self._type
138
+
139
+ response = self.class.gateway.update(self.id, { script: "ctx._source.#{attribute} += #{value}"}.merge(options))
140
+
141
+ self[attribute] += value
142
+
143
+ @_index = response['_index']
144
+ @_type = response['_type']
145
+ @_version = response['_version']
146
+
147
+ response
148
+ end
149
+
150
+ # Decrements a numeric attribute (via Elasticsearch's "Update" API) and returns the response
151
+ #
152
+ # @example Decrement the `salary` attribute by 1
153
+ #
154
+ # p.decrement :salary
155
+ #
156
+ # @example Decrement the `salary` attribute by 100
157
+ #
158
+ # p.decrement :salary, 100
159
+ #
160
+ # @return [Hash] The Elasticsearch response as a Hash
161
+ #
162
+ def decrement(attribute, value=1, options={})
163
+ raise DocumentNotPersisted, "Object not persisted: #{self.inspect}" unless persisted?
164
+
165
+ options.update index: self._index if self._index
166
+ options.update type: self._type if self._type
167
+
168
+ response = self.class.gateway.update(self.id, { script: "ctx._source.#{attribute} = ctx._source.#{attribute} - #{value}"}.merge(options))
169
+ self[attribute] -= value
170
+
171
+ @_index = response['_index']
172
+ @_type = response['_type']
173
+ @_version = response['_version']
174
+
175
+ response
176
+ end
177
+
178
+ # Updates the `updated_at` attribute, saves the model and returns the response
179
+ #
180
+ # @example Update the `updated_at` attribute (default)
181
+ #
182
+ # p.touch
183
+ #
184
+ # @example Update a custom attribute: `saved_on`
185
+ #
186
+ # p.touch :saved_on
187
+ #
188
+ # @return [Hash] The Elasticsearch response as a Hash
189
+ #
190
+ def touch(attribute=:updated_at, options={})
191
+ raise DocumentNotPersisted, "Object not persisted: #{self.inspect}" unless persisted?
192
+ raise ArgumentError, "Object does not have '#{attribute}' attribute" unless respond_to?(attribute)
193
+
194
+ run_callbacks :touch do
195
+ options.update index: self._index if self._index
196
+ options.update type: self._type if self._type
197
+
198
+ value = Time.now.utc
199
+ response = self.class.gateway.update(self.id, { doc: { attribute => value.iso8601 }}.merge(options))
200
+
201
+ self[attribute] = value
202
+
203
+ @_index = response['_index']
204
+ @_type = response['_type']
205
+ @_version = response['_version']
206
+
207
+ response
208
+ end
209
+ end
210
+
211
+ # Returns true when the model has been destroyed, false otherwise
212
+ #
213
+ # @return [TrueClass,FalseClass]
214
+ #
215
+ def destroyed?
216
+ !!@destroyed
217
+ end
218
+
219
+ # Returns true when the model has been already saved to the database, false otherwise
220
+ #
221
+ # @return [TrueClass,FalseClass]
222
+ #
223
+ def persisted?
224
+ !!@persisted && !destroyed?
225
+ end
226
+
227
+ # Returns true when the model has not been saved yet, false otherwise
228
+ #
229
+ # @return [TrueClass,FalseClass]
230
+ #
231
+ def new_record?
232
+ !persisted? && !destroyed?
233
+ end
234
+ end
235
+ end
236
+
237
+ end
238
+ end
239
+ end
File without changes
@@ -17,10 +17,12 @@ module Elasticsearch
17
17
  end
18
18
  end
19
19
 
20
- # When included, creates an instance of the {Repository::Class} class as a "gateway"
20
+ # When included, creates an instance of the {Repository::Class Repository} class as a "gateway"
21
21
  #
22
22
  # @example Include the repository in a custom class
23
23
  #
24
+ # require 'elasticsearch/persistence'
25
+ #
24
26
  # class MyRepository
25
27
  # include Elasticsearch::Persistence::Repository
26
28
  # end
@@ -53,6 +53,31 @@ module Elasticsearch
53
53
  end
54
54
  Response::Results.new(self, response)
55
55
  end
56
+
57
+ # Return the number of domain object in the index
58
+ #
59
+ # @example Return the number of all domain objects
60
+ #
61
+ # repository.count
62
+ # # => 2
63
+ #
64
+ # @example Return the count of domain object matching a simple query
65
+ #
66
+ # repository.count('fox or dog')
67
+ # # => 1
68
+ #
69
+ # @example Return the count of domain object matching a query in the Elasticsearch DSL
70
+ #
71
+ # repository.search(query: { match: { title: 'fox dog' } })
72
+ # # => 1
73
+ #
74
+ # @return [Integer]
75
+ #
76
+ def count(query_or_definition=nil, options={})
77
+ query_or_definition ||= { query: { match_all: {} } }
78
+ response = search query_or_definition, options.update(search_type: 'count')
79
+ response.response.hits.total
80
+ end
56
81
  end
57
82
 
58
83
  end
@@ -1,5 +1,5 @@
1
1
  module Elasticsearch
2
2
  module Persistence
3
- VERSION = "0.1.3"
3
+ VERSION = "0.1.4"
4
4
  end
5
5
  end
@@ -0,0 +1,21 @@
1
+ require "rails/generators/elasticsearch_generator"
2
+
3
+ module Elasticsearch
4
+ module Generators
5
+ class ModelGenerator < ::Rails::Generators::NamedBase
6
+ source_root File.expand_path('../templates', __FILE__)
7
+
8
+ desc "Creates an Elasticsearch::Persistence model"
9
+ argument :attributes, type: :array, default: [], banner: "attribute:type attribute:type"
10
+
11
+ check_class_collision
12
+
13
+ def create_model_file
14
+ @padding = attributes.map { |a| a.name.size }.max
15
+ template "model.rb.tt", File.join("app/models", class_path, "#{file_name}.rb")
16
+ end
17
+
18
+ hook_for :test_framework
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,9 @@
1
+ <% module_namespacing do -%>
2
+ class <%= class_name %>
3
+ include Elasticsearch::Persistence::Model
4
+
5
+ <% attributes.each do |attribute| -%>
6
+ <%= "attribute :#{attribute.name},".ljust(@padding+12) %> <%= attribute.type %>
7
+ <% end -%>
8
+ end
9
+ <% end -%>
@@ -0,0 +1,2 @@
1
+ require "rails/generators/named_base"
2
+ require "rails/generators/active_model"
@@ -0,0 +1,157 @@
1
+ require 'test_helper'
2
+
3
+ require 'elasticsearch/persistence/model'
4
+
5
+ module Elasticsearch
6
+ module Persistence
7
+ class PersistenceModelBasicIntegrationTest < Elasticsearch::Test::IntegrationTestCase
8
+
9
+ class ::Person
10
+ include Elasticsearch::Persistence::Model
11
+
12
+ settings index: { number_of_shards: 1 }
13
+
14
+ attribute :name, String,
15
+ mapping: { fields: {
16
+ name: { type: 'string', analyzer: 'snowball' },
17
+ raw: { type: 'string', analyzer: 'keyword' }
18
+ } }
19
+
20
+ attribute :birthday, Date
21
+ attribute :department, String
22
+ attribute :salary, Integer
23
+ attribute :admin, Boolean, default: false
24
+
25
+ validates :name, presence: true
26
+ end
27
+
28
+ context "A basic persistence model" do
29
+ setup do
30
+ Person.create_index! force: true
31
+ end
32
+
33
+ should "save the object with custom ID" do
34
+ person = Person.new id: 1, name: 'Number One'
35
+ person.save
36
+
37
+ document = Person.find(1)
38
+ assert_not_nil document
39
+ assert_equal 'Number One', document.name
40
+ end
41
+
42
+ should "create the object with custom ID" do
43
+ person = Person.create id: 1, name: 'Number One'
44
+
45
+ document = Person.find(1)
46
+ assert_not_nil document
47
+ assert_equal 'Number One', document.name
48
+ end
49
+
50
+ should "save and find the object" do
51
+ person = Person.new name: 'John Smith', birthday: Date.parse('1970-01-01')
52
+ person.save
53
+
54
+ assert_not_nil person.id
55
+ document = Person.find(person.id)
56
+
57
+ assert_instance_of Person, document
58
+ assert_equal 'John Smith', document.name
59
+ assert_equal 'John Smith', Person.find(person.id).name
60
+
61
+ assert_not_nil Elasticsearch::Persistence.client.get index: 'people', type: 'person', id: person.id
62
+ end
63
+
64
+ should "delete the object" do
65
+ person = Person.create name: 'John Smith', birthday: Date.parse('1970-01-01')
66
+
67
+ person.destroy
68
+ assert person.frozen?
69
+
70
+ assert_raise Elasticsearch::Transport::Transport::Errors::NotFound do
71
+ Elasticsearch::Persistence.client.get index: 'people', type: 'person', id: person.id
72
+ end
73
+ end
74
+
75
+ should "update an object attribute" do
76
+ person = Person.create name: 'John Smith'
77
+
78
+ person.update name: 'UPDATED'
79
+
80
+ assert_equal 'UPDATED', person.name
81
+ assert_equal 'UPDATED', Person.find(person.id).name
82
+ end
83
+
84
+ should "increment an object attribute" do
85
+ person = Person.create name: 'John Smith', salary: 1_000
86
+
87
+ person.increment :salary
88
+
89
+ assert_equal 1_001, person.salary
90
+ assert_equal 1_001, Person.find(person.id).salary
91
+ end
92
+
93
+ should "update the object timestamp" do
94
+ person = Person.create name: 'John Smith'
95
+ updated_at = person.updated_at
96
+
97
+ sleep 1
98
+ person.touch
99
+
100
+ assert person.updated_at > updated_at, [person.updated_at, updated_at].inspect
101
+
102
+ found = Person.find(person.id)
103
+ assert found.updated_at > updated_at, [found.updated_at, updated_at].inspect
104
+ end
105
+
106
+ should "find instances by search" do
107
+ Person.create name: 'John Smith'
108
+ Person.create name: 'Mary Smith'
109
+ Person.gateway.refresh_index!
110
+
111
+ people = Person.search query: { match: { name: 'smith' } },
112
+ highlight: { fields: { name: {} } }
113
+
114
+ assert_equal 2, people.total
115
+ assert_equal 2, people.size
116
+
117
+ assert people.map_with_hit { |o,h| h._score }.all? { |s| s > 0 }
118
+
119
+ assert_not_nil people.first.hit
120
+ assert_match /smith/i, people.first.hit.highlight['name'].first
121
+ end
122
+
123
+ should "find instances in batches" do
124
+ 50.times { |i| Person.create name: "John #{i+1}" }
125
+ Person.gateway.refresh_index!
126
+
127
+ @batches = 0
128
+ @results = []
129
+
130
+ Person.find_in_batches(_source_include: 'name') do |batch|
131
+ @batches += 1
132
+ @results += batch.map(&:name)
133
+ end
134
+
135
+ assert_equal 3, @batches
136
+ assert_equal 50, @results.size
137
+ assert_contains @results, 'John 1'
138
+ end
139
+
140
+ should "find each instance" do
141
+ 50.times { |i| Person.create name: "John #{i+1}" }
142
+ Person.gateway.refresh_index!
143
+
144
+ @results = []
145
+
146
+ Person.find_each(_source_include: 'name') do |person|
147
+ @results << person.name
148
+ end
149
+
150
+ assert_equal 50, @results.size
151
+ assert_contains @results, 'John 1'
152
+ end
153
+ end
154
+
155
+ end
156
+ end
157
+ end