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 +4 -4
- data/CHANGELOG.md +10 -0
- data/elasticsearch-rails.gemspec +1 -1
- data/lib/elasticsearch/rails/version.rb +1 -1
- data/lib/rails/templates/01-basic.rb +3 -4
- data/lib/rails/templates/02-pretty.rb +11 -3
- data/lib/rails/templates/03-expert.rb +8 -7
- data/lib/rails/templates/04-dsl.rb +128 -0
- data/lib/rails/templates/index.html.dsl.erb +160 -0
- data/lib/rails/templates/index.html.erb +9 -9
- data/lib/rails/templates/search_controller_test.dsl.rb +130 -0
- data/lib/rails/templates/search_controller_test.rb +130 -0
- data/lib/rails/templates/searchable.dsl.rb +217 -0
- data/test/unit/instrumentation/lograge_test.rb +1 -0
- metadata +11 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6acf0a64b1de80131443fe7818364e938af6e466
|
4
|
+
data.tar.gz: f234b64f5d8eda579694406c216bffb37d08a991
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: efc3dc4312de0805bd9742029a4b1e09b1b03fffbb3460e4bfb25b7e897d21d7d3f3a5c5bb28f4606ec24438584d45d096a1d51c1f9cf2403508cfd42a3190f7
|
7
|
+
data.tar.gz: e50eee5092e29fa9c24fcf96ad64ccc791008ce1b2eb9d2df1e5ddad860150b468d66451ea77c79f220159aa3432d9e7c34d338e6c2a64394d5f549817dba93c
|
data/CHANGELOG.md
CHANGED
@@ -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
|
data/elasticsearch-rails.gemspec
CHANGED
@@ -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", "
|
33
|
+
s.add_development_dependency "rails", ">= 3.1"
|
34
34
|
|
35
35
|
s.add_development_dependency "lograge"
|
36
36
|
|
@@ -115,8 +115,7 @@ end
|
|
115
115
|
|
116
116
|
# ----- Auxiliary gems ----------------------------------------------------------------------------
|
117
117
|
|
118
|
-
gem '
|
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'
|
33
|
-
after:
|
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:
|
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 →'.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 →'.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 →'.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)} — #{__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('…').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('…').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 →'.html_safe, search_path(params.merge(c: nil))%></p>
|
68
|
+
<p class="panel-heading"><%= link_to 'All Sections →'.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 →'.html_safe, search_path(params.merge(a: nil))%></p>
|
83
|
+
<p class="panel-heading"><%= link_to 'All Authors →'.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 →'.html_safe, search_path(params.merge(w: nil))%></p>
|
98
|
+
<p class="panel-heading"><%= link_to 'Any Date →'.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)} — #{__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
|
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.
|
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:
|
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.
|
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.
|
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
|