elasticsearch-persistence-queryable 0.1.8
Sign up to get free protection for your applications and to get access to all the features.
- 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
|