elasticsearch-persistence 0.1.3 → 0.1.4

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