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.
- checksums.yaml +8 -8
- data/CHANGELOG.md +4 -0
- data/README.md +238 -7
- data/elasticsearch-persistence.gemspec +4 -1
- data/examples/music/album.rb +34 -0
- data/examples/music/artist.rb +50 -0
- data/examples/music/artists/_form.html.erb +8 -0
- data/examples/music/artists/artists_controller.rb +67 -0
- data/examples/music/artists/artists_controller_test.rb +53 -0
- data/examples/music/artists/index.html.erb +57 -0
- data/examples/music/artists/show.html.erb +51 -0
- data/examples/music/assets/application.css +226 -0
- data/examples/music/assets/autocomplete.css +48 -0
- data/examples/music/assets/blank_cover.png +0 -0
- data/examples/music/assets/form.css +113 -0
- data/examples/music/index_manager.rb +60 -0
- data/examples/music/search/index.html.erb +93 -0
- data/examples/music/search/search_controller.rb +41 -0
- data/examples/music/search/search_controller_test.rb +9 -0
- data/examples/music/search/search_helper.rb +15 -0
- data/examples/music/suggester.rb +45 -0
- data/examples/music/template.rb +392 -0
- data/examples/music/vendor/assets/jquery-ui-1.10.4.custom.min.css +7 -0
- data/examples/music/vendor/assets/jquery-ui-1.10.4.custom.min.js +6 -0
- data/examples/{sinatra → notes}/.gitignore +0 -0
- data/examples/{sinatra → notes}/Gemfile +0 -0
- data/examples/{sinatra → notes}/README.markdown +0 -0
- data/examples/{sinatra → notes}/application.rb +0 -0
- data/examples/{sinatra → notes}/config.ru +0 -0
- data/examples/{sinatra → notes}/test.rb +0 -0
- data/lib/elasticsearch/persistence.rb +19 -0
- data/lib/elasticsearch/persistence/model.rb +129 -0
- data/lib/elasticsearch/persistence/model/base.rb +75 -0
- data/lib/elasticsearch/persistence/model/errors.rb +8 -0
- data/lib/elasticsearch/persistence/model/find.rb +171 -0
- data/lib/elasticsearch/persistence/model/rails.rb +39 -0
- data/lib/elasticsearch/persistence/model/store.rb +239 -0
- data/lib/elasticsearch/persistence/model/utils.rb +0 -0
- data/lib/elasticsearch/persistence/repository.rb +3 -1
- data/lib/elasticsearch/persistence/repository/search.rb +25 -0
- data/lib/elasticsearch/persistence/version.rb +1 -1
- data/lib/rails/generators/elasticsearch/model/model_generator.rb +21 -0
- data/lib/rails/generators/elasticsearch/model/templates/model.rb.tt +9 -0
- data/lib/rails/generators/elasticsearch_generator.rb +2 -0
- data/test/integration/model/model_basic_test.rb +157 -0
- data/test/integration/repository/default_class_test.rb +6 -0
- data/test/unit/model_base_test.rb +40 -0
- data/test/unit/model_find_test.rb +147 -0
- data/test/unit/model_gateway_test.rb +99 -0
- data/test/unit/model_rails_test.rb +88 -0
- data/test/unit/model_store_test.rb +493 -0
- data/test/unit/repository_search_test.rb +17 -0
- 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
|
@@ -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,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
|