elasticsearch-persistence-queryable 0.1.8

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