elasticsearch-persistence 0.1.3 → 0.1.4

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 (53) hide show
  1. checksums.yaml +8 -8
  2. data/CHANGELOG.md +4 -0
  3. data/README.md +238 -7
  4. data/elasticsearch-persistence.gemspec +4 -1
  5. data/examples/music/album.rb +34 -0
  6. data/examples/music/artist.rb +50 -0
  7. data/examples/music/artists/_form.html.erb +8 -0
  8. data/examples/music/artists/artists_controller.rb +67 -0
  9. data/examples/music/artists/artists_controller_test.rb +53 -0
  10. data/examples/music/artists/index.html.erb +57 -0
  11. data/examples/music/artists/show.html.erb +51 -0
  12. data/examples/music/assets/application.css +226 -0
  13. data/examples/music/assets/autocomplete.css +48 -0
  14. data/examples/music/assets/blank_cover.png +0 -0
  15. data/examples/music/assets/form.css +113 -0
  16. data/examples/music/index_manager.rb +60 -0
  17. data/examples/music/search/index.html.erb +93 -0
  18. data/examples/music/search/search_controller.rb +41 -0
  19. data/examples/music/search/search_controller_test.rb +9 -0
  20. data/examples/music/search/search_helper.rb +15 -0
  21. data/examples/music/suggester.rb +45 -0
  22. data/examples/music/template.rb +392 -0
  23. data/examples/music/vendor/assets/jquery-ui-1.10.4.custom.min.css +7 -0
  24. data/examples/music/vendor/assets/jquery-ui-1.10.4.custom.min.js +6 -0
  25. data/examples/{sinatra → notes}/.gitignore +0 -0
  26. data/examples/{sinatra → notes}/Gemfile +0 -0
  27. data/examples/{sinatra → notes}/README.markdown +0 -0
  28. data/examples/{sinatra → notes}/application.rb +0 -0
  29. data/examples/{sinatra → notes}/config.ru +0 -0
  30. data/examples/{sinatra → notes}/test.rb +0 -0
  31. data/lib/elasticsearch/persistence.rb +19 -0
  32. data/lib/elasticsearch/persistence/model.rb +129 -0
  33. data/lib/elasticsearch/persistence/model/base.rb +75 -0
  34. data/lib/elasticsearch/persistence/model/errors.rb +8 -0
  35. data/lib/elasticsearch/persistence/model/find.rb +171 -0
  36. data/lib/elasticsearch/persistence/model/rails.rb +39 -0
  37. data/lib/elasticsearch/persistence/model/store.rb +239 -0
  38. data/lib/elasticsearch/persistence/model/utils.rb +0 -0
  39. data/lib/elasticsearch/persistence/repository.rb +3 -1
  40. data/lib/elasticsearch/persistence/repository/search.rb +25 -0
  41. data/lib/elasticsearch/persistence/version.rb +1 -1
  42. data/lib/rails/generators/elasticsearch/model/model_generator.rb +21 -0
  43. data/lib/rails/generators/elasticsearch/model/templates/model.rb.tt +9 -0
  44. data/lib/rails/generators/elasticsearch_generator.rb +2 -0
  45. data/test/integration/model/model_basic_test.rb +157 -0
  46. data/test/integration/repository/default_class_test.rb +6 -0
  47. data/test/unit/model_base_test.rb +40 -0
  48. data/test/unit/model_find_test.rb +147 -0
  49. data/test/unit/model_gateway_test.rb +99 -0
  50. data/test/unit/model_rails_test.rb +88 -0
  51. data/test/unit/model_store_test.rb +493 -0
  52. data/test/unit/repository_search_test.rb +17 -0
  53. metadata +79 -9
checksums.yaml CHANGED
@@ -1,15 +1,15 @@
1
1
  ---
2
2
  !binary "U0hBMQ==":
3
3
  metadata.gz: !binary |-
4
- NGIxM2RkNWIyNmRkMGE4YzE1NzY0ZmNkNmI3ZDI4ODI5ZDY5NTM1Nw==
4
+ OTQ2NWJlZWJlNDE0ODRhODAzZDM2ZDFhYTFiNDkzNTdlMTZkZjQzYQ==
5
5
  data.tar.gz: !binary |-
6
- YmFiZGFkYzZhZWU4NTY0NTNlNzVkZjNkZWM0MzJkNWFhMmQ3YmU4NQ==
6
+ MmUzOGJiN2I0MmY1ZDBmOTg3NjRiOWM0NDk2NjkzN2Q0N2I2Yjk3Mg==
7
7
  SHA512:
8
8
  metadata.gz: !binary |-
