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.
- checksums.yaml +8 -8
- data/CHANGELOG.md +4 -0
- data/README.md +238 -7
- data/elasticsearch-persistence.gemspec +4 -1
- data/examples/music/album.rb +34 -0
- data/examples/music/artist.rb +50 -0
- data/examples/music/artists/_form.html.erb +8 -0
- data/examples/music/artists/artists_controller.rb +67 -0
- data/examples/music/artists/artists_controller_test.rb +53 -0
- data/examples/music/artists/index.html.erb +57 -0
- data/examples/music/artists/show.html.erb +51 -0
- data/examples/music/assets/application.css +226 -0
- data/examples/music/assets/autocomplete.css +48 -0
- data/examples/music/assets/blank_cover.png +0 -0
- data/examples/music/assets/form.css +113 -0
- data/examples/music/index_manager.rb +60 -0
- data/examples/music/search/index.html.erb +93 -0
- data/examples/music/search/search_controller.rb +41 -0
- data/examples/music/search/search_controller_test.rb +9 -0
- data/examples/music/search/search_helper.rb +15 -0
- data/examples/music/suggester.rb +45 -0
- data/examples/music/template.rb +392 -0
- data/examples/music/vendor/assets/jquery-ui-1.10.4.custom.min.css +7 -0
- data/examples/music/vendor/assets/jquery-ui-1.10.4.custom.min.js +6 -0
- data/examples/{sinatra → notes}/.gitignore +0 -0
- data/examples/{sinatra → notes}/Gemfile +0 -0
- data/examples/{sinatra → notes}/README.markdown +0 -0
- data/examples/{sinatra → notes}/application.rb +0 -0
- data/examples/{sinatra → notes}/config.ru +0 -0
- data/examples/{sinatra → notes}/test.rb +0 -0
- data/lib/elasticsearch/persistence.rb +19 -0
- data/lib/elasticsearch/persistence/model.rb +129 -0
- data/lib/elasticsearch/persistence/model/base.rb +75 -0
- data/lib/elasticsearch/persistence/model/errors.rb +8 -0
- data/lib/elasticsearch/persistence/model/find.rb +171 -0
- data/lib/elasticsearch/persistence/model/rails.rb +39 -0
- data/lib/elasticsearch/persistence/model/store.rb +239 -0
- data/lib/elasticsearch/persistence/model/utils.rb +0 -0
- data/lib/elasticsearch/persistence/repository.rb +3 -1
- data/lib/elasticsearch/persistence/repository/search.rb +25 -0
- data/lib/elasticsearch/persistence/version.rb +1 -1
- data/lib/rails/generators/elasticsearch/model/model_generator.rb +21 -0
- data/lib/rails/generators/elasticsearch/model/templates/model.rb.tt +9 -0
- data/lib/rails/generators/elasticsearch_generator.rb +2 -0
- data/test/integration/model/model_basic_test.rb +157 -0
- data/test/integration/repository/default_class_test.rb +6 -0
- data/test/unit/model_base_test.rb +40 -0
- data/test/unit/model_find_test.rb +147 -0
- data/test/unit/model_gateway_test.rb +99 -0
- data/test/unit/model_rails_test.rb +88 -0
- data/test/unit/model_store_test.rb +493 -0
- data/test/unit/repository_search_test.rb +17 -0
- metadata +79 -9
checksums.yaml
CHANGED
@@ -1,15 +1,15 @@
|
|
1
1
|
---
|
2
2
|
!binary "U0hBMQ==":
|
3
3
|
metadata.gz: !binary |-
|
4
|
-
|
4
|
+
OTQ2NWJlZWJlNDE0ODRhODAzZDM2ZDFhYTFiNDkzNTdlMTZkZjQzYQ==
|
5
5
|
data.tar.gz: !binary |-
|
6
|
-
|
6
|
+
MmUzOGJiN2I0MmY1ZDBmOTg3NjRiOWM0NDk2NjkzN2Q0N2I2Yjk3Mg==
|
7
7
|
SHA512:
|
8
8
|
metadata.gz: !binary |-
|
9
|
-
|
10
|
-
|
11
|
-
|
9
|
+
YTViMmMzZTNlMGVhMTE4ZWIzYzJmYjU1ZGQwMWJlMzVmMWQ4ZmM2Y2YyNTcz
|
10
|
+
NWQ2NDE2NDlkYzI2YTFkMjMxOWVlMWZlZWE5MWQ4MmNlNjQxMzc1MWVkNzZk
|
11
|
+
MTcxYjM2OTk1MDNlNDQ5NjNiNzk1NTkxNGVlNjVjNTMyZTI4Mzk=
|
12
12
|
data.tar.gz: !binary |-
|
13
|
-
|
14
|
-
|
15
|
-
|
13
|
+
ZjFmNDc0MWYyOTZjNTcwZmNjMzBmYzY5ZTlmZmFmODM3MzEwNTg0OTAwYmY5
|
14
|
+
MzZkZTFmZjNmNzliZTFiNTY4YTZhMjVhYWY1MmNmZWRiNTJmZWYwNDNiYWFj
|
15
|
+
ZmRhMmZhNzg1ZTAyNDI3ODJhODRlMGI3MjI5NDczZTNhYmU4NzQ=
|
data/CHANGELOG.md
CHANGED
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
|
-
|
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
|
-
|
427
|
-
|
428
|
-
|
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 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
|
-
|
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
|