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
data/README.md ADDED
@@ -0,0 +1,678 @@
1
+ # Elasticsearch::Persistence
2
+
3
+ Persistence layer for Ruby domain objects in Elasticsearch, using the Repository and ActiveRecord patterns.
4
+
5
+ The library is compatible with Ruby 1.9.3 (or higher) and Elasticsearch 1.0 (or higher).
6
+
7
+ ## Installation
8
+
9
+ Install the package from [Rubygems](https://rubygems.org):
10
+
11
+ gem install elasticsearch-persistence
12
+
13
+ To use an unreleased version, either add it to your `Gemfile` for [Bundler](http://bundler.io):
14
+
15
+ gem 'elasticsearch-persistence', git: 'git://github.com/elasticsearch/elasticsearch-rails.git'
16
+
17
+ or install it from a source code checkout:
18
+
19
+ git clone https://github.com/elasticsearch/elasticsearch-rails.git
20
+ cd elasticsearch-rails/elasticsearch-persistence
21
+ bundle install
22
+ rake install
23
+
24
+ ## Usage
25
+
26
+ ### The Repository Pattern
27
+
28
+ The `Elasticsearch::Persistence::Repository` module provides an implementation of the
29
+ [repository pattern](http://martinfowler.com/eaaCatalog/repository.html) and allows
30
+ to save, delete, find and search objects stored in Elasticsearch, as well as configure
31
+ mappings and settings for the index.
32
+
33
+ Let's have a simple plain old Ruby object (PORO):
34
+
35
+ ```ruby
36
+ class Note
37
+ attr_reader :attributes
38
+
39
+ def initialize(attributes={})
40
+ @attributes = attributes
41
+ end
42
+
43
+ def to_hash
44
+ @attributes
45
+ end
46
+ end
47
+ ```
48
+
49
+ Let's create a default, "dumb" repository, as a first step:
50
+
51
+ ```ruby
52
+ require 'elasticsearch/persistence'
53
+ repository = Elasticsearch::Persistence::Repository.new
54
+ ```
55
+
56
+ We can save a `Note` instance into the repository...
57
+
58
+ ```ruby
59
+ note = Note.new id: 1, text: 'Test'
60
+
61
+ repository.save(note)
62
+ # PUT http://localhost:9200/repository/note/1 [status:201, request:0.210s, query:n/a]
63
+ # > {"id":1,"text":"Test"}
64
+ # < {"_index":"repository","_type":"note","_id":"1","_version":1,"created":true}
65
+ ```
66
+
67
+ ...find it...
68
+
69
+ ```ruby
70
+ n = repository.find(1)
71
+ # GET http://localhost:9200/repository/_all/1 [status:200, request:0.003s, query:n/a]
72
+ # < {"_index":"repository","_type":"note","_id":"1","_version":2,"found":true, "_source" : {"id":1,"text":"Test"}}
73
+ => <Note:0x007fcbfc0c4980 @attributes={"id"=>1, "text"=>"Test"}>
74
+ ```
75
+
76
+ ...search for it...
77
+
78
+ ```ruby
79
+ repository.search(query: { match: { text: 'test' } }).first
80
+ # GET http://localhost:9200/repository/_search [status:200, request:0.005s, query:0.002s]
81
+ # > {"query":{"match":{"text":"test"}}}
82
+ # < {"took":2, ... "hits":{"total":1, ... "hits":[{ ... "_source" : {"id":1,"text":"Test"}}]}}
83
+ => <Note:0x007fcbfc1c7b70 @attributes={"id"=>1, "text"=>"Test"}>
84
+ ```
85
+
86
+ ...or delete it:
87
+
88
+ ```ruby
89
+ repository.delete(note)
90
+ # DELETE http://localhost:9200/repository/note/1 [status:200, request:0.014s, query:n/a]
91
+ # < {"found":true,"_index":"repository","_type":"note","_id":"1","_version":3}
92
+ => {"found"=>true, "_index"=>"repository", "_type"=>"note", "_id"=>"1", "_version"=>2}
93
+ ```
94
+
95
+ The repository module provides a number of features and facilities to configure and customize the behaviour:
96
+
97
+ * Configuring the Elasticsearch [client](https://github.com/elasticsearch/elasticsearch-ruby#usage) being used
98
+ * Setting the index name, document type, and object class for deserialization
99
+ * Composing mappings and settings for the index
100
+ * Creating, deleting or refreshing the index
101
+ * Finding or searching for documents
102
+ * Providing access both to domain objects and hits for search results
103
+ * Providing access to the Elasticsearch response for search results (aggregations, total, ...)
104
+ * Defining the methods for serialization and deserialization
105
+
106
+ You can use the default repository class, or include the module in your own. Let's review it in detail.
107
+
108
+ #### The Default Class
109
+
110
+ For simple cases, you can use the default, bundled repository class, and configure/customize it:
111
+
112
+ ```ruby
113
+ repository = Elasticsearch::Persistence::Repository.new do
114
+ # Configure the Elasticsearch client
115
+ client Elasticsearch::Client.new url: ENV['ELASTICSEARCH_URL'], log: true
116
+
117
+ # Set a custom index name
118
+ index :my_notes
119
+
120
+ # Set a custom document type
121
+ type :my_note
122
+
123
+ # Specify the class to inicialize when deserializing documents
124
+ klass Note
125
+
126
+ # Configure the settings and mappings for the Elasticsearch index
127
+ settings number_of_shards: 1 do
128
+ mapping do
129
+ indexes :text, analyzer: 'snowball'
130
+ end
131
+ end
132
+
133
+ # Customize the serialization logic
134
+ def serialize(document)
135
+ super.merge(my_special_key: 'my_special_stuff')
136
+ end
137
+
138
+ # Customize the de-serialization logic
139
+ def deserialize(document)
140
+ puts "# ***** CUSTOM DESERIALIZE LOGIC KICKING IN... *****"
141
+ super
142
+ end
143
+ end
144
+ ```
145
+
146
+ The custom Elasticsearch client will be used now, with a custom index and type names,
147
+ as well as the custom serialization and de-serialization logic.
148
+
149
+ We can create the index with the desired settings and mappings:
150
+
151
+ ```ruby
152
+ repository.create_index! force: true
153
+ # PUT http://localhost:9200/my_notes
154
+ # > {"settings":{"number_of_shards":1},"mappings":{ ... {"text":{"analyzer":"snowball","type":"string"}}}}}
155
+ ```
156
+
157
+ Save the document with extra properties added by the `serialize` method:
158
+
159
+ ```ruby
160
+ repository.save(note)
161
+ # PUT http://localhost:9200/my_notes/my_note/1
162
+ # > {"id":1,"text":"Test","my_special_key":"my_special_stuff"}
163
+ {"_index"=>"my_notes", "_type"=>"my_note", "_id"=>"1", "_version"=>4, ... }
164
+ ```
165
+
166
+ And `deserialize` it:
167
+
168
+ ```ruby
169
+ repository.find(1)
170
+ # ***** CUSTOM DESERIALIZE LOGIC KICKING IN... *****
171
+ <Note:0x007f9bd782b7a0 @attributes={... "my_special_key"=>"my_special_stuff"}>
172
+ ```
173
+
174
+ #### A Custom Class
175
+
176
+ In most cases, though, you'll want to use a custom class for the repository, so let's do that:
177
+
178
+ ```ruby
179
+ require 'base64'
180
+
181
+ class NoteRepository
182
+ include Elasticsearch::Persistence::Repository
183
+
184
+ def initialize(options={})
185
+ index options[:index] || 'notes'
186
+ client Elasticsearch::Client.new url: options[:url], log: options[:log]
187
+ end
188
+
189
+ klass Note
190
+
191
+ settings number_of_shards: 1 do
192
+ mapping do
193
+ indexes :text, analyzer: 'snowball'
194
+ # Do not index images
195
+ indexes :image, index: 'no'
196
+ end
197
+ end
198
+
199
+ # Base64 encode the "image" field in the document
200
+ #
201
+ def serialize(document)
202
+ hash = document.to_hash.clone
203
+ hash['image'] = Base64.encode64(hash['image']) if hash['image']
204
+ hash.to_hash
205
+ end
206
+
207
+ # Base64 decode the "image" field in the document
208
+ #
209
+ def deserialize(document)
210
+ hash = document['_source']
211
+ hash['image'] = Base64.decode64(hash['image']) if hash['image']
212
+ klass.new hash
213
+ end
214
+ end
215
+ ```
216
+
217
+ Include the `Elasticsearch::Persistence::Repository` module to add the repository methods into the class.
218
+
219
+ You can customize the repository in the familiar way, by calling the DSL-like methods.
220
+
221
+ You can implement a custom initializer for your repository, add complex logic in its
222
+ class and instance methods -- in general, have all the freedom of a standard Ruby class.
223
+
224
+ ```ruby
225
+ repository = NoteRepository.new url: 'http://localhost:9200', log: true
226
+
227
+ # Configure the repository instance
228
+ repository.index = 'notes_development'
229
+ repository.client.transport.logger.formatter = proc { |s, d, p, m| "\e[2m# #{m}\n\e[0m" }
230
+
231
+ repository.create_index! force: true
232
+
233
+ note = Note.new 'id' => 1, 'text' => 'Document with image', 'image' => '... BINARY DATA ...'
234
+
235
+ repository.save(note)
236
+ # PUT http://localhost:9200/notes_development/note/1
237
+ # > {"id":1,"text":"Document with image","image":"Li4uIEJJTkFSWSBEQVRBIC4uLg==\n"}
238
+ puts repository.find(1).attributes['image']
239
+ # GET http://localhost:9200/notes_development/note/1
240
+ # < {... "_source" : { ... "image":"Li4uIEJJTkFSWSBEQVRBIC4uLg==\n"}}
241
+ # => ... BINARY DATA ...
242
+ ```
243
+
244
+ #### Methods Provided by the Repository
245
+
246
+ ##### Client
247
+
248
+ The repository uses the standard Elasticsearch [client](https://github.com/elasticsearch/elasticsearch-ruby#usage),
249
+ which is accessible with the `client` getter and setter methods:
250
+
251
+ ```ruby
252
+ repository.client = Elasticsearch::Client.new url: 'http://search.server.org'
253
+ repository.client.transport.logger = Logger.new(STDERR)
254
+ ```
255
+
256
+ ##### Naming
257
+
258
+ The `index` method specifies the Elasticsearch index to use for storage, lookup and search
259
+ (when not set, the value is inferred from the repository class name):
260
+
261
+ ```ruby
262
+ repository.index = 'notes_development'
263
+ ```
264
+
265
+ The `type` method specifies the Elasticsearch document type to use for storage, lookup and search
266
+ (when not set, the value is inferred from the document class name, or `_all` is used):
267
+
268
+ ```ruby
269
+ repository.type = 'my_note'
270
+ ```
271
+
272
+ The `klass` method specifies the Ruby class name to use when initializing objects from
273
+ documents retrieved from the repository (when not set, the value is inferred from the
274
+ document `_type` as fetched from Elasticsearch):
275
+
276
+ ```ruby
277
+ repository.klass = MyNote
278
+ ```
279
+
280
+ ##### Index Configuration
281
+
282
+ The `settings` and `mappings` methods, provided by the
283
+ [`elasticsearch-model`](http://rubydoc.info/gems/elasticsearch-model/Elasticsearch/Model/Indexing/ClassMethods)
284
+ gem, allow to configure the index properties:
285
+
286
+ ```ruby
287
+ repository.settings number_of_shards: 1
288
+ repository.settings.to_hash
289
+ # => {:number_of_shards=>1}
290
+
291
+ repository.mappings { indexes :title, analyzer: 'snowball' }
292
+ repository.mappings.to_hash
293
+ # => { :note => {:properties=> ... }}
294
+ ```
295
+
296
+ The convenience methods `create_index!`, `delete_index!` and `refresh_index!` allow you to manage the index lifecycle.
297
+
298
+ ##### Serialization
299
+
300
+ The `serialize` and `deserialize` methods allow you to customize the serialization of the document when passing it
301
+ to the storage, and the initialization procedure when loading it from the storage:
302
+
303
+ ```ruby
304
+ class NoteRepository
305
+ def serialize(document)
306
+ Hash[document.to_hash.map() { |k,v| v.upcase! if k == :title; [k,v] }]
307
+ end
308
+ def deserialize(document)
309
+ MyNote.new ActiveSupport::HashWithIndifferentAccess.new(document['_source']).deep_symbolize_keys
310
+ end
311
+ end
312
+ ```
313
+
314
+ ##### Storage
315
+
316
+ The `save` method allows you to store a domain object in the repository:
317
+
318
+ ```ruby
319
+ note = Note.new id: 1, title: 'Quick Brown Fox'
320
+ repository.save(note)
321
+ # => {"_index"=>"notes_development", "_type"=>"my_note", "_id"=>"1", "_version"=>1, "created"=>true}
322
+ ```
323
+
324
+ The `update` method allows you to perform a partial update of a document in the repository.
325
+ Use either a partial document:
326
+
327
+ ```ruby
328
+ repository.update id: 1, title: 'UPDATED', tags: []
329
+ # => {"_index"=>"notes_development", "_type"=>"note", "_id"=>"1", "_version"=>2}
330
+ ```
331
+
332
+ Or a script (optionally with parameters):
333
+
334
+ ```ruby
335
+ repository.update 1, script: 'if (!ctx._source.tags.contains(t)) { ctx._source.tags += t }', params: { t: 'foo' }
336
+ # => {"_index"=>"notes_development", "_type"=>"note", "_id"=>"1", "_version"=>3}
337
+ ```
338
+
339
+
340
+ The `delete` method allows to remove objects from the repository (pass either the object itself or its ID):
341
+
342
+ ```ruby
343
+ repository.delete(note)
344
+ repository.delete(1)
345
+ ```
346
+
347
+ ##### Finding
348
+
349
+ The `find` method allows to find one or many documents in the storage and returns them as deserialized Ruby objects:
350
+
351
+ ```ruby
352
+ repository.save Note.new(id: 2, title: 'Fast White Dog')
353
+
354
+ note = repository.find(1)
355
+ # => <MyNote ... QUICK BROWN FOX>
356
+
357
+ notes = repository.find(1, 2)
358
+ # => [<MyNote... QUICK BROWN FOX>, <MyNote ... FAST WHITE DOG>]
359
+ ```
360
+
361
+ When the document with a specific ID isn't found, a `nil` is returned instead of the deserialized object:
362
+
363
+ ```ruby
364
+ notes = repository.find(1, 3, 2)
365
+ # => [<MyNote ...>, nil, <MyNote ...>]
366
+ ```
367
+
368
+ Handle the missing objects in the application code, or call `compact` on the result.
369
+
370
+ ##### Search
371
+
372
+ The `search` method to retrieve objects from the repository by a query string or definition in the Elasticsearch DSL:
373
+
374
+ ```ruby
375
+ repository.search('fox or dog').to_a
376
+ # GET http://localhost:9200/notes_development/my_note/_search?q=fox
377
+ # => [<MyNote ... FOX ...>, <MyNote ... DOG ...>]
378
+
379
+ repository.search(query: { match: { title: 'fox dog' } }).to_a
380
+ # GET http://localhost:9200/notes_development/my_note/_search
381
+ # > {"query":{"match":{"title":"fox dog"}}}
382
+ # => [<MyNote ... FOX ...>, <MyNote ... DOG ...>]
383
+ ```
384
+
385
+ The returned object is an instance of the `Elasticsearch::Persistence::Repository::Response::Results` class,
386
+ which provides access to the results, the full returned response and hits.
387
+
388
+ ```ruby
389
+ results = repository.search(query: { match: { title: 'fox dog' } })
390
+
391
+ # Iterate over the objects
392
+ #
393
+ results.each do |note|
394
+ puts "* #{note.attributes[:title]}"
395
+ end
396
+ # * QUICK BROWN FOX
397
+ # * FAST WHITE DOG
398
+
399
+ # Iterate over the objects and hits
400
+ #
401
+ results.each_with_hit do |note, hit|
402
+ puts "* #{note.attributes[:title]}, score: #{hit._score}"
403
+ end
404
+ # * QUICK BROWN FOX, score: 0.29930896
405
+ # * FAST WHITE DOG, score: 0.29930896
406
+
407
+ # Get total results
408
+ #
409
+ results.total
410
+ # => 2
411
+
412
+ # Access the raw response as a Hashie::Mash instance
413
+ results.response._shards.failed
414
+ # => 0
415
+ ```
416
+
417
+ #### Example Application
418
+
419
+ An example Sinatra application is available in [`examples/notes/application.rb`](examples/notes/application.rb),
420
+ and demonstrates a rich set of features:
421
+
422
+ * How to create and configure a custom repository class
423
+ * How to work with a plain Ruby class as the domain object
424
+ * How to integrate the repository with a Sinatra application
425
+ * How to write complex search definitions, including pagination, highlighting and aggregations
426
+ * How to use search results in the application view
427
+
428
+ ### The ActiveRecord Pattern
429
+
430
+ The `Elasticsearch::Persistence::Model` module provides an implementation of the
431
+ active record [pattern](http://www.martinfowler.com/eaaCatalog/activeRecord.html),
432
+ with a familiar interface for using Elasticsearch as a persistence layer in
433
+ Ruby on Rails applications.
434
+
435
+ All the methods are documented with comprehensive examples in the source code,
436
+ available also online at <http://rubydoc.info/gems/elasticsearch-persistence/Elasticsearch/Persistence/Model>.
437
+
438
+ #### Installation/Usage
439
+
440
+ To use the library in a Rails application, add it to your `Gemfile` with a `require` statement:
441
+
442
+ ```ruby
443
+ gem "elasticsearch-persistence", require: 'elasticsearch/persistence/model'
444
+ ```
445
+
446
+ To use the library without Bundler, install it, and require the file:
447
+
448
+ ```bash
449
+ gem install elasticsearch-persistence
450
+ ```
451
+
452
+ ```ruby
453
+ # In your code
454
+ require 'elasticsearch/persistence/model'
455
+ ```
456
+
457
+ #### Model Definition
458
+
459
+ The integration is implemented by including the module in a Ruby class.
460
+ The model attribute definition support is implemented with the
461
+ [_Virtus_](https://github.com/solnic/virtus) Rubygem, and the
462
+ naming, validation, etc. features with the
463
+ [_ActiveModel_](https://github.com/rails/rails/tree/master/activemodel) Rubygem.
464
+
465
+ ```ruby
466
+ class Article
467
+ include Elasticsearch::Persistence::Model
468
+
469
+ # Define a plain `title` attribute
470
+ #
471
+ attribute :title, String
472
+
473
+ # Define an `author` attribute, with multiple analyzers for this field
474
+ #
475
+ attribute :author, String, mapping: { fields: {
476
+ author: { type: 'string'},
477
+ raw: { type: 'string', analyzer: 'keyword' }
478
+ } }
479
+
480
+
481
+ # Define a `views` attribute, with default value
482
+ #
483
+ attribute :views, Integer, default: 0, mapping: { type: 'integer' }
484
+
485
+ # Validate the presence of the `title` attribute
486
+ #
487
+ validates :title, presence: true
488
+
489
+ # Execute code after saving the model.
490
+ #
491
+ after_save { puts "Successfuly saved: #{self}" }
492
+ end
493
+ ```
494
+
495
+ Attribute validations work like for any other _ActiveModel_-compatible implementation:
496
+
497
+ ```ruby
498
+ article = Article.new # => #<Article { ... }>
499
+
500
+ article.valid?
501
+ # => false
502
+
503
+ article.errors.to_a
504
+ # => ["Title can't be blank"]
505
+ ```
506
+
507
+ #### Persistence
508
+
509
+ We can create a new article in the database...
510
+
511
+ ```ruby
512
+ Article.create id: 1, title: 'Test', author: 'John'
513
+ # PUT http://localhost:9200/articles/article/1 [status:201, request:0.015s, query:n/a]
514
+ ```
515
+
516
+ ... and find it:
517
+
518
+ ```ruby
519
+ article = Article.find(1)
520
+ # => #<Article { ... }>
521
+
522
+ article._index
523
+ # => "articles"
524
+
525
+ article.id
526
+ # => "1"
527
+
528
+ article.title
529
+ # => "Test"
530
+ ```
531
+
532
+ To update the model, either update the attribute and save the model:
533
+
534
+ ```ruby
535
+ article.title = 'Updated'
536
+
537
+ article.save
538
+ # => {"_index"=>"articles", "_type"=>"article", "_id"=>"1", "_version"=>2, "created"=>false}
539
+ ```
540
+
541
+ ... or use the `update_attributes` method:
542
+
543
+ ```ruby
544
+ article.update_attributes title: 'Test', author: 'Mary'
545
+ # => {"_index"=>"articles", "_type"=>"article", "_id"=>"1", "_version"=>3}
546
+ ```
547
+
548
+ The implementation supports the familiar interface for updating model timestamps:
549
+
550
+ ```ruby
551
+ article.touch
552
+ # => => { ... "_version"=>4}
553
+ ```
554
+
555
+ ... and numeric attributes:
556
+
557
+ ```ruby
558
+ article.views
559
+ # => 0
560
+
561
+ article.increment :views
562
+ article.views
563
+ # => 1
564
+ ```
565
+
566
+ Any callbacks defined in the model will be triggered during the persistence operations:
567
+
568
+ ```ruby
569
+ article.save
570
+ # Successfuly saved: #<Article {...}>
571
+ ```
572
+
573
+ The model also supports familiar `find_in_batches` and `find_each` methods to efficiently
574
+ retrieve big collections of model instances, using the Elasticsearch's _Scan API_:
575
+
576
+ ```ruby
577
+ Article.find_each(_source_include: 'title') { |a| puts "===> #{a.title.upcase}" }
578
+ # GET http://localhost:9200/articles/article/_search?scroll=5m&search_type=scan&size=20
579
+ # GET http://localhost:9200/_search/scroll?scroll=5m&scroll_id=c2Nhb...
580
+ # ===> TEST
581
+ # GET http://localhost:9200/_search/scroll?scroll=5m&scroll_id=c2Nhb...
582
+ # => "c2Nhb..."
583
+ ```
584
+
585
+ #### Search
586
+
587
+ The model class provides a `search` method to retrieve model instances with a regular
588
+ search definition, including highlighting, aggregations, etc:
589
+
590
+ ```ruby
591
+ results = Article.search query: { match: { title: 'test' } },
592
+ aggregations: { authors: { terms: { field: 'author.raw' } } },
593
+ highlight: { fields: { title: {} } }
594
+
595
+ puts results.first.title
596
+ # Test
597
+
598
+ puts results.first.hit.highlight['title']
599
+ # <em>Test</em>
600
+
601
+ puts results.response.aggregations.authors.buckets.each { |b| puts "#{b['key']} : #{b['doc_count']}" }
602
+ # John : 1
603
+ ```
604
+
605
+ #### Accessing the Repository Gateway
606
+
607
+ The integration with Elasticsearch is implemented by embedding the repository object in the model.
608
+ You can access it through the `gateway` method:
609
+
610
+ ```ruby
611
+ Artist.gateway.client.info
612
+ # GET http://localhost:9200/ [status:200, request:0.011s, query:n/a]
613
+ # => {"status"=>200, "name"=>"Lightspeed", ...}
614
+ ```
615
+
616
+ #### Rails Compatibility
617
+
618
+ The model instances are fully compatible with Rails' conventions and helpers:
619
+
620
+ ```ruby
621
+ url_for article
622
+ # => "http://localhost:3000/articles/1"
623
+
624
+ div_for article
625
+ # => '<div class="article" id="article_1"></div>'
626
+ ```
627
+
628
+ ... as well as form values for dates and times:
629
+
630
+ ```ruby
631
+ article = Article.new "title" => "Date", "published(1i)"=>"2014", "published(2i)"=>"1", "published(3i)"=>"1"
632
+
633
+ article.published.iso8601
634
+ # => "2014-01-01"
635
+ ```
636
+
637
+ The library provides a Rails ORM generator to facilitate building the application scaffolding:
638
+
639
+ ```bash
640
+ rails generate scaffold Person name:String email:String birthday:Date --orm=elasticsearch
641
+ ```
642
+
643
+ #### Example application
644
+
645
+ A fully working Ruby on Rails application can be generated with the following command:
646
+
647
+ ```bash
648
+ rails new music --force --skip --skip-bundle --skip-active-record --template https://raw.githubusercontent.com/elasticsearch/elasticsearch-rails/master/elasticsearch-persistence/examples/music/template.rb
649
+ ```
650
+
651
+ The application demonstrates:
652
+
653
+ * How to set up model attributes with custom mappings
654
+ * How to define model relationships with Elasticsearch's parent/child
655
+ * How to configure models to use a common index, and create the index with proper mappings
656
+ * How to use Elasticsearch's completion suggester to drive auto-complete functionality
657
+ * How to use Elasticsearch-persisted models in Rails' views and forms
658
+ * How to write controller tests
659
+
660
+ The source files for the application are available in the [`examples/music`](examples/music) folder.
661
+
662
+ ## License
663
+
664
+ This software is licensed under the Apache 2 license, quoted below.
665
+
666
+ Copyright (c) 2014 Elasticsearch <http://www.elasticsearch.org>
667
+
668
+ Licensed under the Apache License, Version 2.0 (the "License");
669
+ you may not use this file except in compliance with the License.
670
+ You may obtain a copy of the License at
671
+
672
+ http://www.apache.org/licenses/LICENSE-2.0
673
+
674
+ Unless required by applicable law or agreed to in writing, software
675
+ distributed under the License is distributed on an "AS IS" BASIS,
676
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
677
+ See the License for the specific language governing permissions and
678
+ limitations under the License.