9
- MzFhNzNjMDdjM2NlZTNmN2IzZDA4ZjM0ZjVjNGRkODlhYzMwNzNlZTk0MTMw
10
- NGZkNWYyOThlMzA4NWZmZTRkNjc5MzI1N2ZjZTMwODJlYzAxZTVlODQ0ZWIy
11
- MTQ0YWQ4M2QyMTAwNzc0OTI0ZDUwZGI2NmM3YjUxZGJmMmNhYjU=
9
+ YTViMmMzZTNlMGVhMTE4ZWIzYzJmYjU1ZGQwMWJlMzVmMWQ4ZmM2Y2YyNTcz
10
+ NWQ2NDE2NDlkYzI2YTFkMjMxOWVlMWZlZWE5MWQ4MmNlNjQxMzc1MWVkNzZk
11
+ MTcxYjM2OTk1MDNlNDQ5NjNiNzk1NTkxNGVlNjVjNTMyZTI4Mzk=
12
12
  data.tar.gz: !binary |-
13
- ODdjOTY2NDE0ODkwMzA5YThlZjU1MmZlNTVhZDc2ODZmODA0NmY4YTBhNzY2
14
- ODI0MDc1M2MxOTk5ZWVjMWU3ZGRmYWJhZTk3ZjYyYmM1NDcyM2RmZDQ4YzEz
15
- NmNiMzQ1ODliYzNhYzEyNzE0OWVhNmQ4NjcxYzY2ZWM3MmZmMjU=
13
+ ZjFmNDc0MWYyOTZjNTcwZmNjMzBmYzY5ZTlmZmFmODM3MzEwNTg0OTAwYmY5
14
+ MzZkZTFmZjNmNzliZTFiNTY4YTZhMjVhYWY1MmNmZWRiNTJmZWYwNDNiYWFj
15
+ ZmRhMmZhNzg1ZTAyNDI3ODJhODRlMGI3MjI5NDczZTNhYmU4NzQ=
data/CHANGELOG.md CHANGED
@@ -1,3 +1,7 @@
1
+ ## 0.1.4
2
+
3
+ * Added the Elasticsearch::Persistence::Model feature
4
+
1
5
  ## 0.1.3
2
6
 
3
7
  * Released the "elasticsearch-persistence" Rubygem
data/README.md CHANGED
@@ -416,17 +416,248 @@ results.response._shards.failed
416
416
 
417
417
  #### Example Application
418
418
 
419
- An example Sinatra application is available in
420
- [`examples/sinatra/application.rb`](examples/sinatra/application.rb),
421
- and demonstrates a rich set of features of the repository.
419
+ An example Sinatra application is available in [`examples/notes/application.rb`](examples/notes/application.rb),
420
+ and demonstrates a rich set of features:
422
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
423
427
 
424
428
  ### The ActiveRecord Pattern
425
429
 
