elasticsearch-persistence-queryable 0.1.8

Sign up to get free protection for your applications and to get access to all the features.
Files changed (99) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +17 -0
  3. data/CHANGELOG.md +16 -0
  4. data/Gemfile +4 -0
  5. data/LICENSE.txt +13 -0
  6. data/README.md +678 -0
  7. data/Rakefile +57 -0
  8. data/elasticsearch-persistence.gemspec +57 -0
  9. data/examples/music/album.rb +34 -0
  10. data/examples/music/artist.rb +50 -0
  11. data/examples/music/artists/_form.html.erb +8 -0
  12. data/examples/music/artists/artists_controller.rb +67 -0
  13. data/examples/music/artists/artists_controller_test.rb +53 -0
  14. data/examples/music/artists/index.html.erb +57 -0
  15. data/examples/music/artists/show.html.erb +51 -0
  16. data/examples/music/assets/application.css +226 -0
  17. data/examples/music/assets/autocomplete.css +48 -0
  18. data/examples/music/assets/blank_cover.png +0 -0
  19. data/examples/music/assets/form.css +113 -0
  20. data/examples/music/index_manager.rb +60 -0
  21. data/examples/music/search/index.html.erb +93 -0
  22. data/examples/music/search/search_controller.rb +41 -0
  23. data/examples/music/search/search_controller_test.rb +9 -0
  24. data/examples/music/search/search_helper.rb +15 -0
  25. data/examples/music/suggester.rb +45 -0
  26. data/examples/music/template.rb +392 -0
  27. data/examples/music/vendor/assets/jquery-ui-1.10.4.custom.min.css +7 -0
  28. data/examples/music/vendor/assets/jquery-ui-1.10.4.custom.min.js +6 -0
  29. data/examples/notes/.gitignore +7 -0
  30. data/examples/notes/Gemfile +28 -0
  31. data/examples/notes/README.markdown +36 -0
  32. data/examples/notes/application.rb +238 -0
  33. data/examples/notes/config.ru +7 -0
  34. data/examples/notes/test.rb +118 -0
  35. data/lib/elasticsearch/per_thread_registry.rb +53 -0
  36. data/lib/elasticsearch/persistence/client.rb +51 -0
  37. data/lib/elasticsearch/persistence/inheritence.rb +9 -0
  38. data/lib/elasticsearch/persistence/model/base.rb +95 -0
  39. data/lib/elasticsearch/persistence/model/callbacks.rb +37 -0
  40. data/lib/elasticsearch/persistence/model/errors.rb +9 -0
  41. data/lib/elasticsearch/persistence/model/find.rb +155 -0
  42. data/lib/elasticsearch/persistence/model/gateway_delegation.rb +23 -0
  43. data/lib/elasticsearch/persistence/model/hash_wrapper.rb +17 -0
  44. data/lib/elasticsearch/persistence/model/rails.rb +39 -0
  45. data/lib/elasticsearch/persistence/model/store.rb +271 -0
  46. data/lib/elasticsearch/persistence/model.rb +148 -0
  47. data/lib/elasticsearch/persistence/null_relation.rb +56 -0
  48. data/lib/elasticsearch/persistence/query_cache.rb +68 -0
  49. data/lib/elasticsearch/persistence/querying.rb +21 -0
  50. data/lib/elasticsearch/persistence/relation/delegation.rb +130 -0
  51. data/lib/elasticsearch/persistence/relation/finder_methods.rb +39 -0
  52. data/lib/elasticsearch/persistence/relation/merger.rb +179 -0
  53. data/lib/elasticsearch/persistence/relation/query_builder.rb +279 -0
  54. data/lib/elasticsearch/persistence/relation/query_methods.rb +362 -0
  55. data/lib/elasticsearch/persistence/relation/search_option_methods.rb +44 -0
  56. data/lib/elasticsearch/persistence/relation/spawn_methods.rb +61 -0
  57. data/lib/elasticsearch/persistence/relation.rb +110 -0
  58. data/lib/elasticsearch/persistence/repository/class.rb +71 -0
  59. data/lib/elasticsearch/persistence/repository/find.rb +73 -0
  60. data/lib/elasticsearch/persistence/repository/naming.rb +115 -0
  61. data/lib/elasticsearch/persistence/repository/response/results.rb +105 -0
  62. data/lib/elasticsearch/persistence/repository/search.rb +156 -0
  63. data/lib/elasticsearch/persistence/repository/serialize.rb +31 -0
  64. data/lib/elasticsearch/persistence/repository/store.rb +94 -0
  65. data/lib/elasticsearch/persistence/repository.rb +77 -0
  66. data/lib/elasticsearch/persistence/scoping/default.rb +137 -0
  67. data/lib/elasticsearch/persistence/scoping/named.rb +70 -0
  68. data/lib/elasticsearch/persistence/scoping.rb +52 -0
  69. data/lib/elasticsearch/persistence/version.rb +5 -0
  70. data/lib/elasticsearch/persistence.rb +157 -0
  71. data/lib/elasticsearch/rails_compatibility.rb +17 -0
  72. data/lib/rails/generators/elasticsearch/model/model_generator.rb +21 -0
  73. data/lib/rails/generators/elasticsearch/model/templates/model.rb.tt +9 -0
  74. data/lib/rails/generators/elasticsearch_generator.rb +2 -0
  75. data/lib/rails/instrumentation/railtie.rb +31 -0
  76. data/lib/rails/instrumentation.rb +10 -0
  77. data/test/integration/model/model_basic_test.rb +157 -0
  78. data/test/integration/repository/custom_class_test.rb +85 -0
  79. data/test/integration/repository/customized_class_test.rb +82 -0
  80. data/test/integration/repository/default_class_test.rb +114 -0
  81. data/test/integration/repository/virtus_model_test.rb +114 -0
  82. data/test/test_helper.rb +53 -0
  83. data/test/unit/model_base_test.rb +48 -0
  84. data/test/unit/model_find_test.rb +148 -0
  85. data/test/unit/model_gateway_test.rb +99 -0
  86. data/test/unit/model_rails_test.rb +88 -0
  87. data/test/unit/model_store_test.rb +514 -0
  88. data/test/unit/persistence_test.rb +32 -0
  89. data/test/unit/repository_class_test.rb +51 -0
  90. data/test/unit/repository_client_test.rb +32 -0
  91. data/test/unit/repository_find_test.rb +388 -0
  92. data/test/unit/repository_indexing_test.rb +37 -0
  93. data/test/unit/repository_module_test.rb +146 -0
  94. data/test/unit/repository_naming_test.rb +146 -0
  95. data/test/unit/repository_response_results_test.rb +98 -0
  96. data/test/unit/repository_search_test.rb +117 -0
  97. data/test/unit/repository_serialize_test.rb +57 -0
  98. data/test/unit/repository_store_test.rb +303 -0
  99. metadata +487 -0
