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
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.