elasticsearch-rails 0.1.6 → 0.1.7

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 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