elasticsearch-model-queryable 0.1.5

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 (70) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +20 -0
  3. data/CHANGELOG.md +26 -0
  4. data/Gemfile +4 -0
  5. data/LICENSE.txt +13 -0
  6. data/README.md +695 -0
  7. data/Rakefile +59 -0
  8. data/elasticsearch-model.gemspec +57 -0
  9. data/examples/activerecord_article.rb +77 -0
  10. data/examples/activerecord_associations.rb +162 -0
  11. data/examples/couchbase_article.rb +66 -0
  12. data/examples/datamapper_article.rb +71 -0
  13. data/examples/mongoid_article.rb +68 -0
  14. data/examples/ohm_article.rb +70 -0
  15. data/examples/riak_article.rb +52 -0
  16. data/gemfiles/3.0.gemfile +12 -0
  17. data/gemfiles/4.0.gemfile +11 -0
  18. data/lib/elasticsearch/model/adapter.rb +145 -0
  19. data/lib/elasticsearch/model/adapters/active_record.rb +104 -0
  20. data/lib/elasticsearch/model/adapters/default.rb +50 -0
  21. data/lib/elasticsearch/model/adapters/mongoid.rb +92 -0
  22. data/lib/elasticsearch/model/callbacks.rb +35 -0
  23. data/lib/elasticsearch/model/client.rb +61 -0
  24. data/lib/elasticsearch/model/ext/active_record.rb +14 -0
  25. data/lib/elasticsearch/model/hash_wrapper.rb +15 -0
  26. data/lib/elasticsearch/model/importing.rb +144 -0
  27. data/lib/elasticsearch/model/indexing.rb +472 -0
  28. data/lib/elasticsearch/model/naming.rb +101 -0
  29. data/lib/elasticsearch/model/proxy.rb +127 -0
  30. data/lib/elasticsearch/model/response/base.rb +44 -0
  31. data/lib/elasticsearch/model/response/pagination.rb +173 -0
  32. data/lib/elasticsearch/model/response/records.rb +69 -0
  33. data/lib/elasticsearch/model/response/result.rb +63 -0
  34. data/lib/elasticsearch/model/response/results.rb +31 -0
  35. data/lib/elasticsearch/model/response.rb +71 -0
  36. data/lib/elasticsearch/model/searching.rb +107 -0
  37. data/lib/elasticsearch/model/serializing.rb +35 -0
  38. data/lib/elasticsearch/model/version.rb +5 -0
  39. data/lib/elasticsearch/model.rb +157 -0
  40. data/test/integration/active_record_associations_parent_child.rb +139 -0
  41. data/test/integration/active_record_associations_test.rb +307 -0
  42. data/test/integration/active_record_basic_test.rb +179 -0
  43. data/test/integration/active_record_custom_serialization_test.rb +62 -0
  44. data/test/integration/active_record_import_test.rb +100 -0
  45. data/test/integration/active_record_namespaced_model_test.rb +49 -0
  46. data/test/integration/active_record_pagination_test.rb +132 -0
  47. data/test/integration/mongoid_basic_test.rb +193 -0
  48. data/test/test_helper.rb +63 -0
  49. data/test/unit/adapter_active_record_test.rb +140 -0
  50. data/test/unit/adapter_default_test.rb +41 -0
  51. data/test/unit/adapter_mongoid_test.rb +102 -0
  52. data/test/unit/adapter_test.rb +69 -0
  53. data/test/unit/callbacks_test.rb +31 -0
  54. data/test/unit/client_test.rb +27 -0
  55. data/test/unit/importing_test.rb +176 -0
  56. data/test/unit/indexing_test.rb +478 -0
  57. data/test/unit/module_test.rb +57 -0
  58. data/test/unit/naming_test.rb +76 -0
  59. data/test/unit/proxy_test.rb +89 -0
  60. data/test/unit/response_base_test.rb +40 -0
  61. data/test/unit/response_pagination_kaminari_test.rb +189 -0
  62. data/test/unit/response_pagination_will_paginate_test.rb +208 -0
  63. data/test/unit/response_records_test.rb +91 -0
  64. data/test/unit/response_result_test.rb +90 -0
  65. data/test/unit/response_results_test.rb +31 -0
  66. data/test/unit/response_test.rb +67 -0
  67. data/test/unit/searching_search_request_test.rb +78 -0
  68. data/test/unit/searching_test.rb +41 -0
  69. data/test/unit/serializing_test.rb +17 -0
  70. metadata +466 -0