@@ -0,0 +1,48 @@
1
+ .ui-autocomplete {
2
+ font-family: 'Helvetica Neue', Helvetica, sans-serif !important;
3
+ border: none !important;
4
+ border-radius: 0 !important;
5
+ background-color: #fff !important;
6
+ margin: 0 !important;
7
+ padding: 0 !important;
8
+ box-shadow: 0px 3px 3px 0px rgba(0,0,0,0.75);
9
+ }
10
+
11
+ .ui-autocomplete-category {
12
+ color: #fff;
13
+ background: #222;
14
+ font-size: 90%;
15
+ font-weight: 300;
16
+ text-transform: uppercase;
17
+ margin: 0 !important;
18
+ padding: 0.25em 0.5em 0.25em 0.5em;
19
+ }
20
+
21
+ .ui-autocomplete-item {
22
+ border-bottom: 1px solid #000;
23
+ margin: 0 !important;
24
+ padding: 0 !important;
25
+ }
26
+
27
+ .ui-autocomplete-item:hover,
28
+ .ui-autocomplete-item:focus {
29
+ color: #fff !important;
30
+ background: #0b6aff !important;
31
+ }
32
+
33
+ .ui-state-focus,
34
+ .ui-state-focus a,
35
+ .ui-state-active,
36
+ .ui-state-active a,
37
+ .ui-autocomplete-item:hover a {
38
+ color: #fff !important;
39
+ background: #0b6aff !important;
40
+ outline: none !important;
41
+ border: none !important;
42
+ border-radius: 0 !important;
43
+ }
44
+
45
+ a.ui-state-focus,
46
+ a.ui-state-active {
47
+ margin: 0px !important;
48
+ }
@@ -0,0 +1,113 @@
1
+ /* Based on https://github.com/plataformatec/simple_form/wiki/CSS-for-simple_form */
2
+
3
+ body.edit h1,
4
+ body.new h1 {
5
+ color: #999;
6
+ font-size: 100%;
7
+ text-transform: uppercase;
8
+ margin: 0 0 1em 5.5em;
9
+ }
10
+
11
+ body.edit a[href^="/artists"],
12
+ body.new a[href^="/artists"],
13
+ body.edit a[href^="/music/artists"],
14
+ body.new a[href^="/music/artists"] {
15
+ color: #222;
16
+ background: #ccc;
17
+ text-decoration: none;
18
+ border-radius: 0.3em;
19
+ padding: 0.25em 0.5em;
20
+ margin: 2em 0 0 5.5em;
21
+ display: inline-block;
22
+ }
23
+
24
+ body.edit a[href^="/artists"]:hover,
25
+ body.new a[href^="/artists"]:hover,
26
+ body.edit a[href^="/music/artists"]:hover,
27
+ body.new a[href^="/music/artists"]:hover {
28
+ color: #fff;
29
+ background: #333;
30
+ }
31
+
32
+ body.edit a[href^="/artists"]:last-child,
33
+ body.new a[href^="/artists"]:last-child,
34
+ body.edit a[href^="/music/artists"]:last-child,
35
+ body.new a[href^="/music/artists"]:last-child {
36
+ margin-left: 0;
37
+ }
38
+
39
+ .simple_form div.input {
40
+ margin-bottom: 1em;
41
+ clear: both;
42
+ }
43
+
44
+ .simple_form label {
45
+ color: #878787;
46
+ font-size: 80%;
47
+ text-transform: uppercase;
48
+ font-weight: 200;
49
+ float: left;
50
+ width: 5em;
51
+ text-align: right;
52
+ margin: 0.25em 1em;
53
+ }
54
+
55
+ div.boolean, .simple_form input[type='submit'] {
56
+ margin-left: 8.5em;
57
+ }
58
+
59
+ .field_with_errors input {
60
+ border: 2px solid #c70008 !important;
61
+ }
62
+
63
+ .simple_form .error {
64
+ color: #fff !important;
65
+ background: #c70008;
66
+ font-weight: bold;
67
+ clear: left;
68
+ display: block;
69
+ padding: 0.25em 0.5em;
70
+ margin-left: 5.6em;
71
+ width: 27.45em;
72
+ }
73
+
74
+ .simple_form .hint {
75
+ color: #878787;
76
+ font-size: 80%;
77
+ font-style: italic;
78
+ display: block;
79
+ margin: 0.25em 0 0 7em;
80
+ clear: left;
81
+ }
82
+
83
+ input {
84
+ margin: 0;
85
+ }
86
+
87
+ input.radio {
88
+ margin-right: 5px;
89
+ vertical-align: -3px;
90
+ }
91
+
92
+ input.check_boxes {
93
+ margin-left: 3px;
94
+ vertical-align: -3px;
95
+ }
96
+
97
+ label.collection_check_boxes {
98
+ float: none;
99
+ margin: 0;
100
+ vertical-align: -2px;
101
+ margin-left: 2px;
102
+ }
103
+
104
+ input.string,
105
+ textarea.text {
106
+ padding: 0.5em;
107
+ min-width: 40em;
108
+ border: 1px solid #ccc;
109
+ }
110
+
111
+ textarea.text {
112
+ min-height: 5em;
113
+ }
@@ -0,0 +1,60 @@
1
+ require 'open-uri'
2
+
3
+ class IndexManager
4
+ def self.create_index(options={})
5
+ client = Artist.gateway.client
6
+ index_name = Artist.index_name
7
+
8
+ client.indices.delete index: index_name rescue nil if options[:force]
9
+
10
+ settings = Artist.settings.to_hash.merge(Album.settings.to_hash)
11
+ mappings = Artist.mappings.to_hash.merge(Album.mappings.to_hash)
12
+
13
+ client.indices.create index: index_name,
14
+ body: {
15
+ settings: settings.to_hash,
16
+ mappings: mappings.to_hash }
17
+ end
18
+
19
+ def self.import_from_yaml(source, options={})
20
+ create_index force: true if options[:force]
21
+
22
+ input = open(source)
23
+ artists = YAML.load_documents input
24
+
25
+ artists.each do |artist|
26
+ Artist.create artist.update(
27
+ 'album_count' => artist['releases'].size,
28
+ 'members_combined' => artist['members'].join(', '),
29
+ 'suggest_name' => {
30
+ 'input' => artist['namevariations'].unshift(artist['name']),
31
+ 'output' => artist['name'],
32
+ 'payload' => { 'url' => "/artists/#{artist['id']}" }
33
+ },
34
+ 'suggest_member' => {
35
+ 'input' => artist['members'],
36
+ 'output' => artist['name'],
37
+ 'payload' => { 'url' => "/artists/#{artist['id']}" }
38
+ }
39
+ )
40
+
41
+ artist['releases'].each do |album|
42
+ album.update(
43
+ 'suggest_title' => {
44
+ 'input' => album['title'],
45
+ 'output' => album['title'],
46
+ 'payload' => { 'url' => "/artists/#{artist['id']}#album_#{album['id']}" }
47
+ },
48
+ 'suggest_track' => {
49
+ 'input' => album['tracklist'].map { |d| d['title'] },
50
+ 'output' => album['title'],
51
+ 'payload' => { 'url' => "/artists/#{artist['id']}#album_#{album['id']}" }
52
+ }
53
+ )
54
+ album['notes'] = album['notes'].to_s.gsub(/<.+?>/, '').gsub(/ {2,}/, '')
55
+ album['released'] = nil if album['released'] < 1
56
+ Album.create album, id: album['id'], parent: artist['id']
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,93 @@
1
+ <header>
2
+ <h1>
3
+ <span class="back"><%= link_to "〈".html_safe, :back, title: "Back" %></span>
4
+ Artists &amp; Albums
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
+ <%= content_tag :div, class: 'result clearfix' do %>
17
+ <h2>
18
+ <%= link_to artist do %>
19
+ <span class="name"><%= highlighted(artist, :name) %></span>
20
+ <small><%= pluralize artist.album_count, 'album' %></small>
21
+ <% end %>
22
+ </h2>
23
+ <% if highlight = highlight(artist, :members_combined) %>
24
+ <p class="small">
25
+ <span class="label">Members</span>
26
+ <%= highlight.first.html_safe %>
27
+ </p>
28
+ <% end %>
29
+ <% if highlight = highlight(artist, :profile) %>
30
+ <p class="small">
31
+ <span class="label">Profile</span>
32
+ <%= highlight.join('&hellip;').html_safe %>
33
+ </p>
34
+ <% end %>
35
+ <% end %>
36
+ <% end %>
37
+ </section>
38
+
39
+ <section class="albums">
40
+ <% @albums.each do |album| %>
41
+ <%= content_tag :div, class: 'result clearfix' do %>
42
+ <h2>
43
+ <%= link_to artist_path(album.artist_id, anchor: "album_#{album.id}") do %>
44
+ <span class="name"><%= highlighted(album, :title) %></span>
45
+ <small><%= album.artist %></small>
46
+ <small>(<%= [album.meta.formats.first, album.released].compact.join(' ') %>)</small>
47
+ <% end %>
48
+ </h2>
49
+
50
+ <% if highlight = highlight(album, 'tracklist.title') %>
51
+ <p class="small">
52
+ <span class="label">Tracks</span>
53
+ <%= highlight.join('&hellip;').html_safe %>
54
+ </p>
55
+ <% end %>
56
+
57
+ <% if highlight = highlight(album, :notes) %>
58
+ <p class="small">
59
+ <span class="label">Notes</span>
60
+ <%= highlight.map { |d| d.gsub(/^\.\s?/, '') }.join('&hellip;').html_safe %>
61
+ </p>
62
+ <% end %>
63
+ <% end %>
64
+ <% end %>
65
+ </section>
66
+
67
+ <% if @artists.empty? && @albums.empty? %>
68
+ <section class="no-results">
69
+ <p>The search hasn't returned any results...</p>
70
+ </section>
71
+ <% end %>
72
+
73
+ <script>
74
+ $.widget( "custom.suggest", $.ui.autocomplete, {
75
+ _renderMenu: function( ul, items ) {
76
+ $.each( items, function( index, item ) {
77
+ var category = ul.append( "<li class='ui-autocomplete-category'>" + item.label + "</li>" );
78
+
79
+ $.each( item.value, function( index, item ) {
80
+ 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 )
81
+ category.append(li)
82
+ } )
83
+ });
84
+ }
85
+ });
86
+
87
+ $( "#q" ).suggest({
88
+ source: '<%= suggest_path %>',
89
+ select: function(event, ui) {
90
+ document.location.href = '<%= Rails.application.config.relative_url_root %>'+ui.item.payload.url
91
+ }
92
+ });
93
+ </script>
@@ -0,0 +1,41 @@
1
+ class SearchController < ApplicationController
2
+
3
+ def index
4
+ tags = { pre_tags: '<em class="hl">', post_tags: '</em>' }
5
+ @artists = Artist.search \
6
+ query: {
7
+ multi_match: {
8
+ query: params[:q],
9
+ fields: ['name^10','members^2','profile']
10
+ }
11
+ },
12
+ highlight: {
13
+ tags_schema: 'styled',
14
+ fields: {
15
+ name: { number_of_fragments: 0 },
16
+ members_combined: { number_of_fragments: 0 },
17
+ profile: { fragment_size: 50 }
18
+ }
19
+ }
20
+
21
+ @albums = Album.search \
22
+ query: {
23
+ multi_match: {
24
+ query: params[:q],
25
+ fields: ['title^100','tracklist.title^10','notes^1']
26
+ }
27
+ },
28
+ highlight: {
29
+ tags_schema: 'styled',
30
+ fields: {
31
+ title: { number_of_fragments: 0 },
32
+ 'tracklist.title' => { number_of_fragments: 0 },
33
+ notes: { fragment_size: 50 }
34
+ }
35
+ }
36
+ end
37
+
38
+ def suggest
39
+ render json: Suggester.new(params)
40
+ end
41
+ end
@@ -0,0 +1,9 @@
1
+ require 'test_helper'
2
+
3
+ class SearchControllerTest < ActionController::TestCase
4
+ test "should get suggest" do
5
+ get :suggest
6
+ assert_response :success
7
+ end
8
+
9
+ end
@@ -0,0 +1,15 @@
1
+ module SearchHelper
2
+
3
+ def highlight(object, field)
4
+ object.try(:hit).try(:highlight).try(field)
5
+ end
6
+
7
+ def highlighted(object, field)
8
+ if h = object.try(:hit).try(:highlight).try(field).try(:first)
9
+ h.html_safe
10
+ else
11
+ field.to_s.split('.').reduce(object) { |result,item| result.try(item) }
12
+ end
13
+ end
14
+
15
+ end
@@ -0,0 +1,45 @@
1
+ class Suggester
2
+ attr_reader :response
3
+
4
+ def initialize(params={})
5
+ @term = params[:term]
6
+ end
7
+
8
+ def response
9
+ @response ||= begin
10
+ Elasticsearch::Persistence.client.suggest \
11
+ index: Artist.index_name,
12
+ body: {
13
+ artists: {
14
+ text: @term,
15
+ completion: { field: 'suggest_name' }
16
+ },
17
+ members: {
18
+ text: @term,
19
+ completion: { field: 'suggest_member' }
20
+ },
21
+ albums: {
22
+ text: @term,
23
+ completion: { field: 'suggest_title' }
24
+ },
25
+ tracks: {
26
+ text: @term,
27
+ completion: { field: 'suggest_track' }
28
+ }
29
+ }
30
+ end
31
+ end
32
+
33
+ def as_json(options={})
34
+ response
35
+ .except('_shards')
36
+ .reduce([]) do |sum,d|
37
+ # category = { d.first => d.second.first['options'] }
38
+ item = { :label => d.first.titleize, :value => d.second.first['options'] }
39
+ sum << item
40
+ end
41
+ .reject do |d|
42
+ d[:value].empty?
43
+ end
44
+ end
45
+ end