elasticsearch-persistence 0.1.3 → 0.1.4

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