Skip to content

Commit 2a137e0

Browse files
committed
[STORE] Added the template for generating a full Rails application with persistence model
Usage: rails new music --force --skip --skip-bundle --skip-active-record --template /path/to/template.rb 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
1 parent 1ec8514 commit 2a137e0

21 files changed

+1394
-0
lines changed

elasticsearch-persistence/README.md

+19
Original file line numberDiff line numberDiff line change
@@ -606,6 +606,25 @@ The library provides a Rails ORM generator:
606606
rails generate scaffold Person name:String email:String birthday:Date --orm=elasticsearch
607607
```
608608

609+
#### Example application
610+
611+
A fully working Ruby on Rails application can be generated with the following command:
612+
613+
```bash
614+
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
615+
```
616+
617+
The application demonstrates:
618+
619+
* How to set up model attributes with custom mappings
620+
* How to configure model relationships with Elasticsearch's parent/child
621+
* How to configure models to use a common index, and create the index with proper mappings
622+
* How to use Elasticsearch's completion suggester to drive auto-complete functionality
623+
* How to use Elasticsearch-persisted model in Rails' views and forms
624+
* How to write controller tests
625+
626+
The source files for the application are available in the [`examples/music`](examples/music) folder.
627+
609628
## License
610629

611630
This software is licensed under the Apache 2 license, quoted below.
Original file line numberDiff line numberDiff line change
@@ -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
Original file line numberDiff line numberDiff line change
@@ -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
Original file line numberDiff line numberDiff line change
@@ -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 %>
Original file line numberDiff line numberDiff line change
@@ -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
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
require 'test_helper'
2+
3+
class ArtistsControllerTest < ActionController::TestCase
4+
setup do
5+
IndexManager.create_index force: true
6+
@artist = Artist.create(id: 1, name: 'TEST')
7+
Artist.gateway.refresh_index!
8+
end
9+
10+
test "should get index" do
11+
get :index
12+
assert_response :success
13+
assert_not_nil assigns(:artists)
14+
end
15+
16+
test "should get new" do
17+
get :new
18+
assert_response :success
19+
end
20+
21+
test "should create artist" do
22+
assert_difference('Artist.count') do
23+
post :create, artist: { name: @artist.name }
24+
Artist.gateway.refresh_index!
25+
end
26+
27+
assert_redirected_to artist_path(assigns(:artist))
28+
end
29+
30+
test "should show artist" do
31+
get :show, id: @artist
32+
assert_response :success
33+
end
34+
35+
test "should get edit" do
36+
get :edit, id: @artist
37+
assert_response :success
38+
end
39+
40+
test "should update artist" do
41+
patch :update, id: @artist, artist: { name: @artist.name }
42+
assert_redirected_to artist_path(assigns(:artist))
43+
end
44+
45+
test "should destroy artist" do
46+
assert_difference('Artist.count', -1) do
47+
delete :destroy, id: @artist
48+
Artist.gateway.refresh_index!
49+
end
50+
51+
assert_redirected_to artists_path
52+
end
53+
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
<header>
2+
<h1>
3+
Artists
4+
<%= button_to 'New Artist', new_artist_path, method: 'get', tabindex: 5 %>
5+
</h1>
6+
</header>
7+
8+
<section id="searchbox">
9+
<%= form_tag search_path, method: 'get' do %>
10+
<input type="text" name="q" value="<%= params[:q] %>" id="q" autofocus="autofocus" placeholder="start typing to search..." tabindex="0" />
11+
<% end %>
12+
</section>
13+
14+
<section class="artists">
15+
<% @artists.each do |artist| %>
16+
<%= div_for artist, class: 'result clearfix' do %>
17+
<h2>
18+
<%= link_to artist do %>
19+
<span class="name"><%= artist.name %></span>
20+
<small><%= pluralize artist.album_count, 'album' %></small>
21+
<% end %>
22+
</h2>
23+
<div class="actions clearfix">
24+
<%= button_to 'Edit', edit_artist_path(artist), method: 'get' %>
25+
<%= button_to 'Destroy', artist, method: :delete, data: { confirm: 'Are you sure?' } %>
26+
</div>
27+
<% end %>
28+
<% end %>
29+
</section>
30+
31+
<% if @artists.empty? %>
32+
<section class="no-results">
33+
<p>The search hasn't returned any results...</p>
34+
</section>
35+
<% end %>
36+
37+
<script>
38+
$.widget( "custom.suggest", $.ui.autocomplete, {
39+
_renderMenu: function( ul, items ) {
40+
$.each( items, function( index, item ) {
41+
var category = ul.append( "<li class='ui-autocomplete-category'>" + item.label + "</li>" );
42+
43+
$.each( item.value, function( index, item ) {
44+
var li = $('<li class="ui-autocomplete-item"><a href="<%= Rails.application.config.relative_url_root %>'+ item.payload.url +'">'+ item.text +'</a></li>').data('ui-autocomplete-item', item )
45+
category.append(li)
46+
} )
47+
});
48+
}
49+
});
50+
51+
$( "#q" ).suggest({
52+
source: '<%= suggest_path %>',
53+
select: function(event, ui) {
54+
document.location.href = '<%= Rails.application.config.relative_url_root %>'+ui.item.payload.url
55+
}
56+
});
57+
</script>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
<header>
2+
<h1>
3+
<span class="back"><%= link_to "〈".html_safe, artists_path, title: "Back" %></span>
4+
<%= @artist.name %>
5+
<%= button_to 'Edit', edit_artist_path(@artist), method: 'get' %>
6+
</h1>
7+
</header>
8+
9+
<p id="notice"><%= notice %></p>
10+
11+
<section class="artist-info">
12+
<span><%= @artist.members.to_sentence last_word_connector: ' and ' %></span> |
13+
<span><%= pluralize @albums.size, 'album' %></span>
14+
<p class="artist-profile"><%= @artist.profile %></p>
15+
</section>
16+
17+
<section class="albums">
18+
<% @albums.each do |album| %>
19+
<%= div_for album, class: 'clearfix' do %>
20+
<h3>
21+
<span class="title"><%= album.title %></span>
22+
<div class="info">
23+
<small><%= album.meta.formats.join(', ') %></small>
24+
<small><%= album.released %></small>
25+
</div>
26+
</h3>
27+
28+
<div class="cover">
29+
<%= image_tag "http://ruby-demo-assets.s3.amazonaws.com/discogs/covers/#{album.id}.jpeg", width: '100px', class: 'cover' %>
30+
</div>
31+
32+
<div class="content">
33+
<% album.tracklist.in_groups_of(album.tracklist.size/2+1).each_with_index do |half, g| %>
34+
<ul class=<%= cycle 'first', 'second' %> start="<%= g < 1 ? 1 : album.tracklist.size/2+2 %>">
35+
<% half.compact.each_with_index do |track, i| %>
36+
<li>
37+
<i class="counter"><%= g < 1 ? i+1 : i+(g*album.tracklist.size/2+2) %></i>
38+
<%= track['title'] %>
39+
<small><%= track['duration'] %></small>
40+
</li>
41+
<% end %>
42+
</ul>
43+
<% end %>
44+
</div>
45+
<% end %>
46+
47+
<% end %>
48+
49+
<script>$('img').error(function(){ $(this).attr('src', '/images/blank_cover.png'); });</script>
50+
<script>$(document.location.hash).effect('highlight', 1500)</script>
51+
</section>

0 commit comments

Comments
 (0)