elasticsearch-rails 0.1.6 → 0.1.7

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 702e99d736daeaca35a28b5c9b0410c22076eee2
4
- data.tar.gz: f0bcd67eb479b7eff5b9324ca5b3c646dc6931c9
3
+ metadata.gz: 6acf0a64b1de80131443fe7818364e938af6e466
4
+ data.tar.gz: f234b64f5d8eda579694406c216bffb37d08a991
5
5
  SHA512:
6
- metadata.gz: 8191b30a62f1f5dffa9d4efa6b0a0c1b08beb9992f8cc52036c427886f7da1ff51e191a8e39df62e5f29c63d0495c47860d7c2724050a119eb7534adf2f0c08a
7
- data.tar.gz: 91710af6b69d1bf1360b01994949f7e6cb5515006cb78242c9986bf66c7acc69a943563d841dec322d7cc4a93e556ec9f809441318f6501e7e7f073f836fe887
6
+ metadata.gz: efc3dc4312de0805bd9742029a4b1e09b1b03fffbb3460e4bfb25b7e897d21d7d3f3a5c5bb28f4606ec24438584d45d096a1d51c1f9cf2403508cfd42a3190f7
7
+ data.tar.gz: e50eee5092e29fa9c24fcf96ad64ccc791008ce1b2eb9d2df1e5ddad860150b468d66451ea77c79f220159aa3432d9e7c34d338e6c2a64394d5f549817dba93c
@@ -1,3 +1,13 @@
1
+ # 0.1.7
2
+
3
+ * Updated dependencies for the gem and example applications
4
+ * Fixed various small errors in the `01-basic.rb` template
5
+ * Fixed error when inserting the Kaminari gem into Gemfile in the 02-pretty.rb template
6
+ * Fixed incorrect regex for adding Rails instrumentation into the application.rb in the `02-pretty.rb` template
7
+ * Fixed other small errors in the `02-pretty.rb` template
8
+ * Improved and added tests for the generated application from the `02-pretty.rb` template
9
+ * Added the `04-dsl.rb` template which uses the `elasticsearch-dsl` gem to build the search definition
10
+
1
11
  ## 0.1.6
2
12
 
3
13
  * Fixed errors in templates for the Rails example applications
@@ -30,7 +30,7 @@ Gem::Specification.new do |s|
30
30
  s.add_development_dependency "elasticsearch-model"
31
31
 
32
32
  s.add_development_dependency "oj"
33
- s.add_development_dependency "rails", "> 3.0"
33
+ s.add_development_dependency "rails", ">= 3.1"
34
34
 
35
35
  s.add_development_dependency "lograge"
36
36
 
@@ -1,5 +1,5 @@
1
1
  module Elasticsearch
2
2
  module Rails
3
- VERSION = "0.1.6"
3
+ VERSION = "0.1.7"
4
4
  end
5
5
  end
@@ -115,8 +115,7 @@ end
115
115
 
116
116
  # ----- Auxiliary gems ----------------------------------------------------------------------------
117
117
 
118
- gem 'turn', group: 'test'
119
- gem 'mocha', group: 'test', require: 'mocha/setup'
118
+ gem 'mocha', group: 'test', require: 'mocha/api'
120
119
 
121
120
  # ----- Remove CoffeeScript, Sass and "all that jazz" ---------------------------------------------
122
121
 
@@ -209,9 +208,10 @@ inject_into_file 'app/controllers/articles_controller.rb', before: %r|^\s*# GET
209
208
  CODE
210
209
  end
211
210
 
212
- inject_into_file 'app/views/articles/index.html.erb', after: %r{<h1>Listing articles</h1>} do
211
+ inject_into_file 'app/views/articles/index.html.erb', after: %r{<h1>Listing articles</h1>}i do
213
212
  <<-CODE
214
213
 
214
+
215
215
  <hr>
216
216
 
217
217
  <%= form_tag search_articles_path, method: 'get' do %>