426
- [_Work in progress_](https://github.com/elasticsearch/elasticsearch-rails/pull/91).
427
- The ActiveRecord [pattern](http://www.martinfowler.com/eaaCatalog/activeRecord.html) will work
428
- in a very similar way as `Tire::Model::Persistence`, allowing a drop-in replacement of
429
- an Elasticsearch-backed model in Ruby on Rails applications.
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 works 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 instance, 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 Elasticsearch integration 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:
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/persistence-model/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 configure 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 model 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.
430
661
 
431
662
  ## License
432
663
 
@@ -26,13 +26,16 @@ Gem::Specification.new do |s|
26
26
  s.add_dependency "elasticsearch", '> 0.4'
27
27
  s.add_dependency "elasticsearch-model", '>= 0.1'
28
28
  s.add_dependency "activesupport", '> 3'
29
+ s.add_dependency "activemodel", '> 3'
29
30
  s.add_dependency "hashie"
31
+ s.add_dependency "virtus"
30
32
 
31
33
  s.add_development_dependency "bundler", "~> 1.5"
32
34
  s.add_development_dependency "rake"
33
35
 
34
36
  s.add_development_dependency "oj"
35
- s.add_development_dependency "virtus"
37
+
38
+ s.add_development_dependency "rails"
36
39
 
37
40
  s.add_development_dependency "elasticsearch-extensions"
38
41
 
@@ -0,0 +1,34 @@
1
+ class Meta
2
+ include Virtus.model
3
+
4
+ attribute :rating
5
+ attribute :have
6
+ attribute :want
7
+ attribute :formats
8
+ end
9
+
10
+ class Album
11
+ include Elasticsearch::Persistence::Model
12
+
13
+ index_name [Rails.application.engine_name, Rails.env].join('-')
14
+
15
+ mapping _parent: { type: 'artist' } do
16
+ indexes :suggest_title, type: 'completion', payloads: true
17
+ indexes :suggest_track, type: 'completion', payloads: true
18
+ end
19
+
20
+ attribute :artist
21
+ attribute :artist_id, String, mapping: { index: 'not_analyzed' }
22
+ attribute :label, Hash, mapping: { type: 'object' }
23
+
24
+ attribute :title
25
+ attribute :suggest_title, String, default: {}, mapping: { type: 'completion', payloads: true }
26
+ attribute :released, Date
27
+ attribute :notes
28
+ attribute :uri
29
+
30
+ attribute :tracklist, Array, mapping: { type: 'object' }
31
+
32
+ attribute :styles
33
+ attribute :meta, Meta, mapping: { type: 'object' }
34
+ end
@@ -0,0 +1,50 @@
1
+ class Artist
2
+ include Elasticsearch::Persistence::Model
3
+
4
+ index_name [Rails.application.engine_name, Rails.env].join('-')
5
+
6
+ analyzed_and_raw = { fields: {
7
+ name: { type: 'string', analyzer: 'snowball' },
8
+ raw: { type: 'string', analyzer: 'keyword' }
9
+ } }
10
+
11
+ attribute :name, String, mapping: analyzed_and_raw
12
+ attribute :suggest_name, String, default: {}, mapping: { type: 'completion', payloads: true }
13
+
14
+ attribute :profile
15
+ attribute :date, Date
16
+
17
+ attribute :members, String, default: [], mapping: analyzed_and_raw
18
+ attribute :members_combined, String, default: [], mapping: { analyzer: 'snowball' }
19
+ attribute :suggest_member, String, default: {}, mapping: { type: 'completion', payloads: true }
20
+
21
+ attribute :urls, String, default: []
22
+ attribute :album_count, Integer, default: 0
23
+
24
+ validates :name, presence: true
25
+
26
+ def albums
27
+ Album.search(
28
+ { query: {
29
+ has_parent: {
30
+ type: 'artist',
31
+ query: {
32
+ filtered: {
33
+ filter: {
34
+ ids: { values: [ self.id ] }
35
+ }
36
+ }
37
+ }
38
+ }
39
+ },
40
+ sort: 'released',
41
+ size: 100
42
+ },
43
+ { type: 'album' }
44
+ )
45
+ end
46
+
47
+ def to_param
48
+ [id, name.parameterize].join('-')
49
+ end
50
+ end
@@ -0,0 +1,8 @@
1
+ <%= simple_form_for @artist do |f| %>
2
+ <%= f.input :name %>
3
+ <%= f.input :profile, as: :text %>
4
+ <%= f.input :date, as: :date %>
5
+ <%= f.input :members, hint: 'Separate names by comma', input_html: { value: f.object.members.join(', ') } %>
6
+
7
+ <%= f.button :submit %>
8
+ <% end %>
@@ -0,0 +1,67 @@
1
+ class ArtistsController < ApplicationController
2
+ before_action :set_artist, only: [:show, :edit, :update, :destroy]
3
+
4
+ rescue_from Elasticsearch::Persistence::Repository::DocumentNotFound do
5
+ render file: "public/404.html", status: 404, layout: false
6
+ end
7
+
8
+ def index
9
+ @artists = Artist.all sort: 'name.raw', _source: ['name', 'album_count']
10
+ end
11
+
12
+ def show
13
+ @albums = @artist.albums
14
+ end
15
+
16
+ def new
17
+ @artist = Artist.new
18
+ end
19
+
20
+ def edit
21
+ end
22
+
23
+ def create
24
+ @artist = Artist.new(artist_params)
25
+
26
+ respond_to do |format|
27
+ if @artist.save refresh: true
28
+ format.html { redirect_to @artist, notice: 'Artist was successfully created.' }
29
+ format.json { render :show, status: :created, location: @artist }
30
+ else
31
+ format.html { render :new }
32
+ format.json { render json: @artist.errors, status: :unprocessable_entity }
33
+ end
34
+ end
35
+ end
36
+
37
+ def update
38
+ respond_to do |format|
39
+ if @artist.update(artist_params, refresh: true)
40
+ format.html { redirect_to @artist, notice: 'Artist was successfully updated.' }
41
+ format.json { render :show, status: :ok, location: @artist }
42
+ else
43
+ format.html { render :edit }
44
+ format.json { render json: @artist.errors, status: :unprocessable_entity }
45
+ end
46
+ end
47
+ end
48
+
49
+ def destroy
50
+ @artist.destroy refresh: true
51
+ respond_to do |format|
52
+ format.html { redirect_to artists_url, notice: 'Artist was successfully destroyed.' }
53
+ format.json { head :no_content }
54
+ end
55
+ end
56
+
57
+ private
58
+ def set_artist
59
+ @artist = Artist.find(params[:id].split('-').first)
60
+ end
61
+
62
+ def artist_params
63
+ a = params.require(:artist)
64
+ a[:members] = a[:members].split(/,\s?/) unless a[:members].is_a?(Array) || a[:members].blank?
65
+ return a
66
+ end
67
+ end