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.
Files changed (99) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +17 -0
  3. data/CHANGELOG.md +16 -0
  4. data/Gemfile +4 -0
  5. data/LICENSE.txt +13 -0
  6. data/README.md +678 -0
  7. data/Rakefile +57 -0
  8. data/elasticsearch-persistence.gemspec +57 -0
  9. data/examples/music/album.rb +34 -0
  10. data/examples/music/artist.rb +50 -0
  11. data/examples/music/artists/_form.html.erb +8 -0
  12. data/examples/music/artists/artists_controller.rb +67 -0
  13. data/examples/music/artists/artists_controller_test.rb +53 -0
  14. data/examples/music/artists/index.html.erb +57 -0
  15. data/examples/music/artists/show.html.erb +51 -0
  16. data/examples/music/assets/application.css +226 -0
  17. data/examples/music/assets/autocomplete.css +48 -0
  18. data/examples/music/assets/blank_cover.png +0 -0
  19. data/examples/music/assets/form.css +113 -0
  20. data/examples/music/index_manager.rb +60 -0
  21. data/examples/music/search/index.html.erb +93 -0
  22. data/examples/music/search/search_controller.rb +41 -0
  23. data/examples/music/search/search_controller_test.rb +9 -0
  24. data/examples/music/search/search_helper.rb +15 -0
  25. data/examples/music/suggester.rb +45 -0
  26. data/examples/music/template.rb +392 -0
  27. data/examples/music/vendor/assets/jquery-ui-1.10.4.custom.min.css +7 -0
  28. data/examples/music/vendor/assets/jquery-ui-1.10.4.custom.min.js +6 -0
  29. data/examples/notes/.gitignore +7 -0
  30. data/examples/notes/Gemfile +28 -0
  31. data/examples/notes/README.markdown +36 -0
  32. data/examples/notes/application.rb +238 -0
  33. data/examples/notes/config.ru +7 -0
  34. data/examples/notes/test.rb +118 -0
  35. data/lib/elasticsearch/per_thread_registry.rb +53 -0
  36. data/lib/elasticsearch/persistence/client.rb +51 -0
  37. data/lib/elasticsearch/persistence/inheritence.rb +9 -0
  38. data/lib/elasticsearch/persistence/model/base.rb +95 -0
  39. data/lib/elasticsearch/persistence/model/callbacks.rb +37 -0
  40. data/lib/elasticsearch/persistence/model/errors.rb +9 -0
  41. data/lib/elasticsearch/persistence/model/find.rb +155 -0
  42. data/lib/elasticsearch/persistence/model/gateway_delegation.rb +23 -0
  43. data/lib/elasticsearch/persistence/model/hash_wrapper.rb +17 -0
  44. data/lib/elasticsearch/persistence/model/rails.rb +39 -0
  45. data/lib/elasticsearch/persistence/model/store.rb +271 -0
  46. data/lib/elasticsearch/persistence/model.rb +148 -0
  47. data/lib/elasticsearch/persistence/null_relation.rb +56 -0
  48. data/lib/elasticsearch/persistence/query_cache.rb +68 -0
  49. data/lib/elasticsearch/persistence/querying.rb +21 -0
  50. data/lib/elasticsearch/persistence/relation/delegation.rb +130 -0
  51. data/lib/elasticsearch/persistence/relation/finder_methods.rb +39 -0
  52. data/lib/elasticsearch/persistence/relation/merger.rb +179 -0
  53. data/lib/elasticsearch/persistence/relation/query_builder.rb +279 -0
  54. data/lib/elasticsearch/persistence/relation/query_methods.rb +362 -0
  55. data/lib/elasticsearch/persistence/relation/search_option_methods.rb +44 -0
  56. data/lib/elasticsearch/persistence/relation/spawn_methods.rb +61 -0
  57. data/lib/elasticsearch/persistence/relation.rb +110 -0
  58. data/lib/elasticsearch/persistence/repository/class.rb +71 -0
  59. data/lib/elasticsearch/persistence/repository/find.rb +73 -0
  60. data/lib/elasticsearch/persistence/repository/naming.rb +115 -0
  61. data/lib/elasticsearch/persistence/repository/response/results.rb +105 -0
  62. data/lib/elasticsearch/persistence/repository/search.rb +156 -0
  63. data/lib/elasticsearch/persistence/repository/serialize.rb +31 -0
  64. data/lib/elasticsearch/persistence/repository/store.rb +94 -0
  65. data/lib/elasticsearch/persistence/repository.rb +77 -0
  66. data/lib/elasticsearch/persistence/scoping/default.rb +137 -0
  67. data/lib/elasticsearch/persistence/scoping/named.rb +70 -0
  68. data/lib/elasticsearch/persistence/scoping.rb +52 -0
  69. data/lib/elasticsearch/persistence/version.rb +5 -0
  70. data/lib/elasticsearch/persistence.rb +157 -0
  71. data/lib/elasticsearch/rails_compatibility.rb +17 -0
  72. data/lib/rails/generators/elasticsearch/model/model_generator.rb +21 -0
  73. data/lib/rails/generators/elasticsearch/model/templates/model.rb.tt +9 -0
  74. data/lib/rails/generators/elasticsearch_generator.rb +2 -0
  75. data/lib/rails/instrumentation/railtie.rb +31 -0
  76. data/lib/rails/instrumentation.rb +10 -0
  77. data/test/integration/model/model_basic_test.rb +157 -0
  78. data/test/integration/repository/custom_class_test.rb +85 -0
  79. data/test/integration/repository/customized_class_test.rb +82 -0
  80. data/test/integration/repository/default_class_test.rb +114 -0
  81. data/test/integration/repository/virtus_model_test.rb +114 -0
  82. data/test/test_helper.rb +53 -0
  83. data/test/unit/model_base_test.rb +48 -0
  84. data/test/unit/model_find_test.rb +148 -0
  85. data/test/unit/model_gateway_test.rb +99 -0
  86. data/test/unit/model_rails_test.rb +88 -0
  87. data/test/unit/model_store_test.rb +514 -0
  88. data/test/unit/persistence_test.rb +32 -0
  89. data/test/unit/repository_class_test.rb +51 -0
  90. data/test/unit/repository_client_test.rb +32 -0
  91. data/test/unit/repository_find_test.rb +388 -0
  92. data/test/unit/repository_indexing_test.rb +37 -0
  93. data/test/unit/repository_module_test.rb +146 -0
  94. data/test/unit/repository_naming_test.rb +146 -0
  95. data/test/unit/repository_response_results_test.rb +98 -0
  96. data/test/unit/repository_search_test.rb +117 -0
  97. data/test/unit/repository_serialize_test.rb +57 -0
  98. data/test/unit/repository_store_test.rb +303 -0
  99. 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