@@ -221,7 +221,6 @@ inject_into_file 'app/views/articles/index.html.erb', after: %r{<h1>Listing arti
221
221
  <% end %>
222
222
 
223
223
  <hr>
224
-
225
224
  CODE
226
225
  end
227
226
 
@@ -29,8 +29,8 @@ say_status "Rubygems", "Adding Rails logger integration...\n", :yellow
29
29
  puts '-'*80, ''; sleep 0.25
30
30
 
31
31
  insert_into_file 'config/application.rb',
32
- "\n\nrequire 'elasticsearch/rails/instrumentation'\n",
33
- after: 'Bundler.require(:default, Rails.env)'
32
+ "\n\nrequire 'elasticsearch/rails/instrumentation'",
33
+ after: /Bundler\.require.+$/
34
34
 
35
35
  git add: "config/application.rb"
36
36
  git commit: "-m 'Added the Rails logger integration to application.rb'"
@@ -43,7 +43,7 @@ puts '-'*80, ''; sleep 0.25
43
43
 
44
44
  # NOTE: Kaminari has to be loaded before Elasticsearch::Model so the callbacks are executed
45
45
  #
46
- insert_into_file 'Gemfile', <<-CODE, before: 'gem "elasticsearch"'
46
+ insert_into_file 'Gemfile', <<-CODE, before: /gem ["']elasticsearch["'].+$/
47
47
 
48
48
  # NOTE: Kaminari has to be loaded before Elasticsearch::Model so the callbacks are executed
49
49
  gem 'kaminari'
@@ -86,6 +86,14 @@ insert_into_file 'app/models/article.rb', <<-CODE, after: 'include Elasticsearch
86
86
  end
87
87
  CODE
88
88
 
89
+ insert_into_file "#{Rails::VERSION::STRING > '4' ? 'test/models' : 'test/unit' }/article_test.rb", <<-CODE, after: /class ArticleTest < ActiveSupport::TestCase$/
90
+
91
+ teardown do
92
+ Article.__elasticsearch__.unstub(:search)
93
+ end
94
+
95
+ CODE
96
+
89
97
  gsub_file "#{Rails::VERSION::STRING > '4' ? 'test/models' : 'test/unit' }/article_test.rb", %r{# test "the truth" do.*?# end}m, <<-CODE
90
98
 
91
99
  test "has a search method delegating to __elasticsearch__" do
@@ -154,6 +154,8 @@ class Article < ActiveRecord::Base
154
154
  end
155
155
  CODE
156
156
 
157
+ gsub_file "#{Rails::VERSION::STRING > '4' ? 'test/models' : 'test/unit' }/article_test.rb", %r{assert_equal 'foo', definition\[:query\]\[:multi_match\]\[:query\]}, "assert_equal 'foo', definition.to_hash[:query][:bool][:should][0][:multi_match][:query]"
158
+
157
159
  # copy_file File.expand_path('../searchable.rb', __FILE__), 'app/models/concerns/searchable.rb'
158
160
  get 'https://raw.github.com/elasticsearch/elasticsearch-rails/templates/elasticsearch-rails/lib/rails/templates/searchable.rb',
159
161
  'app/models/concerns/searchable.rb'
@@ -170,7 +172,7 @@ insert_into_file "app/models/article.rb", after: "ActiveRecord::Base" do
170
172
  CODE
171
173
  end
172
174
 
173
- git add: "app/models/"
175
+ git add: "app/models/ test/models"
174
176
  git commit: "-m 'Refactored the Elasticsearch integration into a concern\n\nSee:\n\n* http://37signals.com/svn/posts/3372-put-chubby-models-on-a-diet-with-concerns\n* http://joshsymonds.com/blog/2012/10/25/rails-concerns-v-searchable-with-elasticsearch/'"
175
177
 
176
178
  # ----- Add Sidekiq indexer -----------------------------------------------------------------------
@@ -199,8 +201,6 @@ puts '-'*80, ''; sleep 0.25
199
201
  create_file 'app/controllers/search_controller.rb' do
200
202
  <<-CODE.gsub(/^ /, '')
201
203
  class SearchController < ApplicationController
202
- respond_to :json, :html
203
-
204
204
  def index
205
205
  options = {
206
206
  category: params[:c],
@@ -211,15 +211,16 @@ create_file 'app/controllers/search_controller.rb' do
211
211
  comments: params[:comments]
212
212
  }
213
213
  @articles = Article.search(params[:q], options).page(params[:page]).results
214
-
215
- respond_with @articles
216
214
  end
217
-
218
215
  end
219
216
 
220
217
  CODE
221
218
  end
222
219
 
220
+ copy_file File.expand_path('../search_controller_test.rb', __FILE__), 'test/controllers/search_controller_test.rb'
221
+ # get 'https://raw.github.com/elasticsearch/elasticsearch-rails/templates/elasticsearch-rails/lib/rails/templates/search_controller_test.rb',
222
+ 'test/controllers/search_controller_test.rb'
223
+
223
224
  route "get '/search', to: 'search#index', as: 'search'"
224
225
  gsub_file 'config/routes.rb', %r{root to: 'articles#index'$}, "root to: 'search#index'"
225
226
 
@@ -231,7 +232,7 @@ get 'https://raw.github.com/elasticsearch/elasticsearch-rails/templates/elastics
231
232
  get 'https://raw.github.com/elasticsearch/elasticsearch-rails/templates/elasticsearch-rails/lib/rails/templates/search.css',
232
233
  'app/assets/stylesheets/search.css'
233
234
 
234
- git add: "app/controllers/ config/routes.rb"
235
+ git add: "app/controllers/ test/controllers/ config/routes.rb"
235
236
  git add: "app/views/search/ app/assets/stylesheets/search.css"
236
237
  git commit: "-m 'Added SearchController#index'"
237
238
 
@@ -0,0 +1,128 @@
1
+ # $ rails new searchapp --skip --skip-bundle --template https://raw.githubusercontent.com/elastic/elasticsearch-rails/master/elasticsearch-rails/lib/rails/templates/04-dsl.rb
2
+
3
+ # (See: 01-basic.rb, 02-pretty.rb, 03-expert.rb)
4
+
5
+ append_to_file 'README.rdoc', <<-README
6
+
7
+ == [4] DSL
8
+
9
+ The `dsl` template refactors the search definition in SearchController#index
10
+ to use the [`elasticsearch-dsl`](https://github.com/elastic/elasticsearch-ruby/tree/dsl/elasticsearch-dsl)
11
+ Rubygem for better expresivity and readability of the code.
12
+
13
+ README
14
+
15
+ git add: "README.rdoc"
16
+ git commit: "-m '[03] Updated the application README'"
17
+
18
+ run 'rm -f app/assets/stylesheets/*.scss'
19
+ run 'rm -f app/assets/javascripts/*.coffee'
20
+
21
+ # ----- Add gems into Gemfile ---------------------------------------------------------------------
22
+
23
+ puts
24
+ say_status "Rubygems", "Adding Rubygems into Gemfile...\n", :yellow
25
+ puts '-'*80, ''; sleep 0.25
26
+
27
+ gem "elasticsearch-dsl", git: "git://github.com/elastic/elasticsearch-ruby.git"
28
+
29
+ git add: "Gemfile*"
30
+ git commit: "-m 'Added the `elasticsearch-dsl` gem'"
31
+
32
+ # ----- Run bundle install ------------------------------------------------------------------------
33
+
34
+ run "bundle install"
35
+
36
+ # ----- Change the search definition implementation and associated views and tests ----------------
37
+
38
+ # copy_file File.expand_path('../searchable.dsl.rb', __FILE__), 'app/models/concerns/searchable.rb', force: true
39
+ get 'https://raw.githubusercontent.com/elastic/elasticsearch-rails/master/elasticsearch-rails/lib/rails/templates/searchable.dsl.rb',
40
+ 'app/models/concerns/searchable.rb'
41
+
42
+ # copy_file File.expand_path('../index.html.dsl.erb', __FILE__), 'app/views/search/index.html.erb', force: true
43
+ get 'https://raw.githubusercontent.com/elastic/elasticsearch-rails/master/elasticsearch-rails/lib/rails/templates/index.html.dsl.erb',
44
+ 'app/views/search/index.html.erb'
45
+
46
+ gsub_file "test/controllers/search_controller_test.rb", %r{test "should return facets" do.*?end}m, <<-CODE
47
+ test "should return aggregations" do
48
+ get :index, q: 'one'
49
+ assert_response :success
50
+
51
+ aggregations = assigns(:articles).response.response['aggregations']
52
+
53
+ assert_equal 2, aggregations['categories']['categories']['buckets'].size
54
+ assert_equal 2, aggregations['authors']['authors']['buckets'].size
55
+ assert_equal 2, aggregations['published']['published']['buckets'].size
56
+
57
+ assert_equal 'John Smith', aggregations['authors']['authors']['buckets'][0]['key']
58
+ assert_equal 'One', aggregations['categories']['categories']['buckets'][0]['key']
59
+ assert_equal '2015-03-02T00:00:00.000Z', aggregations['published']['published']['buckets'][0]['key_as_string']
60
+ end
61
+ CODE
62
+
63
+ gsub_file "test/controllers/search_controller_test.rb", %r{test "should filter search results and the author and published date facets when user selects a category" do.*?end}m, <<-CODE
64
+ test "should filter search results and the author and published date facets when user selects a category" do
65
+ get :index, q: 'one', c: 'One'
66
+ assert_response :success
67
+
68
+ assert_equal 2, assigns(:articles).size
69
+
70
+ aggregations = assigns(:articles).response.response['aggregations']
71
+
72
+ assert_equal 1, aggregations['authors']['authors']['buckets'].size
73
+ assert_equal 1, aggregations['published']['published']['buckets'].size
74
+
75
+ # Do NOT filter the category facet
76
+ assert_equal 2, aggregations['categories']['categories']['buckets'].size
77
+ end
78
+ CODE
79
+
80
+ gsub_file "test/controllers/search_controller_test.rb", %r{test "should filter search results and the category and published date facets when user selects a category" do.*?end}m, <<-CODE
81
+ test "should filter search results and the category and published date facets when user selects a category" do
82
+ get :index, q: 'one', a: 'Mary Smith'
83
+ assert_response :success
84
+
85
+ assert_equal 1, assigns(:articles).size
86
+
87
+ aggregations = assigns(:articles).response.response['aggregations']
88
+
89
+ assert_equal 1, aggregations['categories']['categories']['buckets'].size
90
+ assert_equal 1, aggregations['published']['published']['buckets'].size
91
+
92
+ # Do NOT filter the authors facet
93
+ assert_equal 2, aggregations['authors']['authors']['buckets'].size
94
+ end
95
+ CODE
96
+
97
+ git add: "app/models/concerns/ app/views/search/ test/controllers/search_controller_test.rb"
98
+ git commit: "-m 'Updated the Article.search method to use the Ruby DSL and updated the associated views and tests'"
99
+
100
+ # ----- Print Git log -----------------------------------------------------------------------------
101
+
102
+ puts
103
+ say_status "Git", "Details about the application:", :yellow
104
+ puts '-'*80, ''
105
+
106
+ git tag: "dsl"
107
+ git log: "--reverse --oneline HEAD...expert"
108
+
109
+ # ----- Start the application ---------------------------------------------------------------------
110
+
111
+ unless ENV['RAILS_NO_SERVER_START']
112
+ require 'net/http'
113
+ if (begin; Net::HTTP.get(URI('http://localhost:3000')); rescue Errno::ECONNREFUSED; false; rescue Exception; true; end)
114
+ puts "\n"
115
+ say_status "ERROR", "Some other application is running on port 3000!\n", :red
116
+ puts '-'*80
117
+
118
+ port = ask("Please provide free port:", :bold)
119
+ else
120
+ port = '3000'
121
+ end
122
+
123
+ puts "", "="*80
124
+ say_status "DONE", "\e[1mStarting the application. Open http://localhost:#{port}\e[0m", :yellow
125
+ puts "="*80, ""
126
+
127
+ run "rails server --port=#{port}"
128
+ end
@@ -0,0 +1,160 @@
1
+ <div class="col-md-12">
2
+ <h1 class="text-right"><%= link_to 'Search New York Times articles', root_path %></h1>
3
+
4
+ <%= form_tag search_path, method: 'get', role: 'search' do %>
5
+ <div class="input-group">
6
+ <%= text_field_tag :q, params[:q], class: 'form-control', placeholder: 'Search...' %>
7
+
8
+ <span class="input-group-btn">
9
+ <button type="submit" class="btn btn-default">
10
+ <span class="glyphicon glyphicon-search"></span>
11
+ </button>
12
+ </span>
13
+ </div>
14
+
15
+ <div id="form-options" class="clearfix">
16
+ <div class="btn-group pull-left">
17
+ <label class="checkbox-inline">
18
+ <%= check_box_tag 'comments', 'y', params[:comments] == 'y', onclick: "$(this).closest('form').submit()" %>
19
+ Search in comments?
20
+ </label>
21
+ <% params.slice(:a, :c, :s).each do |name, value| %>
22
+ <%= hidden_field_tag name, value %>
23
+ <% end %>
24
+ </div>
25
+
26
+ <div class="btn-group pull-right">
27
+ <p style="float: left; margin: 0.1em 0 0 0"><small>Displaying <%= (params[:page] || 1).to_i.ordinalize %> page with <%= @articles.size %> articles
28
+ of <strong>total <%= @articles.total %></strong></small></p>
29
+
30
+ <button class="btn btn-default btn-xs dropdown-toggle" type="button" data-toggle="dropdown" style="margin-left: 0.5em">
31
+ <% sort = case
32
+ when params[:s] then params[:s]
33
+ when params[:q].blank? then 'published_on'
34
+ else 'relevancy'
35
+ end
36
+ %>
37
+ sorted by <%= sort.humanize.downcase %> <span class="caret"></span>
38
+ </button>
39
+ <ul class="dropdown-menu" role="menu">
40
+ <li><%= link_to "Sort by published on", search_path(params.except(:controller, :action).merge(s: 'published_on')), class: 'btn-xs' %></li>
41
+ <li><%= link_to "Sort by relevancy", search_path(params.except(:controller, :action).merge(s: nil)), class: 'btn-xs' %></li>
42
+ </ul>
43
+ </div>
44
+ </div>
45
+ <% end %>
46
+
47
+ <hr>
48
+ </div>
49
+
50
+ <% if @articles.size < 1 && (suggestions = @articles.response.response['suggest']) && suggestions.present? %>
51
+ <div class="col-md-12">
52
+ <p class="alert alert-warning">
53
+ No documents have been found.
54
+ <% if suggestions['suggest_title'].present? || suggestions['suggest_body'].present? %>
55
+ Maybe you mean
56
+ <%= suggestions.map { |k,v| v.first['options'] }.flatten.map {|v| v['text']}.uniq.map do |term|
57
+ link_to term, search_path(params.except(:controller, :action).merge q: term)
58
+ end.to_sentence(last_word_connector: ' or ').html_safe %>?
59
+ <% end %>
60
+ </p>
61
+ </div>
62
+ <% end %>
63
+
64
+ <div id="facets" class="col-md-3">
65
+ <% unless @articles.size < 1 %>
66
+
67
+ <div class="categories panel panel-default">
68
+ <p class="panel-heading"><%= link_to 'All Sections &rarr;'.html_safe, search_path(params.except(:controller, :action).merge(c: nil))%></p>
69
+
70
+ <div class="list-group">
71
+ <% @articles.response.response['aggregations']['categories']['categories']['buckets'].each do |c| %>
72
+ <%=
73
+ link_to search_path(params.except(:controller, :action).merge(c: c['key'])),
74
+ class: "list-group-item#{' active' if params[:c] == c['key']}" do
75
+ c['key'].titleize.html_safe + content_tag(:small, c['doc_count'], class: 'badge').html_safe
76
+ end
77
+ %>
78
+ <% end %>
79
+ </div>
80
+ </div>
81
+
82
+ <div class="authors panel panel-default">
83
+ <p class="panel-heading"><%= link_to 'All Authors &rarr;'.html_safe, search_path(params.except(:controller, :action).merge(a: nil))%></p>
84
+
85
+ <div class="list-group">
86
+ <% @articles.response.response['aggregations']['authors']['authors']['buckets'].each do |a| %>
87
+ <%=
88
+ link_to search_path(params.except(:controller, :action).merge(a: a['key'])),
89
+ class: "list-group-item#{' active' if params[:a] == a['key']}" do
90
+ a['key'].titleize.html_safe + content_tag(:small, a['doc_count'], class: 'badge').html_safe
91
+ end
92
+ %>
93
+ <% end %>
94
+ </div>
95
+ </div>
96
+
97
+ <div class="authors panel panel-default">
98
+ <p class="panel-heading"><%= link_to 'Any Date &rarr;'.html_safe, search_path(params.except(:controller, :action).merge(w: nil))%></p>
99
+
100
+ <div class="list-group">
101
+ <% @articles.response.response['aggregations']['published']['published']['buckets'].each do |w| %>
102
+ <%=
103
+ __start = Time.at(w['key']/1000)
104
+ __end = __start.end_of_week
105
+ __date = __start.to_date.to_s(:iso)
106
+
107
+ link_to search_path(params.except(:controller, :action).merge(w: __date)),
108
+ class: "list-group-item#{' active' if params[:w] == __date}" do
109
+ "#{__start.to_date.to_s(:short)} &mdash; #{__end.to_date.to_s(:short)}".html_safe + \
110
+ content_tag(:small, w['doc_count'], class: 'badge').html_safe
111
+ end
112
+ %>
113
+ <% end %>
114
+ </div>
115
+ </div>
116
+ <% end %>
117
+ </div>
118
+
119
+ <div class="col-md-9">
120
+ <div id="results">
121
+ <% @articles.each do |article| %>
122
+ <div class="result">
123
+ <h3 class="title">
124
+ <%= (article.try(:highlight).try(:title) ? article.highlight.title.join.html_safe : article.title) %>
125
+ <small class="category"><%= article.categories.to_sentence %></small>
126
+ </h3>
127
+
128
+ <p class="body">
129
+ <% if article.try(:highlight).try(:abstract) %>
130
+ <%= article.highlight.abstract.join.html_safe %>
131
+ <% else %>
132
+ <%= article.try(:highlight).try(:content) ? article.highlight.content.join('&hellip;').html_safe : article.abstract %>
133
+ <% end %>
134
+ </p>
135
+
136
+ <% if comments = article.try(:highlight) && article.highlight['comments.body'] %>
137
+ <p class="comments">
138
+ Comments: <%= comments.join('&hellip;').html_safe %>
139
+ </p>
140
+ <% end %>
141
+
142
+ <p class="text-muted">
143
+ <small>Authors: <%= article.authors.map(&:full_name).to_sentence %></small> |
144
+ <small>Published: <%= article.published_on %></small> |
145
+ <small>Score: <%= article._score %></small>
146
+ </p>
147
+ </div>
148
+ <% end %>
149
+ </div>
150
+
151
+ <ul class="pager">
152
+ <li class="previous"><%= link_to_previous_page @articles, 'Previous Page', params: params.slice(:q, :c, :a, :comments) %></li>
153
+ <li class="next"><%= link_to_next_page @articles, 'Next Page', params: params.slice(:q, :c, :a, :comments) %></li>
154
+ </ul>
155
+
156
+ </div>
157
+
158
+ <div class="footer <%= @articles.size < 1 ? 'col-md-12' : 'col-md-9 col-md-offset-3' %>">
159
+ <p><small>Content provided by <a href="http://nytimes.com"><em>The New York Times</em></a>.</small></p>
160
+ </div>
@@ -37,8 +37,8 @@
37
37
  sorted by <%= sort.humanize.downcase %> <span class="caret"></span>
38
38
  </button>
39
39
  <ul class="dropdown-menu" role="menu">
40
- <li><%= link_to "Sort by published on", search_path(params.merge(s: 'published_on')), class: 'btn-xs' %></li>
41
- <li><%= link_to "Sort by relevancy", search_path(params.merge(s: nil)), class: 'btn-xs' %></li>
40
+ <li><%= link_to "Sort by published on", search_path(params.except(:controller, :action).merge(s: 'published_on')), class: 'btn-xs' %></li>
41
+ <li><%= link_to "Sort by relevancy", search_path(params.except(:controller, :action).merge(s: nil)), class: 'btn-xs' %></li>
42
42
  </ul>
43
43
  </div>
44
44
  </div>
@@ -54,7 +54,7 @@
54
54
  <% if suggestions['suggest_title'].present? || suggestions['suggest_body'].present? %>
55
55
  Maybe you mean
56
56
  <%= suggestions.map { |k,v| v.first['options'] }.flatten.map {|v| v['text']}.uniq.map do |term|
57
- link_to term, search_path(params.merge q: term)
57
+ link_to term, search_path(params.except(:controller, :action).merge q: term)
58
58
  end.to_sentence(last_word_connector: ' or ').html_safe %>?
59
59
  <% end %>
60
60
  </p>
@@ -65,12 +65,12 @@
65
65
  <% unless @articles.size < 1 %>
66
66
 
67
67
  <div class="categories panel panel-default">
68
- <p class="panel-heading"><%= link_to 'All Sections &rarr;'.html_safe, search_path(params.merge(c: nil))%></p>
68
+ <p class="panel-heading"><%= link_to 'All Sections &rarr;'.html_safe, search_path(params.except(:controller, :action).merge(c: nil))%></p>
69
69
 
70
70
  <div class="list-group">
71
71
  <% @articles.response.response['facets']['categories']['terms'].each do |c| %>
72
72
  <%=
73
- link_to search_path(params.merge(c: c['term'])),
73
+ link_to search_path(params.except(:controller, :action).merge(c: c['term'])),
74
74
  class: "list-group-item#{' active' if params[:c] == c['term']}" do
75
75
  c['term'].titleize.html_safe + content_tag(:small, c['count'], class: 'badge').html_safe
76
76
  end
@@ -80,12 +80,12 @@
80
80
  </div>
81
81
 
82
82
  <div class="authors panel panel-default">
83
- <p class="panel-heading"><%= link_to 'All Authors &rarr;'.html_safe, search_path(params.merge(a: nil))%></p>
83
+ <p class="panel-heading"><%= link_to 'All Authors &rarr;'.html_safe, search_path(params.except(:controller, :action).merge(a: nil))%></p>
84
84
 
85
85
  <div class="list-group">
86
86
  <% @articles.response.response['facets']['authors']['terms'].each do |a| %>
87
87
  <%=
88
- link_to search_path(params.merge(a: a['term'])),
88
+ link_to search_path(params.except(:controller, :action).merge(a: a['term'])),
89
89
  class: "list-group-item#{' active' if params[:a] == a['term']}" do
90
90
  a['term'].titleize.html_safe + content_tag(:small, a['count'], class: 'badge').html_safe
91
91
  end
@@ -95,7 +95,7 @@
95
95
  </div>
96
96
 
97
97
  <div class="authors panel panel-default">
98
- <p class="panel-heading"><%= link_to 'Any Date &rarr;'.html_safe, search_path(params.merge(w: nil))%></p>
98
+ <p class="panel-heading"><%= link_to 'Any Date &rarr;'.html_safe, search_path(params.except(:controller, :action).merge(w: nil))%></p>
99
99
 
100
100
  <div class="list-group">
101
101
  <% @articles.response.response['facets']['published']['entries'].each do |w| %>
@@ -104,7 +104,7 @@
104
104
  __end = __start.end_of_week
105
105
  __date = __start.to_date.to_s(:iso)
106
106
 
107
- link_to search_path(params.merge(w: __date)),
107
+ link_to search_path(params.except(:controller, :action).merge(w: __date)),
108
108
  class: "list-group-item#{' active' if params[:w] == __date}" do
109
109
  "#{__start.to_date.to_s(:short)} &mdash; #{__end.to_date.to_s(:short)}".html_safe + \
110
110
  content_tag(:small, w['count'], class: 'badge').html_safe
@@ -0,0 +1,130 @@
1
+ require 'test_helper'
2
+
3
+ class SearchControllerTest < ActionController::TestCase
4
+ setup do
5
+ Time.stubs(:now).returns(Time.parse('2015-03-16 10:00:00 UTC'))
6
+
7
+ Article.delete_all
8
+
9
+ articles = [
10
+ { title: 'Article One', abstract: 'One', content: 'One', published_on: 1.day.ago, category_title: 'One', author_first_name: 'John', author_last_name: 'Smith' },
11
+ { title: 'Article One Another', abstract: '', content: '', published_on: 2.days.ago, category_title: 'One', author_first_name: 'John', author_last_name: 'Smith' },
12
+ { title: 'Article One Two', abstract: '', content: '', published_on: 10.days.ago, category_title: 'Two', author_first_name: 'Mary', author_last_name: 'Smith' },
13
+ { title: 'Article Two', abstract: '', content: '', published_on: 12.days.ago, category_title: 'Two', author_first_name: 'Mary', author_last_name: 'Smith' },
14
+ { title: 'Article Three', abstract: '', content: '', published_on: 12.days.ago, category_title: 'Three', author_first_name: 'Alice', author_last_name: 'Smith' }
15
+ ]
16
+
17
+ articles.each do |a|
18
+ article = Article.create! \
19
+ title: a[:title],
20
+ abstract: a[:abstract],
21
+ content: a[:content],
22
+ published_on: a[:published_on]
23
+
24
+ article.categories << Category.find_or_create_by!(title: a[:category_title])
25
+
26
+ article.authors << Author.find_or_create_by!(first_name: a[:author_first_name], last_name: a[:author_last_name])
27
+
28
+ article.save!
29
+ end
30
+
31
+ Article.find_by_title('Article Three').comments.create body: 'One'
32
+
33
+ Sidekiq::Queue.new("elasticsearch").clear
34
+
35
+ Article.__elasticsearch__.import force: true
36
+ Article.__elasticsearch__.refresh_index!
37
+ end
38
+
39
+ test "should return search results" do
40
+ get :index, q: 'one'
41
+ assert_response :success
42
+ assert_equal 3, assigns(:articles).size
43
+ end
44
+
45
+ test "should return search results in comments" do
46
+ get :index, q: 'one', comments: 'y'
47
+ assert_response :success
48
+ assert_equal 4, assigns(:articles).size
49
+ end
50
+
51
+ test "should return highlighted snippets" do
52
+ get :index, q: 'one'
53
+ assert_response :success
54
+ assert_match %r{<em class="label label-highlight">One</em>}, assigns(:articles).first.highlight.title.first
55
+ end
56
+
57
+ test "should return suggestions" do
58
+ get :index, q: 'one'
59
+ assert_response :success
60
+
61
+ suggestions = assigns(:articles).response.response['suggest']
62
+
63
+ assert_equal 'one', suggestions['suggest_title'][0]['text']
64
+ end
65
+
66
+ test "should return aggregations" do
67
+ get :index, q: 'one'
68
+ assert_response :success
69
+
70
+ aggregations = assigns(:articles).response.response['aggregations']
71
+
72
+ assert_equal 2, aggregations['categories']['categories']['buckets'].size
73
+ assert_equal 2, aggregations['authors']['authors']['buckets'].size
74
+ assert_equal 2, aggregations['published']['published']['buckets'].size
75
+
76
+ assert_equal 'John Smith', aggregations['authors']['authors']['buckets'][0]['key']
77
+ assert_equal 'One', aggregations['categories']['categories']['buckets'][0]['key']
78
+ assert_equal '2015-03-02T00:00:00.000Z', aggregations['published']['published']['buckets'][0]['key_as_string']
79
+ end
80
+
81
+ test "should sort on the published date" do
82
+ get :index, q: 'one', s: 'published_on'
83
+ assert_response :success
84
+
85
+ assert_equal 3, assigns(:articles).size
86
+ assert_equal '2015-03-15', assigns(:articles)[0].published_on
87
+ assert_equal '2015-03-14', assigns(:articles)[1].published_on
88
+ assert_equal '2015-03-06', assigns(:articles)[2].published_on
89
+ end
90
+
91
+ test "should sort on the published date when no query is provided" do
92
+ get :index, q: ''
93
+ assert_response :success
94
+
95
+ assert_equal 5, assigns(:articles).size
96
+ assert_equal '2015-03-15', assigns(:articles)[0].published_on
97
+ assert_equal '2015-03-14', assigns(:articles)[1].published_on
98
+ assert_equal '2015-03-06', assigns(:articles)[2].published_on
99
+ end
100
+
101
+ test "should filter search results and the author and published date facets when user selects a category" do
102
+ get :index, q: 'one', c: 'One'
103
+ assert_response :success
104
+
105
+ assert_equal 2, assigns(:articles).size
106
+
107
+ aggregations = assigns(:articles).response.response['aggregations']
108
+
109
+ assert_equal 1, aggregations['authors']['authors']['buckets'].size
110
+ assert_equal 1, aggregations['published']['published']['buckets'].size
111
+
112
+ # Do NOT filter the category facet
113
+ assert_equal 2, aggregations['categories']['categories']['buckets'].size
114
+ end
115
+
116
+ test "should filter search results and the category and published date facets when user selects a category" do
117
+ get :index, q: 'one', a: 'Mary Smith'
118
+ assert_response :success
119
+
120
+ assert_equal 1, assigns(:articles).size
121
+
122
+ aggregations = assigns(:articles).response.response['aggregations']
123
+
124
+ assert_equal 1, aggregations['categories']['categories']['buckets'].size
125
+ assert_equal 1, aggregations['published']['published']['buckets'].size
126
+
127
+ # Do NOT filter the authors facet
128
+ assert_equal 2, aggregations['authors']['authors']['buckets'].size
129
+ end
130
+ end
@@ -0,0 +1,130 @@
1
+ require 'test_helper'
2
+
3
+ class SearchControllerTest < ActionController::TestCase
4
+ setup do
5
+ Time.stubs(:now).returns(Time.parse('2015-03-16 10:00:00 UTC'))
6
+
7
+ Article.delete_all
8
+
9
+ articles = [
10
+ { title: 'Article One', abstract: 'One', content: 'One', published_on: 1.day.ago, category_title: 'One', author_first_name: 'John', author_last_name: 'Smith' },
11
+ { title: 'Article One Another', abstract: '', content: '', published_on: 2.days.ago, category_title: 'One', author_first_name: 'John', author_last_name: 'Smith' },
12
+ { title: 'Article One Two', abstract: '', content: '', published_on: 10.days.ago, category_title: 'Two', author_first_name: 'Mary', author_last_name: 'Smith' },
13
+ { title: 'Article Two', abstract: '', content: '', published_on: 12.days.ago, category_title: 'Two', author_first_name: 'Mary', author_last_name: 'Smith' },
14
+ { title: 'Article Three', abstract: '', content: '', published_on: 12.days.ago, category_title: 'Three', author_first_name: 'Alice', author_last_name: 'Smith' }
15
+ ]
16
+
17
+ articles.each do |a|
18
+ article = Article.create! \
19
+ title: a[:title],
20
+ abstract: a[:abstract],
21
+ content: a[:content],
22
+ published_on: a[:published_on]
23
+
24
+ article.categories << Category.find_or_create_by!(title: a[:category_title])
25
+
26
+ article.authors << Author.find_or_create_by!(first_name: a[:author_first_name], last_name: a[:author_last_name])
27
+
28
+ article.save!
29
+ end
30
+
31
+ Article.find_by_title('Article Three').comments.create body: 'One'
32
+
33
+ Sidekiq::Queue.new("elasticsearch").clear
34
+
35
+ Article.__elasticsearch__.import force: true
36
+ Article.__elasticsearch__.refresh_index!
37
+ end
38
+
39
+ test "should return search results" do
40
+ get :index, q: 'one'
41
+ assert_response :success
42
+ assert_equal 3, assigns(:articles).size
43
+ end
44
+
45
+ test "should return search results in comments" do
46
+ get :index, q: 'one', comments: 'y'
47
+ assert_response :success
48
+ assert_equal 4, assigns(:articles).size
49
+ end
50
+
51
+ test "should return highlighted snippets" do
52
+ get :index, q: 'one'
53
+ assert_response :success
54
+ assert_match %r{<em class="label label-highlight">One</em>}, assigns(:articles).first.highlight.title.first
55
+ end
56
+
57
+ test "should return suggestions" do
58
+ get :index, q: 'one'
59
+ assert_response :success
60
+
61
+ suggestions = assigns(:articles).response.response['suggest']
62
+
63
+ assert_equal 'one', suggestions['suggest_title'][0]['text']
64
+ end
65
+
66
+ test "should return facets" do
67
+ get :index, q: 'one'
68
+ assert_response :success
69
+
70
+ facets = assigns(:articles).response.response['facets']
71
+
72
+ assert_equal 2, facets['categories']['terms'].size
73
+ assert_equal 2, facets['authors']['terms'].size
74
+ assert_equal 2, facets['published']['entries'].size
75
+
76
+ assert_equal 'One', facets['categories']['terms'][0]['term']
77
+ assert_equal 'John Smith', facets['authors']['terms'][0]['term']
78
+ assert_equal 1425254400000, facets['published']['entries'][0]['time']
79
+ end
80
+
81
+ test "should sort on the published date" do
82
+ get :index, q: 'one', s: 'published_on'
83
+ assert_response :success
84
+
85
+ assert_equal 3, assigns(:articles).size
86
+ assert_equal '2015-03-15', assigns(:articles)[0].published_on
87
+ assert_equal '2015-03-14', assigns(:articles)[1].published_on
88
+ assert_equal '2015-03-06', assigns(:articles)[2].published_on
89
+ end
90
+
91
+ test "should sort on the published date when no query is provided" do
92
+ get :index, q: ''
93
+ assert_response :success
94
+
95
+ assert_equal 5, assigns(:articles).size
96
+ assert_equal '2015-03-15', assigns(:articles)[0].published_on
97
+ assert_equal '2015-03-14', assigns(:articles)[1].published_on
98
+ assert_equal '2015-03-06', assigns(:articles)[2].published_on
99
+ end
100
+
101
+ test "should filter search results and the author and published date facets when user selects a category" do
102
+ get :index, q: 'one', c: 'One'
103
+ assert_response :success
104
+
105
+ assert_equal 2, assigns(:articles).size
106
+
107
+ facets = assigns(:articles).response.response['facets']
108
+
109
+ assert_equal 1, facets['authors']['terms'].size
110
+ assert_equal 1, facets['published']['entries'].size
111
+
112
+ # Do NOT filter the category facet
113
+ assert_equal 2, facets['categories']['terms'].size
114
+ end
115
+
116
+ test "should filter search results and the category and published date facets when user selects a category" do
117
+ get :index, q: 'one', a: 'Mary Smith'
118
+ assert_response :success
119
+
120
+ assert_equal 1, assigns(:articles).size
121
+
122
+ facets = assigns(:articles).response.response['facets']
123
+
124
+ assert_equal 1, facets['categories']['terms'].size
125
+ assert_equal 1, facets['published']['entries'].size
126
+
127
+ # Do NOT filter the authors facet
128
+ assert_equal 2, facets['authors']['terms'].size
129
+ end
130
+ end
@@ -0,0 +1,217 @@
1
+ module Searchable
2
+ extend ActiveSupport::Concern
3
+
4
+ included do
5
+ include Elasticsearch::Model
6
+
7
+ # Customize the index name
8
+ #
9
+ index_name [Rails.application.engine_name, Rails.env].join('_')
10
+
11
+ # Set up index configuration and mapping
12
+ #
13
+ settings index: { number_of_shards: 1, number_of_replicas: 0 } do
14
+ mapping do
15
+ indexes :title, type: 'multi_field' do
16
+ indexes :title, analyzer: 'snowball'
17
+ indexes :tokenized, analyzer: 'simple'
18
+ end
19
+
20
+ indexes :content, type: 'multi_field' do
21
+ indexes :content, analyzer: 'snowball'
22
+ indexes :tokenized, analyzer: 'simple'
23
+ end
24
+
25
+ indexes :published_on, type: 'date'
26
+
27
+ indexes :authors do
28
+ indexes :full_name, type: 'multi_field' do
29
+ indexes :full_name
30
+ indexes :raw, analyzer: 'keyword'
31
+ end
32
+ end
33
+
34
+ indexes :categories, analyzer: 'keyword'
35
+
36
+ indexes :comments, type: 'nested' do
37
+ indexes :body, analyzer: 'snowball'
38
+ indexes :stars
39
+ indexes :pick
40
+ indexes :user, analyzer: 'keyword'
41
+ indexes :user_location, type: 'multi_field' do
42
+ indexes :user_location
43
+ indexes :raw, analyzer: 'keyword'
44
+ end
45
+ end
46
+ end
47
+ end
48
+
49
+ # Set up callbacks for updating the index on model changes
50
+ #
51
+ after_commit lambda { Indexer.perform_async(:index, self.class.to_s, self.id) }, on: :create
52
+ after_commit lambda { Indexer.perform_async(:update, self.class.to_s, self.id) }, on: :update
53
+ after_commit lambda { Indexer.perform_async(:delete, self.class.to_s, self.id) }, on: :destroy
54
+ after_touch lambda { Indexer.perform_async(:update, self.class.to_s, self.id) }
55
+
56
+ # Customize the JSON serialization for Elasticsearch
57
+ #
58
+ def as_indexed_json(options={})
59
+ hash = self.as_json(
60
+ include: { authors: { methods: [:full_name], only: [:full_name] },
61
+ comments: { only: [:body, :stars, :pick, :user, :user_location] }
62
+ })
63
+ hash['categories'] = self.categories.map(&:title)
64
+ hash
65
+ end
66
+
67
+ # Return documents matching the user's query, include highlights and aggregations in response,
68
+ # and implement a "cross" faceted navigation
69
+ #
70
+ # @param q [String] The user query
71
+ # @return [Elasticsearch::Model::Response::Response]
72
+ #
73
+ def self.search(q, options={})
74
+ @search_definition = Elasticsearch::DSL::Search.search do
75
+ query do
76
+
77
+ # If a user query is present...
78
+ #
79
+ unless q.blank?
80
+ bool do
81
+
82
+ # ... search in `title`, `abstract` and `content`, boosting `title`
83
+ #
84
+ should do
85
+ multi_match do
86
+ query q
87
+ fields ['title^10', 'abstract^2', 'content']
88
+ operator 'and'
89
+ end
90
+ end
91
+
92
+ # ... search in comment body if user checked the comments checkbox
93
+ #
94
+ if q.present? && options[:comments]
95
+ should do
96
+ nested do
97
+ path :comments
98
+ query do
99
+ multi_match do
100
+ query q
101
+ fields 'body'
102
+ operator 'and'
103
+ end
104
+ end
105
+ end
106
+ end
107
+ end
108
+ end
109
+
110
+ # ... otherwise, just return all articles
111
+ else
112
+ match_all
113
+ end
114
+ end
115
+
116
+ # Filter the search results based on user selection
117
+ #
118
+ post_filter do
119
+ bool do
120
+ must { term categories: options[:category] } if options[:category]
121
+ must { match_all } if options.keys.none? { |k| [:c, :a, :w].include? k }
122
+ must { term 'authors.full_name.raw' => options[:author] } if options[:author]
123
+ must { range published_on: { gte: options[:published_week], lte: "#{options[:published_week]}||+1w" } } if options[:published_week]
124
+ end
125
+ end
126
+
127
+ # Return top categories for faceted navigation
128
+ #
129
+ aggregation :categories do
130
+ # Filter the aggregation with any selected `author` and `published_week`
131
+ #
132
+ f = Elasticsearch::DSL::Search::Filters::Bool.new
133
+ f.must { match_all }
134
+ f.must { term 'authors.full_name.raw' => options[:author] } if options[:author]
135
+ f.must { range published_on: { gte: options[:published_week], lte: "#{options[:published_week]}||+1w" } } if options[:published_week]
136
+
137
+ filter f.to_hash do
138
+ aggregation :categories do
139
+ terms field: 'categories'
140
+ end
141
+ end
142
+ end
143
+
144
+ # Return top authors for faceted navigation
145
+ #
146
+ aggregation :authors do
147
+ # Filter the aggregation with any selected `category` and `published_week`
148
+ #
149
+ f = Elasticsearch::DSL::Search::Filters::Bool.new
150
+ f.must { match_all }
151
+ f.must { term categories: options[:category] } if options[:category]
152
+ f.must { range published_on: { gte: options[:published_week], lte: "#{options[:published_week]}||+1w" } } if options[:published_week]
153
+
154
+ filter f do
155
+ aggregation :authors do
156
+ terms field: 'authors.full_name.raw'
157
+ end
158
+ end
159
+ end
160
+
161
+ # Return the published date ranges for faceted navigation
162
+ #
163
+ aggregation :published do
164
+ # Filter the aggregation with any selected `author` and `category`
165
+ #
166
+ f = Elasticsearch::DSL::Search::Filters::Bool.new
167
+ f.must { match_all }
168
+ f.must { term 'authors.full_name.raw' => options[:author] } if options[:author]
169
+ f.must { term categories: options[:category] } if options[:category]
170
+
171
+ filter f do
172
+ aggregation :published do
173
+ date_histogram do
174
+ field 'published_on'
175
+ interval 'week'
176
+ end
177
+ end
178
+ end
179
+ end
180
+
181
+ # Highlight the snippets in results
182
+ #
183
+ highlight do
184
+ fields title: { number_of_fragments: 0 },
185
+ abstract: { number_of_fragments: 0 },
186
+ content: { fragment_size: 50 }
187
+
188
+ field 'comments.body', fragment_size: 50 if q.present? && options[:comments]
189
+
190
+ pre_tags '<em class="label label-highlight">'
191
+ post_tags '</em>'
192
+ end
193
+
194
+ case
195
+ # By default, sort by relevance, but when a specific sort option is present, use it ...
196
+ #
197
+ when options[:sort]
198
+ sort options[:sort].to_sym => 'desc'
199
+ track_scores true
200
+ #
201
+ # ... when there's no user query, sort on published date
202
+ #
203
+ when q.blank?
204
+ sort published_on: 'desc'
205
+ end
206
+
207
+ # Return suggestions unless there's no query from the user
208
+ unless q.blank?
209
+ suggest :suggest_title, text: q, term: { field: 'title.tokenized', suggest_mode: 'always' }
210
+ suggest :suggest_body, text: q, term: { field: 'content.tokenized', suggest_mode: 'always' }
211
+ end
212
+ end
213
+
214
+ __elasticsearch__.search(@search_definition)
215
+ end
216
+ end
217
+ end
@@ -1,6 +1,7 @@
1
1
  require 'test_helper'
2
2
 
3
3
  require 'rails/railtie'
4
+ require 'action_pack'
4
5
  require 'lograge'
5
6
 
6
7
  require 'elasticsearch/rails/lograge'
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: elasticsearch-rails
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.6
4
+ version: 0.1.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - Karel Minarik
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-10-01 00:00:00.000000000 Z
11
+ date: 2015-04-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -84,16 +84,16 @@ dependencies:
84
84
  name: rails
85
85
  requirement: !ruby/object:Gem::Requirement
86
86
  requirements:
87
- - - ">"
87
+ - - ">="
88
88
  - !ruby/object:Gem::Version
89
- version: '3.0'
89
+ version: '3.1'
90
90
  type: :development
91
91
  prerelease: false
92
92
  version_requirements: !ruby/object:Gem::Requirement
93
93
  requirements:
94
- - - ">"
94
+ - - ">="
95
95
  - !ruby/object:Gem::Version
96
- version: '3.0'
96
+ version: '3.1'
97
97
  - !ruby/object:Gem::Dependency
98
98
  name: lograge
99
99
  requirement: !ruby/object:Gem::Requirement
@@ -290,10 +290,15 @@ files:
290
290
  - lib/rails/templates/01-basic.rb
291
291
  - lib/rails/templates/02-pretty.rb
292
292
  - lib/rails/templates/03-expert.rb
293
+ - lib/rails/templates/04-dsl.rb
293
294
  - lib/rails/templates/articles.yml.gz
295
+ - lib/rails/templates/index.html.dsl.erb
294
296
  - lib/rails/templates/index.html.erb
295
297
  - lib/rails/templates/indexer.rb
296
298
  - lib/rails/templates/search.css
299
+ - lib/rails/templates/search_controller_test.dsl.rb
300
+ - lib/rails/templates/search_controller_test.rb
301
+ - lib/rails/templates/searchable.dsl.rb
297
302
  - lib/rails/templates/searchable.rb
298
303
  - lib/rails/templates/seeds.rb
299
304
  - test/test_helper.rb