elasticsearch-persistence-queryable 0.1.8
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 +7 -0
- data/.gitignore +17 -0
- data/CHANGELOG.md +16 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +13 -0
- data/README.md +678 -0
- data/Rakefile +57 -0
- data/elasticsearch-persistence.gemspec +57 -0
- 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/notes/.gitignore +7 -0
- data/examples/notes/Gemfile +28 -0
- data/examples/notes/README.markdown +36 -0
- data/examples/notes/application.rb +238 -0
- data/examples/notes/config.ru +7 -0
- data/examples/notes/test.rb +118 -0
- data/lib/elasticsearch/per_thread_registry.rb +53 -0
- data/lib/elasticsearch/persistence/client.rb +51 -0
- data/lib/elasticsearch/persistence/inheritence.rb +9 -0
- data/lib/elasticsearch/persistence/model/base.rb +95 -0
- data/lib/elasticsearch/persistence/model/callbacks.rb +37 -0
- data/lib/elasticsearch/persistence/model/errors.rb +9 -0
- data/lib/elasticsearch/persistence/model/find.rb +155 -0
- data/lib/elasticsearch/persistence/model/gateway_delegation.rb +23 -0
- data/lib/elasticsearch/persistence/model/hash_wrapper.rb +17 -0
- data/lib/elasticsearch/persistence/model/rails.rb +39 -0
- data/lib/elasticsearch/persistence/model/store.rb +271 -0
- data/lib/elasticsearch/persistence/model.rb +148 -0
- data/lib/elasticsearch/persistence/null_relation.rb +56 -0
- data/lib/elasticsearch/persistence/query_cache.rb +68 -0
- data/lib/elasticsearch/persistence/querying.rb +21 -0
- data/lib/elasticsearch/persistence/relation/delegation.rb +130 -0
- data/lib/elasticsearch/persistence/relation/finder_methods.rb +39 -0
- data/lib/elasticsearch/persistence/relation/merger.rb +179 -0
- data/lib/elasticsearch/persistence/relation/query_builder.rb +279 -0
- data/lib/elasticsearch/persistence/relation/query_methods.rb +362 -0
- data/lib/elasticsearch/persistence/relation/search_option_methods.rb +44 -0
- data/lib/elasticsearch/persistence/relation/spawn_methods.rb +61 -0
- data/lib/elasticsearch/persistence/relation.rb +110 -0
- data/lib/elasticsearch/persistence/repository/class.rb +71 -0
- data/lib/elasticsearch/persistence/repository/find.rb +73 -0
- data/lib/elasticsearch/persistence/repository/naming.rb +115 -0
- data/lib/elasticsearch/persistence/repository/response/results.rb +105 -0
- data/lib/elasticsearch/persistence/repository/search.rb +156 -0
- data/lib/elasticsearch/persistence/repository/serialize.rb +31 -0
- data/lib/elasticsearch/persistence/repository/store.rb +94 -0
- data/lib/elasticsearch/persistence/repository.rb +77 -0
- data/lib/elasticsearch/persistence/scoping/default.rb +137 -0
- data/lib/elasticsearch/persistence/scoping/named.rb +70 -0
- data/lib/elasticsearch/persistence/scoping.rb +52 -0
- data/lib/elasticsearch/persistence/version.rb +5 -0
- data/lib/elasticsearch/persistence.rb +157 -0
- data/lib/elasticsearch/rails_compatibility.rb +17 -0
- 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/lib/rails/instrumentation/railtie.rb +31 -0
- data/lib/rails/instrumentation.rb +10 -0
- data/test/integration/model/model_basic_test.rb +157 -0
- data/test/integration/repository/custom_class_test.rb +85 -0
- data/test/integration/repository/customized_class_test.rb +82 -0
- data/test/integration/repository/default_class_test.rb +114 -0
- data/test/integration/repository/virtus_model_test.rb +114 -0
- data/test/test_helper.rb +53 -0
- data/test/unit/model_base_test.rb +48 -0
- data/test/unit/model_find_test.rb +148 -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 +514 -0
- data/test/unit/persistence_test.rb +32 -0
- data/test/unit/repository_class_test.rb +51 -0
- data/test/unit/repository_client_test.rb +32 -0
- data/test/unit/repository_find_test.rb +388 -0
- data/test/unit/repository_indexing_test.rb +37 -0
- data/test/unit/repository_module_test.rb +146 -0
- data/test/unit/repository_naming_test.rb +146 -0
- data/test/unit/repository_response_results_test.rb +98 -0
- data/test/unit/repository_search_test.rb +117 -0
- data/test/unit/repository_serialize_test.rb +57 -0
- data/test/unit/repository_store_test.rb +303 -0
- metadata +487 -0
@@ -0,0 +1,155 @@
|
|
1
|
+
module Elasticsearch
|
2
|
+
module Persistence
|
3
|
+
module Model
|
4
|
+
module Find
|
5
|
+
module ClassMethods
|
6
|
+
|
7
|
+
# Returns the number of models
|
8
|
+
#
|
9
|
+
# @example Return the count of all models
|
10
|
+
#
|
11
|
+
# Person.count
|
12
|
+
# # => 2
|
13
|
+
#
|
14
|
+
# @example Return the count of models matching a simple query
|
15
|
+
#
|
16
|
+
# Person.count('fox or dog')
|
17
|
+
# # => 1
|
18
|
+
#
|
19
|
+
# @example Return the count of models matching a query in the Elasticsearch DSL
|
20
|
+
#
|
21
|
+
# Person.search(query: { match: { title: 'fox dog' } })
|
22
|
+
# # => 1
|
23
|
+
#
|
24
|
+
# @return [Integer]
|
25
|
+
#
|
26
|
+
def count(query_or_definition = nil, options = {})
|
27
|
+
gateway.count(query_or_definition, options)
|
28
|
+
end
|
29
|
+
|
30
|
+
# Returns all models efficiently via the Elasticsearch's scan/scroll API
|
31
|
+
#
|
32
|
+
# You can restrict the models being returned with a query.
|
33
|
+
#
|
34
|
+
# The {http://rubydoc.info/gems/elasticsearch-api/Elasticsearch/API/Actions#search-instance_method Search API}
|
35
|
+
# options are passed to the search method as parameters, all remaining options are passed
|
36
|
+
# as the `:body` parameter.
|
37
|
+
#
|
38
|
+
# The full {Persistence::Repository::Response::Results} instance is yielded to the passed
|
39
|
+
# block in each batch, so you can access any of its properties; calling `to_a` will
|
40
|
+
# convert the object to an Array of model instances.
|
41
|
+
#
|
42
|
+
# @example Return all models in batches of 20 x number of primary shards
|
43
|
+
#
|
44
|
+
# Person.find_in_batches { |batch| puts batch.map(&:name) }
|
45
|
+
#
|
46
|
+
# @example Return all models in batches of 100 x number of primary shards
|
47
|
+
#
|
48
|
+
# Person.find_in_batches(size: 100) { |batch| puts batch.map(&:name) }
|
49
|
+
#
|
50
|
+
# @example Return all models matching a specific query
|
51
|
+
#
|
52
|
+
# Person.find_in_batches(query: { match: { name: 'test' } }) { |batch| puts batch.map(&:name) }
|
53
|
+
#
|
54
|
+
# @example Return all models, fetching only the `name` attribute from Elasticsearch
|
55
|
+
#
|
56
|
+
# Person.find_in_batches( _source_include: 'name') { |_| puts _.response.hits.hits.map(&:to_hash) }
|
57
|
+
#
|
58
|
+
# @example Leave out the block to return an Enumerator instance
|
59
|
+
#
|
60
|
+
# Person.find_in_batches(size: 100).map { |batch| batch.size }
|
61
|
+
# # => [100, 100, 100, ... ]
|
62
|
+
#
|
63
|
+
# @return [String,Enumerator] The `scroll_id` for the request or Enumerator when the block is not passed
|
64
|
+
#
|
65
|
+
def find_in_batches(options = {}, &block)
|
66
|
+
return to_enum(:find_in_batches, options) unless block_given?
|
67
|
+
|
68
|
+
search_params = options.slice(
|
69
|
+
:index,
|
70
|
+
:type,
|
71
|
+
:scroll,
|
72
|
+
:size,
|
73
|
+
:explain,
|
74
|
+
:ignore_indices,
|
75
|
+
:ignore_unavailable,
|
76
|
+
:allow_no_indices,
|
77
|
+
:expand_wildcards,
|
78
|
+
:preference,
|
79
|
+
:q,
|
80
|
+
:routing,
|
81
|
+
:source,
|
82
|
+
:_source,
|
83
|
+
:_source_include,
|
84
|
+
:_source_exclude,
|
85
|
+
:stats,
|
86
|
+
:timeout
|
87
|
+
)
|
88
|
+
|
89
|
+
scroll = search_params.delete(:scroll) || "5m"
|
90
|
+
|
91
|
+
body = options
|
92
|
+
|
93
|
+
puts "BODY: #{body}".color :red
|
94
|
+
# Get the initial scroll_id
|
95
|
+
#
|
96
|
+
response = gateway.client.search({ index: gateway.index_name,
|
97
|
+
type: gateway.document_type,
|
98
|
+
search_type: "scan",
|
99
|
+
scroll: scroll,
|
100
|
+
size: 20,
|
101
|
+
body: body }.merge(search_params))
|
102
|
+
|
103
|
+
# Get the initial batch of documents
|
104
|
+
#
|
105
|
+
response = gateway.client.scroll({ scroll_id: response["_scroll_id"], scroll: scroll })
|
106
|
+
|
107
|
+
# Break when receiving an empty array of hits
|
108
|
+
#
|
109
|
+
while response["hits"]["hits"].any?
|
110
|
+
yield Repository::Response::Results.new(gateway, response)
|
111
|
+
|
112
|
+
response = gateway.client.scroll({ scroll_id: response["_scroll_id"], scroll: scroll })
|
113
|
+
end
|
114
|
+
|
115
|
+
return response["_scroll_id"]
|
116
|
+
end
|
117
|
+
|
118
|
+
# Iterate effectively over models using the `find_in_batches` method.
|
119
|
+
#
|
120
|
+
# All the options are passed to `find_in_batches` and each result is yielded to the passed block.
|
121
|
+
#
|
122
|
+
# @example Print out the people's names by scrolling through the index
|
123
|
+
#
|
124
|
+
# Person.find_each { |person| puts person.name }
|
125
|
+
#
|
126
|
+
# # # GET http://localhost:9200/people/person/_search?scroll=5m&search_type=scan&size=20
|
127
|
+
# # # GET http://localhost:9200/_search/scroll?scroll=5m&scroll_id=c2Nhbj...
|
128
|
+
# # Test 0
|
129
|
+
# # Test 1
|
130
|
+
# # Test 2
|
131
|
+
# # ...
|
132
|
+
# # # GET http://localhost:9200/_search/scroll?scroll=5m&scroll_id=c2Nhbj...
|
133
|
+
# # Test 20
|
134
|
+
# # Test 21
|
135
|
+
# # Test 22
|
136
|
+
#
|
137
|
+
# @example Leave out the block to return an Enumerator instance
|
138
|
+
#
|
139
|
+
# Person.find_each.select { |person| person.name =~ /John/ }
|
140
|
+
# # => => [#<Person {id: "NkltJP5vRxqk9_RMP7SU8Q", name: "John Smith", ...}>]
|
141
|
+
#
|
142
|
+
# @return [String,Enumerator] The `scroll_id` for the request or Enumerator when the block is not passed
|
143
|
+
#
|
144
|
+
def find_each(options = {})
|
145
|
+
return to_enum(:find_each, options) unless block_given?
|
146
|
+
|
147
|
+
find_in_batches(options) do |batch|
|
148
|
+
batch.each { |result| yield result }
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Elasticsearch
|
2
|
+
module Persistence
|
3
|
+
module Model
|
4
|
+
module GatewayDelegation
|
5
|
+
delegate :settings,
|
6
|
+
:mappings,
|
7
|
+
:mapping,
|
8
|
+
:document_type,
|
9
|
+
:document_type=,
|
10
|
+
:index_name,
|
11
|
+
:index_name=,
|
12
|
+
:search,
|
13
|
+
:find,
|
14
|
+
:exists?,
|
15
|
+
:create_index!,
|
16
|
+
:delete_index!,
|
17
|
+
:index_exists?,
|
18
|
+
:refresh_index!,
|
19
|
+
to: :gateway
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Elasticsearch
|
2
|
+
module Persistence
|
3
|
+
module Model
|
4
|
+
|
5
|
+
# Subclass of `Hashie::Mash` to wrap Hash-like structures
|
6
|
+
# (responses from Elasticsearch, search definitions, etc)
|
7
|
+
#
|
8
|
+
# The primary goal of the subclass is to disable the
|
9
|
+
# warning being printed by Hashie for re-defined
|
10
|
+
# methods, such as `sort`.
|
11
|
+
#
|
12
|
+
class HashWrapper < ::Hashie::Mash
|
13
|
+
disable_warnings if respond_to?(:disable_warnings)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module Elasticsearch
|
2
|
+
module Persistence
|
3
|
+
module Model
|
4
|
+
|
5
|
+
# Make the `Persistence::Model` models compatible with Ruby On Rails applications
|
6
|
+
#
|
7
|
+
module Rails
|
8
|
+
def self.included(base)
|
9
|
+
base.class_eval do
|
10
|
+
|
11
|
+
# Decorates the passed in `attributes` so they extract the date & time values from Rails forms
|
12
|
+
#
|
13
|
+
# @example Correctly combine the date and time to a datetime string
|
14
|
+
#
|
15
|
+
# params = { "published_on(1i)"=>"2014",
|
16
|
+
# "published_on(2i)"=>"1",
|
17
|
+
# "published_on(3i)"=>"1",
|
18
|
+
# "published_on(4i)"=>"12",
|
19
|
+
# "published_on(5i)"=>"00"
|
20
|
+
# }
|
21
|
+
# MyRailsModel.new(params).published_on.iso8601
|
22
|
+
# # => "2014-01-01T12:00:00+00:00"
|
23
|
+
#
|
24
|
+
def initialize(attributes={})
|
25
|
+
day = attributes.select { |p| p =~ /\([1-3]/ }.reduce({}) { |sum, item| (sum[item.first.gsub(/\(.+\)/, '')] ||= '' )<< item.last+'-'; sum }
|
26
|
+
time = attributes.select { |p| p =~ /\([4-6]/ }.reduce({}) { |sum, item| (sum[item.first.gsub(/\(.+\)/, '')] ||= '' )<< item.last+':'; sum }
|
27
|
+
unless day.empty?
|
28
|
+
attributes.update day.reduce({}) { |sum, item| sum[item.first] = item.last; sum[item.first] += ' ' + time[item.first] unless time.empty?; sum }
|
29
|
+
end
|
30
|
+
|
31
|
+
super(attributes)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,271 @@
|
|
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.save(options)
|
22
|
+
object
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
module InstanceMethods
|
27
|
+
|
28
|
+
# Saves the model (if validations pass) and returns the response (or `false`)
|
29
|
+
#
|
30
|
+
# @example Save a valid model instance
|
31
|
+
#
|
32
|
+
# p = Person.new(name: 'John')
|
33
|
+
# p.save
|
34
|
+
# => {"_index"=>"people", ... "_id"=>"RzFSXFR0R8u1CZIWNs2Gvg", "_version"=>1, "created"=>true}
|
35
|
+
#
|
36
|
+
# @example Save an invalid model instance
|
37
|
+
#
|
38
|
+
# p = Person.new(name: nil)
|
39
|
+
# p.save
|
40
|
+
# # => false
|
41
|
+
#
|
42
|
+
# @return [Hash,FalseClass] The Elasticsearch response as a Hash or `false`
|
43
|
+
#
|
44
|
+
def save(options = {})
|
45
|
+
return false unless valid?
|
46
|
+
|
47
|
+
run_callbacks :save do
|
48
|
+
options.update id: self.id
|
49
|
+
options.update index: self._index if self._index
|
50
|
+
options.update type: self._type if self._type
|
51
|
+
|
52
|
+
if new_record?
|
53
|
+
response = run_callbacks :create do
|
54
|
+
response = self.class.gateway.save(self, options)
|
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
|
+
else
|
66
|
+
response = self.class.gateway.save(self, options)
|
67
|
+
|
68
|
+
self[:updated_at] = Time.now.utc
|
69
|
+
|
70
|
+
@_id = response["_id"]
|
71
|
+
@_index = response["_index"]
|
72
|
+
@_type = response["_type"]
|
73
|
+
@_version = response["_version"]
|
74
|
+
@persisted = true
|
75
|
+
|
76
|
+
response
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
# Deletes the model from Elasticsearch (if it's persisted), freezes it, and returns the response
|
82
|
+
#
|
83
|
+
# @example Delete a model instance
|
84
|
+
#
|
85
|
+
# p.destroy
|
86
|
+
# => {"_index"=>"people", ... "_id"=>"RzFSXFR0R8u1CZIWNs2Gvg", "_version"=>2 ...}
|
87
|
+
#
|
88
|
+
# @return [Hash] The Elasticsearch response as a Hash
|
89
|
+
#
|
90
|
+
def destroy(options = {})
|
91
|
+
raise DocumentNotPersisted, "Object not persisted: #{self.inspect}" unless persisted?
|
92
|
+
|
93
|
+
run_callbacks :destroy do
|
94
|
+
options.update index: self._index if self._index
|
95
|
+
options.update type: self._type if self._type
|
96
|
+
|
97
|
+
response = self.class.gateway.delete(self.id, options)
|
98
|
+
|
99
|
+
@destroyed = true
|
100
|
+
@persisted = false
|
101
|
+
self.freeze
|
102
|
+
response
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
alias :delete :destroy
|
107
|
+
|
108
|
+
# Updates the model (via Elasticsearch's "Update" API) and returns the response
|
109
|
+
#
|
110
|
+
# @example Update a model with partial attributes
|
111
|
+
#
|
112
|
+
# p.update name: 'UPDATED'
|
113
|
+
# => {"_index"=>"people", ... "_version"=>2}
|
114
|
+
#
|
115
|
+
# @return [Hash] The Elasticsearch response as a Hash
|
116
|
+
#
|
117
|
+
def update(attributes = {}, options = {})
|
118
|
+
raise DocumentNotPersisted, "Object not persisted: #{self.inspect}" unless persisted?
|
119
|
+
|
120
|
+
run_callbacks :update do
|
121
|
+
options.update index: self._index if self._index
|
122
|
+
options.update type: self._type if self._type
|
123
|
+
|
124
|
+
attributes.update({ updated_at: Time.now.utc })
|
125
|
+
|
126
|
+
response = self.class.gateway.update(self.id, { doc: attributes }.merge(options))
|
127
|
+
|
128
|
+
self.attributes = self.attributes.merge(attributes)
|
129
|
+
@_index = response["_index"]
|
130
|
+
@_type = response["_type"]
|
131
|
+
@_version = response["_version"]
|
132
|
+
|
133
|
+
response
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
alias :update_attributes :update
|
138
|
+
|
139
|
+
# Increments a numeric attribute (via Elasticsearch's "Update" API) and returns the response
|
140
|
+
#
|
141
|
+
# @example Increment the `salary` attribute by 1
|
142
|
+
#
|
143
|
+
# p.increment :salary
|
144
|
+
#
|
145
|
+
# @example Increment the `salary` attribute by 100
|
146
|
+
#
|
147
|
+
# p.increment :salary, 100
|
148
|
+
#
|
149
|
+
# @return [Hash] The Elasticsearch response as a Hash
|
150
|
+
#
|
151
|
+
def increment(attribute, value = 1, options = {})
|
152
|
+
raise DocumentNotPersisted, "Object not persisted: #{self.inspect}" unless persisted?
|
153
|
+
|
154
|
+
options.update index: self._index if self._index
|
155
|
+
options.update type: self._type if self._type
|
156
|
+
|
157
|
+
response = self.class.gateway.update(self.id, { script: "ctx._source.#{attribute} += #{value}" }.merge(options))
|
158
|
+
|
159
|
+
self[attribute] += value
|
160
|
+
|
161
|
+
@_index = response["_index"]
|
162
|
+
@_type = response["_type"]
|
163
|
+
@_version = response["_version"]
|
164
|
+
|
165
|
+
response
|
166
|
+
end
|
167
|
+
|
168
|
+
# Decrements a numeric attribute (via Elasticsearch's "Update" API) and returns the response
|
169
|
+
#
|
170
|
+
# @example Decrement the `salary` attribute by 1
|
171
|
+
#
|
172
|
+
# p.decrement :salary
|
173
|
+
#
|
174
|
+
# @example Decrement the `salary` attribute by 100
|
175
|
+
#
|
176
|
+
# p.decrement :salary, 100
|
177
|
+
#
|
178
|
+
# @return [Hash] The Elasticsearch response as a Hash
|
179
|
+
#
|
180
|
+
def decrement(attribute, value = 1, options = {})
|
181
|
+
raise DocumentNotPersisted, "Object not persisted: #{self.inspect}" unless persisted?
|
182
|
+
|
183
|
+
options.update index: self._index if self._index
|
184
|
+
options.update type: self._type if self._type
|
185
|
+
|
186
|
+
response = self.class.gateway.update(self.id, { script: "ctx._source.#{attribute} = ctx._source.#{attribute} - #{value}" }.merge(options))
|
187
|
+
self[attribute] -= value
|
188
|
+
|
189
|
+
@_index = response["_index"]
|
190
|
+
@_type = response["_type"]
|
191
|
+
@_version = response["_version"]
|
192
|
+
|
193
|
+
response
|
194
|
+
end
|
195
|
+
|
196
|
+
# Updates the `updated_at` attribute, saves the model and returns the response
|
197
|
+
#
|
198
|
+
# @example Update the `updated_at` attribute (default)
|
199
|
+
#
|
200
|
+
# p.touch
|
201
|
+
#
|
202
|
+
# @example Update a custom attribute: `saved_on`
|
203
|
+
#
|
204
|
+
# p.touch :saved_on
|
205
|
+
#
|
206
|
+
# @return [Hash] The Elasticsearch response as a Hash
|
207
|
+
#
|
208
|
+
def touch(attribute = :updated_at, options = {})
|
209
|
+
raise DocumentNotPersisted, "Object not persisted: #{self.inspect}" unless persisted?
|
210
|
+
raise ArgumentError, "Object does not have '#{attribute}' attribute" unless respond_to?(attribute)
|
211
|
+
|
212
|
+
run_callbacks :touch do
|
213
|
+
options.update index: self._index if self._index
|
214
|
+
options.update type: self._type if self._type
|
215
|
+
|
216
|
+
value = Time.now.utc
|
217
|
+
response = self.class.gateway.update(self.id, { doc: { attribute => value.iso8601 } }.merge(options))
|
218
|
+
|
219
|
+
self[attribute] = value
|
220
|
+
|
221
|
+
@_index = response["_index"]
|
222
|
+
@_type = response["_type"]
|
223
|
+
@_version = response["_version"]
|
224
|
+
|
225
|
+
response
|
226
|
+
end
|
227
|
+
end
|
228
|
+
|
229
|
+
# Returns true when the model has been destroyed, false otherwise
|
230
|
+
#
|
231
|
+
# @return [TrueClass,FalseClass]
|
232
|
+
#
|
233
|
+
def destroyed?
|
234
|
+
!!@destroyed
|
235
|
+
end
|
236
|
+
|
237
|
+
# Returns true when the model has been already saved to the database, false otherwise
|
238
|
+
#
|
239
|
+
# @return [TrueClass,FalseClass]
|
240
|
+
#
|
241
|
+
def persisted?
|
242
|
+
!!@persisted && !destroyed?
|
243
|
+
end
|
244
|
+
|
245
|
+
# Returns true when the model has not been saved yet, false otherwise
|
246
|
+
#
|
247
|
+
# @return [TrueClass,FalseClass]
|
248
|
+
#
|
249
|
+
def new_record?
|
250
|
+
!persisted? && !destroyed?
|
251
|
+
end
|
252
|
+
|
253
|
+
def becomes(klass)
|
254
|
+
became = klass.new(attributes)
|
255
|
+
changed_attributes = @changed_attributes if defined?(@changed_attributes)
|
256
|
+
became.instance_variable_set("@changed_attributes", changed_attributes || {})
|
257
|
+
became.instance_variable_set("@new_record", new_record?)
|
258
|
+
became.instance_variable_set("@destroyed", destroyed?)
|
259
|
+
became.instance_variable_set("@errors", errors)
|
260
|
+
became.instance_variable_set("@persisted", persisted?)
|
261
|
+
became.instance_variable_set("@_id", _id)
|
262
|
+
became.instance_variable_set("@_version", _version)
|
263
|
+
became.instance_variable_set("@_index", _index)
|
264
|
+
became.instance_variable_set("@_type", _type)
|
265
|
+
became
|
266
|
+
end
|
267
|
+
end
|
268
|
+
end
|
269
|
+
end
|
270
|
+
end
|
271
|
+
end
|
@@ -0,0 +1,148 @@
|
|
1
|
+
require "active_support/core_ext/module/delegation"
|
2
|
+
|
3
|
+
require "active_model"
|
4
|
+
require "virtus"
|
5
|
+
|
6
|
+
#require 'elasticsearch/persistence'
|
7
|
+
|
8
|
+
require "elasticsearch/persistence/model/base"
|
9
|
+
require "elasticsearch/persistence/model/callbacks"
|
10
|
+
require "elasticsearch/persistence/model/errors"
|
11
|
+
require "elasticsearch/persistence/model/store"
|
12
|
+
require "elasticsearch/persistence/model/find"
|
13
|
+
require "elasticsearch/persistence/model/hash_wrapper"
|
14
|
+
|
15
|
+
module Elasticsearch
|
16
|
+
module Persistence
|
17
|
+
|
18
|
+
# When included, extends a plain Ruby class with persistence-related features via the ActiveRecord pattern
|
19
|
+
#
|
20
|
+
# @example Include the repository in a custom class
|
21
|
+
#
|
22
|
+
# require 'elasticsearch/persistence/model'
|
23
|
+
#
|
24
|
+
# class MyObject
|
25
|
+
# include Elasticsearch::Persistence::Repository
|
26
|
+
# end
|
27
|
+
#
|
28
|
+
module Model
|
29
|
+
def self.included(base)
|
30
|
+
base.class_eval do
|
31
|
+
include ActiveModel::Naming
|
32
|
+
include ActiveModel::Conversion
|
33
|
+
include ActiveModel::Serialization
|
34
|
+
include ActiveModel::Serializers::JSON
|
35
|
+
include ActiveModel::Validations
|
36
|
+
include ActiveModel::Validations::Callbacks
|
37
|
+
|
38
|
+
include Virtus.model
|
39
|
+
extend ActiveModel::Callbacks
|
40
|
+
|
41
|
+
define_model_callbacks :create, :save, :update, :destroy
|
42
|
+
define_model_callbacks :find, :touch, only: :after
|
43
|
+
|
44
|
+
include Elasticsearch::Persistence::Model::Callbacks
|
45
|
+
|
46
|
+
include Elasticsearch::Persistence::Model::Base::InstanceMethods
|
47
|
+
|
48
|
+
extend Elasticsearch::Persistence::Model::Store::ClassMethods
|
49
|
+
include Elasticsearch::Persistence::Model::Store::InstanceMethods
|
50
|
+
|
51
|
+
extend Elasticsearch::Persistence::Model::GatewayDelegation
|
52
|
+
|
53
|
+
extend Elasticsearch::Persistence::Model::Find::ClassMethods
|
54
|
+
extend Elasticsearch::Persistence::Querying
|
55
|
+
extend Elasticsearch::Persistence::Inheritence
|
56
|
+
extend Elasticsearch::Persistence::Delegation::DelegateCache
|
57
|
+
|
58
|
+
include Elasticsearch::Persistence::Scoping
|
59
|
+
|
60
|
+
class << self
|
61
|
+
|
62
|
+
# Re-define the Virtus' `attribute` method, to configure Elasticsearch mapping as well
|
63
|
+
#
|
64
|
+
def attribute(name, type = nil, options = {}, &block)
|
65
|
+
mapping = options.delete(:mapping) || {}
|
66
|
+
|
67
|
+
if type == :keyword || type.nil?
|
68
|
+
type = String
|
69
|
+
mapping = { type: "keyword" }.merge(mapping)
|
70
|
+
end
|
71
|
+
|
72
|
+
super
|
73
|
+
|
74
|
+
gateway.mapping do
|
75
|
+
indexes name, { type: Utils::lookup_type(type) }.merge(mapping)
|
76
|
+
end
|
77
|
+
|
78
|
+
gateway.mapping(&block) if block_given?
|
79
|
+
end
|
80
|
+
|
81
|
+
# Return the {Repository::Class} instance
|
82
|
+
#
|
83
|
+
def gateway(&block)
|
84
|
+
@gateway ||= Elasticsearch::Persistence::Repository::Class.new host: self
|
85
|
+
block.arity < 1 ? @gateway.instance_eval(&block) : block.call(@gateway) if block_given?
|
86
|
+
@gateway
|
87
|
+
end
|
88
|
+
|
89
|
+
# Set the default sort key to be used in sort operations
|
90
|
+
#
|
91
|
+
def default_sort_key(field = nil)
|
92
|
+
@default_sort_key = field unless field.nil?
|
93
|
+
@default_sort_key
|
94
|
+
end
|
95
|
+
|
96
|
+
private
|
97
|
+
|
98
|
+
# Return a Relation instance to chain queries
|
99
|
+
#
|
100
|
+
def relation
|
101
|
+
Relation.create(self, {})
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
# Configure the repository based on the model (set up index_name, etc)
|
106
|
+
#
|
107
|
+
gateway do
|
108
|
+
klass base
|
109
|
+
index_name base.model_name.collection.gsub(/\//, "-")
|
110
|
+
document_type base.model_name.element
|
111
|
+
|
112
|
+
def serialize(document)
|
113
|
+
document.to_hash.except(:id, "id")
|
114
|
+
end
|
115
|
+
|
116
|
+
def deserialize(document)
|
117
|
+
object = klass.new document["_source"] || document["fields"]
|
118
|
+
|
119
|
+
# Set the meta attributes when fetching the document from Elasticsearch
|
120
|
+
#
|
121
|
+
object.instance_variable_set :@_id, document["_id"]
|
122
|
+
object.instance_variable_set :@_index, document["_index"]
|
123
|
+
object.instance_variable_set :@_type, document["_type"]
|
124
|
+
object.instance_variable_set :@_version, document["_version"]
|
125
|
+
|
126
|
+
# Store the "hit" information (highlighting, score, ...)
|
127
|
+
#
|
128
|
+
object.instance_variable_set :@hit,
|
129
|
+
HashWrapper.new(document.except("_index", "_type", "_id", "_version", "_source"))
|
130
|
+
|
131
|
+
object.instance_variable_set(:@persisted, true)
|
132
|
+
object
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
# Set up common attributes
|
137
|
+
#
|
138
|
+
attribute :created_at, DateTime, default: lambda { |o, a| Time.now.utc }
|
139
|
+
attribute :updated_at, DateTime, default: lambda { |o, a| Time.now.utc }
|
140
|
+
|
141
|
+
default_sort_key :created_at
|
142
|
+
|
143
|
+
attr_reader :hit
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|