data/README.md ADDED
@@ -0,0 +1,695 @@
1
+ # Elasticsearch::Model
2
+
3
+ The `elasticsearch-model` library builds on top of the
4
+ the [`elasticsearch`](https://github.com/elasticsearch/elasticsearch-ruby) library.
5
+
6
+ It aims to simplify integration of Ruby classes ("models"), commonly found
7
+ e.g. in [Ruby on Rails](http://rubyonrails.org) applications, with the
8
+ [Elasticsearch](http://www.elasticsearch.org) search and analytics engine.
9
+
10
+ The library is compatible with Ruby 1.9.3 and higher.
11
+
12
+ ## Installation
13
+
14
+ Install the package from [Rubygems](https://rubygems.org):
15
+
16
+ gem install elasticsearch-model
17
+
18
+ To use an unreleased version, either add it to your `Gemfile` for [Bundler](http://bundler.io):
19
+
20
+ gem 'elasticsearch-model', git: 'git://github.com/elasticsearch/elasticsearch-rails.git'
21
+
22
+ or install it from a source code checkout:
23
+
24
+ git clone https://github.com/elasticsearch/elasticsearch-rails.git
25
+ cd elasticsearch-rails/elasticsearch-model
26
+ bundle install
27
+ rake install
28
+
29
+
30
+ ## Usage
31
+
32
+ Let's suppose you have an `Article` model:
33
+
34
+ ```ruby
35
+ require 'active_record'
36
+ ActiveRecord::Base.establish_connection( adapter: 'sqlite3', database: ":memory:" )
37
+ ActiveRecord::Schema.define(version: 1) { create_table(:articles) { |t| t.string :title } }
38
+
39
+ class Article < ActiveRecord::Base; end
40
+
41
+ Article.create title: 'Quick brown fox'
42
+ Article.create title: 'Fast black dogs'
43
+ Article.create title: 'Swift green frogs'
44
+ ```
45
+
46
+ ### Setup
47
+
48
+ To add the Elasticsearch integration for this model, require `elasticsearch/model`
49
+ and include the main module in your class:
50
+
51
+ ```ruby
52
+ require 'elasticsearch/model'
53
+
54
+ class Article < ActiveRecord::Base
55
+ include Elasticsearch::Model
56
+ end
57
+ ```
58
+
59
+ This will extend the model with functionality related to Elasticsearch.
60
+
61
+ #### Feature Extraction Pattern
62
+
63
+ Instead of including the `Elasticsearch::Model` module directly in your model,
64
+ you can include it in a "concern" or "trait" module, which is quite common pattern in Rails applications,
65
+ using e.g. `ActiveSupport::Concern` as the instrumentation:
66
+
67
+ ```ruby
68
+ # In: app/models/concerns/searchable.rb
69
+ #
70
+ module Searchable
71
+ extend ActiveSupport::Concern
72
+
73
+ included do
74
+ include Elasticsearch::Model
75
+
76
+ mapping do
77
+ # ...
78
+ end
79
+
80
+ def self.search(query)
81
+ # ...
82
+ end
83
+ end
84
+ end
85
+
86
+ # In: app/models/article.rb
87
+ #
88
+ class Article
89
+ include Searchable
90
+ end
91
+ ```
92
+
93
+ #### The `__elasticsearch__` Proxy
94
+
95
+ The `Elasticsearch::Model` module contains a big amount of class and instance methods to provide
96
+ all its functionality. To prevent polluting your model namespace, this functionality is primarily
97
+ available via the `__elasticsearch__` class and instance level proxy methods;
98
+ see the `Elasticsearch::Model::Proxy` class documentation for technical information.
99
+
100
+ The module will include important methods, such as `search`, into the class or module only
101
+ when they haven't been defined already. Following two calls are thus functionally equivalent:
102
+
103
+ ```ruby
104
+ Article.__elasticsearch__.search 'fox'
105
+ Article.search 'fox'
106
+ ```
107
+
108
+ See the `Elasticsearch::Model` module documentation for technical information.
109
+
110
+ ### The Elasticsearch client
111
+
112
+ The module will set up a [client](https://github.com/elasticsearch/elasticsearch-ruby/tree/master/elasticsearch),
113
+ connected to `localhost:9200`, by default. You can access and use it as any other `Elasticsearch::Client`:
114
+
115
+ ```ruby
116
+ Article.__elasticsearch__.client.cluster.health
117
+ # => { "cluster_name"=>"elasticsearch", "status"=>"yellow", ... }
118
+ ```
119
+
120
+ To use a client with different configuration, just set up a client for the model:
121
+
122
+ ```ruby
123
+ Article.__elasticsearch__.client = Elasticsearch::Client.new host: 'api.server.org'
124
+ ```
125
+
126
+ Or configure the client for all models:
127
+
128
+ ```ruby
129
+ Elasticsearch::Model.client = Elasticsearch::Client.new log: true
130
+ ```
131
+
132
+ You might want to do this during you application bootstrap process, e.g. in a Rails initializer.
133
+
134
+ Please refer to the
135
+ [`elasticsearch-transport`](https://github.com/elasticsearch/elasticsearch-ruby/tree/master/elasticsearch-transport)
136
+ library documentation for all the configuration options, and to the
137
+ [`elasticsearch-api`](http://rubydoc.info/gems/elasticsearch-api) library documentation
138
+ for information about the Ruby client API.
139
+
140
+ ### Importing the data
141
+
142
+ The first thing you'll want to do is importing your data into the index:
143
+
144
+ ```ruby
145
+ Article.import
146
+ # => 0
147
+ ```
148
+
149
+ It's possible to import only records from a specific `scope`, transform the batch with the `transform`
150
+ and `preprocess` options, or re-create the index by deleting it and creating it with correct mapping with the `force` option -- look for examples in the method documentation.
151
+
152
+ No errors were reported during importing, so... let's search the index!
153
+
154
+
155
+ ### Searching
156
+
157
+ For starters, we can try the "simple" type of search:
158
+
159
+ ```ruby
160
+ response = Article.search 'fox dogs'
161
+
162
+ response.took
163
+ # => 3
164
+
165
+ response.results.total
166
+ # => 2
167
+
168
+ response.results.first._score
169
+ # => 0.02250402
170
+
171
+ response.results.first._source.title
172
+ # => "Quick brown fox"
173
+ ```
174
+
175
+ #### Search results
176
+
177
+ The returned `response` object is a rich wrapper around the JSON returned from Elasticsearch,
178
+ providing access to response metadata and the actual results ("hits").
179
+
180
+ Each "hit" is wrapped in the `Result` class, and provides method access
181
+ to its properties via [`Hashie::Mash`](http://github.com/intridea/hashie).
182
+
183
+ The `results` object supports the `Enumerable` interface:
184
+
185
+ ```ruby
186
+ response.results.map { |r| r._source.title }
187
+ # => ["Quick brown fox", "Fast black dogs"]
188
+
189
+ response.results.select { |r| r.title =~ /^Q/ }
190
+ # => [#<Elasticsearch::Model::Response::Result:0x007 ... "_source"=>{"title"=>"Quick brown fox"}}>]
191
+ ```
192
+
193
+ In fact, the `response` object will delegate `Enumerable` methods to `results`:
194
+
195
+ ```ruby
196
+ response.any? { |r| r.title =~ /fox|dog/ }
197
+ # => true
198
+ ```
199
+
200
+ To use `Array`'s methods (including any _ActiveSupport_ extensions), just call `to_a` on the object:
201
+
202
+ ```ruby
203
+ response.to_a.last.title
204
+ # "Fast black dogs"
205
+ ```
206
+
207
+ #### Search results as database records
208
+
209
+ Instead of returning documents from Elasticsearch, the `records` method will return a collection
210
+ of model instances, fetched from the primary database, ordered by score:
211
+
212
+ ```ruby
213
+ response.records.to_a
214
+ # Article Load (0.3ms) SELECT "articles".* FROM "articles" WHERE "articles"."id" IN (1, 2)
215
+ # => [#<Article id: 1, title: "Quick brown fox">, #<Article id: 2, title: "Fast black dogs">]
216
+ ```
217
+
218
+ The returned object is the genuine collection of model instances returned by your database,
219
+ i.e. `ActiveRecord::Relation` for ActiveRecord, or `Mongoid::Criteria` in case of MongoDB. This allows you to
220
+ chain other methods on top of search results, as you would normally do:
221
+
222
+ ```ruby
223
+ response.records.where(title: 'Quick brown fox').to_a
224
+ # Article Load (0.2ms) SELECT "articles".* FROM "articles" WHERE "articles"."id" IN (1, 2) AND "articles"."title" = 'Quick brown fox'
225
+ # => [#<Article id: 1, title: "Quick brown fox">]
226
+
227
+ response.records.records.class
228
+ # => ActiveRecord::Relation::ActiveRecord_Relation_Article
229
+ ```
230
+
231
+ The ordering of the records by score will be preserved, unless you explicitely specify a different
232
+ order in your model query language:
233
+
234
+ ```ruby
235
+ response.records.order(:title).to_a
236
+ # Article Load (0.2ms) SELECT "articles".* FROM "articles" WHERE "articles"."id" IN (1, 2) ORDER BY "articles".title ASC
237
+ # => [#<Article id: 2, title: "Fast black dogs">, #<Article id: 1, title: "Quick brown fox">]
238
+ ```
239
+
240
+ The `records` method returns the real instances of your model, which is useful when you want to access your
241
+ model methods -- at the expense of slowing down your application, of course.
242
+ In most cases, working with `results` coming from Elasticsearch is sufficient, and much faster. See the
243
+ [`elasticsearch-rails`](https://github.com/elasticsearch/elasticsearch-rails/tree/master/elasticsearch-rails)
244
+ library for more information about compatibility with the Ruby on Rails framework.
245
+
246
+ When you want to access both the database `records` and search `results`, use the `each_with_hit`
247
+ (or `map_with_hit`) iterator:
248
+
249
+ ```ruby
250
+ response.records.each_with_hit { |record, hit| puts "* #{record.title}: #{hit._score}" }
251
+ # * Quick brown fox: 0.02250402
252
+ # * Fast black dogs: 0.02250402
253
+ ```
254
+
255
+ #### Pagination
256
+
257
+ You can implement pagination with the `from` and `size` search parameters. However, search results
258
+ can be automatically paginated with the [`kaminari`](http://rubygems.org/gems/kaminari) or
259
+ [`will_paginate`](https://github.com/mislav/will_paginate) gems.
260
+
261
+ If Kaminari or WillPaginate is loaded, use the familiar paging methods:
262
+
263
+ ```ruby
264
+ response.page(2).results
265
+ response.page(2).records
266
+ ```
267
+
268
+ In a Rails controller, use the the `params[:page]` parameter to paginate through results:
269
+
270
+ ```ruby
271
+ @articles = Article.search(params[:q]).page(params[:page]).records
272
+
273
+ @articles.current_page
274
+ # => 2
275
+ @articles.next_page
276
+ # => 3
277
+ ```
278
+ To initialize and include the Kaminari pagination support manually:
279
+
280
+ ```ruby
281
+ Kaminari::Hooks.init
282
+ Elasticsearch::Model::Response::Response.__send__ :include, Elasticsearch::Model::Response::Pagination::Kaminari
283
+ ```
284
+
285
+ #### The Elasticsearch DSL
286
+
287
+ In most situation, you'll want to pass the search definition
288
+ in the Elasticsearch [domain-specific language](http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/query-dsl.html) to the client:
289
+
290
+ ```ruby
291
+ response = Article.search query: { match: { title: "Fox Dogs" } },
292
+ highlight: { fields: { title: {} } }
293
+
294
+ response.results.first.highlight.title
295
+ # ["Quick brown <em>fox</em>"]
296
+ ```
297
+
298
+ You can pass any object which implements a `to_hash` method, or you can use your favourite JSON builder
299
+ to build the search definition as a JSON string:
300
+
301
+ ```ruby
302
+ require 'jbuilder'
303
+
304
+ query = Jbuilder.encode do |json|
305
+ json.query do
306
+ json.match do
307
+ json.title do
308
+ json.query "fox dogs"
309
+ end
310
+ end
311
+ end
312
+ end
313
+
314
+ response = Article.search query
315
+ response.results.first.title
316
+ # => "Quick brown fox"
317
+ ```
318
+
319
+ ### Index Configuration
320
+
321
+ For proper search engine function, it's often necessary to configure the index properly.
322
+ The `Elasticsearch::Model` integration provides class methods to set up index settings and mappings.
323
+
324
+ ```ruby
325
+ class Article
326
+ settings index: { number_of_shards: 1 } do
327
+ mappings dynamic: 'false' do
328
+ indexes :title, analyzer: 'english', index_options: 'offsets'
329
+ end
330
+ end
331
+ end
332
+
333
+ Article.mappings.to_hash
334
+ # => {
335
+ # :article => {
336
+ # :dynamic => "false",
337
+ # :properties => {
338
+ # :title => {
339
+ # :type => "string",
340
+ # :analyzer => "english",
341
+ # :index_options => "offsets"
342
+ # }
343
+ # }
344
+ # }
345
+ # }
346
+
347
+ Article.settings.to_hash
348
+ # { :index => { :number_of_shards => 1 } }
349
+ ```
350
+
351
+ You can use the defined settings and mappings to create an index with desired configuration:
352
+
353
+ ```ruby
354
+ Article.__elasticsearch__.client.indices.delete index: Article.index_name rescue nil
355
+ Article.__elasticsearch__.client.indices.create \
356
+ index: Article.index_name,
357
+ body: { settings: Article.settings.to_hash, mappings: Article.mappings.to_hash }
358
+ ```
359
+
360
+ There's a shortcut available for this common operation (convenient e.g. in tests):
361
+
362
+ ```ruby
363
+ Article.__elasticsearch__.create_index! force: true
364
+ Article.__elasticsearch__.refresh_index!
365
+ ```
366
+
367
+ By default, index name and document type will be inferred from your class name,
368
+ you can set it explicitely, however:
369
+
370
+ ```ruby
371
+ class Article
372
+ index_name "articles-#{Rails.env}"
373
+ document_type "post"
374
+ end
375
+ ```
376
+
377
+ ### Updating the Documents in the Index
378
+
379
+ Usually, we need to update the Elasticsearch index when records in the database are created, updated or deleted;
380
+ use the `index_document`, `update_document` and `delete_document` methods, respectively:
381
+
382
+ ```ruby
383
+ Article.first.__elasticsearch__.index_document
384
+ # => {"ok"=>true, ... "_version"=>2}
385
+ ```
386
+
387
+ #### Automatic Callbacks
388
+
389
+ You can automatically update the index whenever the record changes, by including
390
+ the `Elasticsearch::Model::Callbacks` module in your model:
391
+
392
+ ```ruby
393
+ class Article
394
+ include Elasticsearch::Model
395
+ include Elasticsearch::Model::Callbacks
396
+ end
397
+
398
+ Article.first.update_attribute :title, 'Updated!'
399
+
400
+ Article.search('*').map { |r| r.title }
401
+ # => ["Updated!", "Lime green frogs", "Fast black dogs"]
402
+ ```
403
+
404
+ The automatic callback on record update keeps track of changes in your model
405
+ (via [`ActiveModel::Dirty`](http://api.rubyonrails.org/classes/ActiveModel/Dirty.html)-compliant implementation),
406
+ and performs a _partial update_ when this support is available.
407
+
408
+ The automatic callbacks are implemented in database adapters coming with `Elasticsearch::Model`. You can easily
409
+ implement your own adapter: please see the relevant chapter below.
410
+
411
+ #### Custom Callbacks
412
+
413
+ In case you would need more control of the indexing process, you can implement these callbacks yourself,
414
+ by hooking into `after_create`, `after_save`, `after_update` or `after_destroy` operations:
415
+
416
+ ```ruby
417
+ class Article
418
+ include Elasticsearch::Model
419
+
420
+ after_save { logger.debug ["Updating document... ", index_document ].join }
421
+ after_destroy { logger.debug ["Deleting document... ", delete_document].join }
422
+ end
423
+ ```
424
+
425
+ For ActiveRecord-based models, you need to hook into the `after_commit` callback, to protect
426
+ your data against inconsistencies caused by transaction rollbacks:
427
+
428
+ ```ruby
429
+ class Article < ActiveRecord::Base
430
+ include Elasticsearch::Model
431
+
432
+ after_commit on: [:create] do
433
+ index_document if self.published?
434
+ end
435
+
436
+ after_commit on: [:update] do
437
+ update_document if self.published?
438
+ end
439
+
440
+ after_commit on: [:destroy] do
441
+ delete_document if self.published?
442
+ end
443
+ end
444
+ ```
445
+
446
+ #### Asynchronous Callbacks
447
+
448
+ Of course, you're still performing an HTTP request during your database transaction, which is not optimal
449
+ for large-scale applications. A better option would be to process the index operations in background,
450
+ with a tool like [_Resque_](https://github.com/resque/resque) or [_Sidekiq_](https://github.com/mperham/sidekiq):
451
+
452
+ ```ruby
453
+ class Article
454
+ include Elasticsearch::Model
455
+
456
+ after_save { Indexer.perform_async(:index, self.id) }
457
+ after_destroy { Indexer.perform_async(:delete, self.id) }
458
+ end
459
+ ```
460
+
461
+ An example implementation of the `Indexer` worker class could look like this:
462
+
463
+ ```ruby
464
+ class Indexer
465
+ include Sidekiq::Worker
466
+ sidekiq_options queue: 'elasticsearch', retry: false
467
+
468
+ Logger = Sidekiq.logger.level == Logger::DEBUG ? Sidekiq.logger : nil
469
+ Client = Elasticsearch::Client.new host: 'localhost:9200', logger: Logger
470
+
471
+ def perform(operation, record_id)
472
+ logger.debug [operation, "ID: #{record_id}"]
473
+
474
+ case operation.to_s
475
+ when /index/
476
+ record = Article.find(record_id)
477
+ Client.index index: 'articles', type: 'article', id: record.id, body: record.as_indexed_json
478
+ when /delete/
479
+ Client.delete index: 'articles', type: 'article', id: record_id
480
+ else raise ArgumentError, "Unknown operation '#{operation}'"
481
+ end
482
+ end
483
+ end
484
+ ```
485
+
486
+ Start the _Sidekiq_ workers with `bundle exec sidekiq --queue elasticsearch --verbose` and
487
+ update a model:
488
+
489
+ ```ruby
490
+ Article.first.update_attribute :title, 'Updated'
491
+ ```
492
+
493
+ You'll see the job being processed in the console where you started the _Sidekiq_ worker:
494
+
495
+ ```
496
+ Indexer JID-eb7e2daf389a1e5e83697128 DEBUG: ["index", "ID: 7"]
497
+ Indexer JID-eb7e2daf389a1e5e83697128 INFO: PUT http://localhost:9200/articles/article/1 [status:200, request:0.004s, query:n/a]
498
+ Indexer JID-eb7e2daf389a1e5e83697128 DEBUG: > {"id":1,"title":"Updated", ...}
499
+ Indexer JID-eb7e2daf389a1e5e83697128 DEBUG: < {"ok":true,"_index":"articles","_type":"article","_id":"1","_version":6}
500
+ Indexer JID-eb7e2daf389a1e5e83697128 INFO: done: 0.006 sec
501
+ ```
502
+
503
+ ### Model Serialization
504
+
505
+ By default, the model instance will be serialized to JSON using the `as_indexed_json` method,
506
+ which is defined automatically by the `Elasticsearch::Model::Serializing` module:
507
+
508
+ ```ruby
509
+ Article.first.__elasticsearch__.as_indexed_json
510
+ # => {"id"=>1, "title"=>"Quick brown fox"}
511
+ ```
512
+
513
+ If you want to customize the serialization, just implement the `as_indexed_json` method yourself,
514
+ for instance with the [`as_json`](http://api.rubyonrails.org/classes/ActiveModel/Serializers/JSON.html#method-i-as_json) method:
515
+
516
+ ```ruby
517
+ class Article
518
+ include Elasticsearch::Model
519
+
520
+ def as_indexed_json(options={})
521
+ as_json(only: 'title')
522
+ end
523
+ end
524
+
525
+ Article.first.as_indexed_json
526
+ # => {"title"=>"Quick brown fox"}
527
+ ```
528
+
529
+ The re-defined method will be used in the indexing methods, such as `index_document`.
530
+
531
+ Please note that in Rails 3, you need to either set `include_root_in_json: false`, or prevent adding
532
+ the "root" in the JSON representation with other means.
533
+
534
+ #### Relationships and Associations
535
+
536
+ When you have a more complicated structure/schema, you need to customize the `as_indexed_json` method -
537
+ or perform the indexing separately, on your own.
538
+ For example, let's have an `Article` model, which _has_many_ `Comment`s,
539
+ `Author`s and `Categories`. We might want to define the serialization like this:
540
+
541
+ ```ruby
542
+ def as_indexed_json(options={})
543
+ self.as_json(
544
+ include: { categories: { only: :title},
545
+ authors: { methods: [:full_name], only: [:full_name] },
546
+ comments: { only: :text }
547
+ })
548
+ end
549
+
550
+ Article.first.as_indexed_json
551
+ # => { "id" => 1,
552
+ # "title" => "First Article",
553
+ # "created_at" => 2013-12-03 13:39:02 UTC,
554
+ # "updated_at" => 2013-12-03 13:39:02 UTC,
555
+ # "categories" => [ { "title" => "One" } ],
556
+ # "authors" => [ { "full_name" => "John Smith" } ],
557
+ # "comments" => [ { "text" => "First comment" } ] }
558
+ ```
559
+
560
+ Of course, when you want to use the automatic indexing callbacks, you need to hook into the appropriate
561
+ _ActiveRecord_ callbacks -- please see the full example in `examples/activerecord_associations.rb`.
562
+
563
+ ### Other ActiveModel Frameworks
564
+
565
+ The `Elasticsearch::Model` module is fully compatible with any ActiveModel-compatible model, such as _Mongoid_:
566
+
567
+ ```ruby
568
+ require 'mongoid'
569
+
570
+ Mongoid.connect_to 'articles'
571
+
572
+ class Article
573
+ include Mongoid::Document
574
+
575
+ field :id, type: String
576
+ field :title, type: String
577
+
578
+ attr_accessible :id, :title, :published_at
579
+
580
+ include Elasticsearch::Model
581
+
582
+ def as_indexed_json(options={})
583
+ as_json(except: [:id, :_id])
584
+ end
585
+ end
586
+
587
+ Article.create id: '1', title: 'Quick brown fox'
588
+ Article.import
589
+
590
+ response = Article.search 'fox';
591
+ response.records.to_a
592
+ # MOPED: 127.0.0.1:27017 QUERY database=articles collection=articles selector={"_id"=>{"$in"=>["1"]}} ...
593
+ # => [#<Article _id: 1, id: nil, title: "Quick brown fox", published_at: nil>]
594
+ ```
595
+
596
+ Full examples for CouchBase, DataMapper, Mongoid, Ohm and Riak models can be found in the `examples` folder.
597
+
598
+ ### Adapters
599
+
600
+ To support various "OxM" (object-relational- or object-document-mapper) implementations and frameworks,
601
+ the `Elasticsearch::Model` integration supports an "adapter" concept.
602
+
603
+ An adapter provides implementations for common behaviour, such as fetching records from the database,
604
+ hooking into model callbacks for automatic index updates, or efficient bulk loading from the database.
605
+ The integration comes with adapters for _ActiveRecord_ and _Mongoid_ out of the box.
606
+
607
+ Writing an adapter for your favourite framework is straightforward -- let's see
608
+ a simplified adapter for [_DataMapper_](http://datamapper.org):
609
+
610
+ ```ruby
611
+ module DataMapperAdapter
612
+
613
+ # Implement the interface for fetching records
614
+ #
615
+ module Records
616
+ def records
617
+ klass.all(id: @ids)
618
+ end
619
+
620
+ # ...
621
+ end
622
+ end
623
+
624
+ # Register the adapter
625
+ #
626
+ Elasticsearch::Model::Adapter.register(
627
+ DataMapperAdapter,
628
+ lambda { |klass| defined?(::DataMapper::Resource) and klass.ancestors.include?(::DataMapper::Resource) }
629
+ )
630
+ ```
631
+
632
+ Require the adapter and include `Elasticsearch::Model` in the class:
633
+
634
+ ```ruby
635
+ require 'datamapper_adapter'
636
+
637
+ class Article
638
+ include DataMapper::Resource
639
+ include Elasticsearch::Model
640
+
641
+ property :id, Serial
642
+ property :title, String
643
+ end
644
+ ```
645
+
646
+ When accessing the `records` method of the response, for example,
647
+ the implementation from our adapter will be used now:
648
+
649
+ ```ruby
650
+ response = Article.search 'foo'
651
+
652
+ response.records.to_a
653
+ # ~ (0.000057) SELECT "id", "title", "published_at" FROM "articles" WHERE "id" IN (3, 1) ORDER BY "id"
654
+ # => [#<Article @id=1 @title="Foo" @published_at=nil>, #<Article @id=3 @title="Foo Foo" @published_at=nil>]
655
+
656
+ response.records.records.class
657
+ # => DataMapper::Collection
658
+ ```
659
+
660
+ More examples can be found in the `examples` folder. Please see the `Elasticsearch::Model::Adapter`
661
+ module and its submodules for technical information.
662
+
663
+ ## Development and Community
664
+
665
+ For local development, clone the repository and run `bundle install`. See `rake -T` for a list of
666
+ available Rake tasks for running tests, generating documentation, starting a testing cluster, etc.
667
+
668
+ Bug fixes and features must be covered by unit tests.
669
+
670
+ Github's pull requests and issues are used to communicate, send bug reports and code contributions.
671
+
672
+ To run all tests against a test Elasticsearch cluster, use a command like this:
673
+
674
+ ```bash
675
+ curl -# https://download.elasticsearch.org/elasticsearch/elasticsearch/elasticsearch-1.0.0.RC1.tar.gz | tar xz -C tmp/
676
+ SERVER=start TEST_CLUSTER_COMMAND=$PWD/tmp/elasticsearch-1.0.0.RC1/bin/elasticsearch bundle exec rake test:all
677
+ ```
678
+
679
+ ## License
680
+
681
+ This software is licensed under the Apache 2 license, quoted below.
682
+
683
+ Copyright (c) 2014 Elasticsearch <http://www.elasticsearch.org>
684
+
685
+ Licensed under the Apache License, Version 2.0 (the "License");
686
+ you may not use this file except in compliance with the License.
687
+ You may obtain a copy of the License at
688
+
689
+ http://www.apache.org/licenses/LICENSE-2.0
690
+
691
+ Unless required by applicable law or agreed to in writing, software
692
+ distributed under the License is distributed on an "AS IS" BASIS,
693
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
694
+ See the License for the specific language governing permissions and
695
+ limitations under the License.