mini-smart-box 0.0.1

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.
Files changed (38) hide show
  1. checksums.yaml +7 -0
  2. data/elasticsearch-rails-8.0.1/CHANGELOG.md +44 -0
  3. data/elasticsearch-rails-8.0.1/Gemfile +38 -0
  4. data/elasticsearch-rails-8.0.1/LICENSE.txt +202 -0
  5. data/elasticsearch-rails-8.0.1/README.md +149 -0
  6. data/elasticsearch-rails-8.0.1/Rakefile +67 -0
  7. data/elasticsearch-rails-8.0.1/elasticsearch-rails.gemspec +67 -0
  8. data/elasticsearch-rails-8.0.1/lib/elasticsearch/rails/instrumentation/controller_runtime.rb +58 -0
  9. data/elasticsearch-rails-8.0.1/lib/elasticsearch/rails/instrumentation/log_subscriber.rb +67 -0
  10. data/elasticsearch-rails-8.0.1/lib/elasticsearch/rails/instrumentation/publishers.rb +53 -0
  11. data/elasticsearch-rails-8.0.1/lib/elasticsearch/rails/instrumentation/railtie.rb +44 -0
  12. data/elasticsearch-rails-8.0.1/lib/elasticsearch/rails/instrumentation.rb +53 -0
  13. data/elasticsearch-rails-8.0.1/lib/elasticsearch/rails/lograge.rb +57 -0
  14. data/elasticsearch-rails-8.0.1/lib/elasticsearch/rails/tasks/import.rb +128 -0
  15. data/elasticsearch-rails-8.0.1/lib/elasticsearch/rails/version.rb +22 -0
  16. data/elasticsearch-rails-8.0.1/lib/elasticsearch/rails.rb +24 -0
  17. data/elasticsearch-rails-8.0.1/lib/rails/templates/01-basic.rb +364 -0
  18. data/elasticsearch-rails-8.0.1/lib/rails/templates/02-pretty.rb +348 -0
  19. data/elasticsearch-rails-8.0.1/lib/rails/templates/03-expert.rb +358 -0
  20. data/elasticsearch-rails-8.0.1/lib/rails/templates/04-dsl.rb +146 -0
  21. data/elasticsearch-rails-8.0.1/lib/rails/templates/05-settings-files.rb +88 -0
  22. data/elasticsearch-rails-8.0.1/lib/rails/templates/articles.yml.gz +0 -0
  23. data/elasticsearch-rails-8.0.1/lib/rails/templates/articles_settings.json +1 -0
  24. data/elasticsearch-rails-8.0.1/lib/rails/templates/index.html.dsl.erb +178 -0
  25. data/elasticsearch-rails-8.0.1/lib/rails/templates/index.html.erb +178 -0
  26. data/elasticsearch-rails-8.0.1/lib/rails/templates/indexer.rb +44 -0
  27. data/elasticsearch-rails-8.0.1/lib/rails/templates/search.css +76 -0
  28. data/elasticsearch-rails-8.0.1/lib/rails/templates/search_controller_test.dsl.rb +148 -0
  29. data/elasticsearch-rails-8.0.1/lib/rails/templates/search_controller_test.rb +148 -0
  30. data/elasticsearch-rails-8.0.1/lib/rails/templates/searchable.dsl.rb +234 -0
  31. data/elasticsearch-rails-8.0.1/lib/rails/templates/searchable.rb +224 -0
  32. data/elasticsearch-rails-8.0.1/lib/rails/templates/seeds.rb +75 -0
  33. data/elasticsearch-rails-8.0.1/spec/instrumentation/log_subscriber_spec.rb +57 -0
  34. data/elasticsearch-rails-8.0.1/spec/instrumentation_spec.rb +103 -0
  35. data/elasticsearch-rails-8.0.1/spec/lograge_spec.rb +52 -0
  36. data/elasticsearch-rails-8.0.1/spec/spec_helper.rb +65 -0
  37. data/mini-smart-box.gemspec +11 -0
  38. metadata +76 -0
@@ -0,0 +1,148 @@
1
+ # Licensed to Elasticsearch B.V. under one or more contributor
2
+ # license agreements. See the NOTICE file distributed with
3
+ # this work for additional information regarding copyright
4
+ # ownership. Elasticsearch B.V. licenses this file to you under
5
+ # the Apache License, Version 2.0 (the "License"); you may
6
+ # not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing,
12
+ # software distributed under the License is distributed on an
13
+ # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14
+ # KIND, either express or implied. See the License for the
15
+ # specific language governing permissions and limitations
16
+ # under the License.
17
+
18
+ require 'test_helper'
19
+
20
+ class SearchControllerTest < ActionController::TestCase
21
+ setup do
22
+ travel_to Time.new(2015, 03, 16, 10, 00, 00, 0)
23
+
24
+ Article.delete_all
25
+
26
+ articles = [
27
+ { title: 'Article One', abstract: 'One', content: 'One', published_on: 1.day.ago, category_title: 'One', author_first_name: 'John', author_last_name: 'Smith' },
28
+ { title: 'Article One Another', abstract: '', content: '', published_on: 2.days.ago, category_title: 'One', author_first_name: 'John', author_last_name: 'Smith' },
29
+ { title: 'Article One Two', abstract: '', content: '', published_on: 10.days.ago, category_title: 'Two', author_first_name: 'Mary', author_last_name: 'Smith' },
30
+ { title: 'Article Two', abstract: '', content: '', published_on: 12.days.ago, category_title: 'Two', author_first_name: 'Mary', author_last_name: 'Smith' },
31
+ { title: 'Article Three', abstract: '', content: '', published_on: 12.days.ago, category_title: 'Three', author_first_name: 'Alice', author_last_name: 'Smith' }
32
+ ]
33
+
34
+ articles.each do |a|
35
+ article = Article.create! \
36
+ title: a[:title],
37
+ abstract: a[:abstract],
38
+ content: a[:content],
39
+ published_on: a[:published_on]
40
+
41
+ article.categories << Category.find_or_create_by!(title: a[:category_title])
42
+
43
+ article.authors << Author.find_or_create_by!(first_name: a[:author_first_name], last_name: a[:author_last_name])
44
+
45
+ article.save!
46
+ end
47
+
48
+ Article.find_by_title('Article Three').comments.create body: 'One'
49
+
50
+ Sidekiq::Worker.clear_all
51
+
52
+ Article.__elasticsearch__.import force: true
53
+ Article.__elasticsearch__.refresh_index!
54
+ end
55
+
56
+ test "should return search results" do
57
+ get :index, params: { q: 'one' }
58
+ assert_response :success
59
+ assert_equal 3, assigns(:articles).size
60
+ end
61
+
62
+ test "should return search results in comments" do
63
+ get :index, params: { q: 'one', comments: 'y' }
64
+ assert_response :success
65
+
66
+ assert_equal 4, assigns(:articles).size
67
+ end
68
+
69
+ test "should return highlighted snippets" do
70
+ get :index, params: { q: 'one' }
71
+ assert_response :success
72
+ assert_match %r{<em class="label label-highlight">One</em>}, assigns(:articles).first.highlight.title.first
73
+ end
74
+
75
+ test "should return suggestions" do
76
+ get :index, params: { q: 'one' }
77
+ assert_response :success
78
+
79
+ suggestions = assigns(:articles).response.suggestions
80
+
81
+ assert_equal 'one', suggestions['suggest_title'][0]['text']
82
+ end
83
+
84
+ test "should return facets" do
85
+ get :index, params: { q: 'one' }
86
+ assert_response :success
87
+
88
+ aggregations = assigns(:articles).response.response['aggregations']
89
+
90
+ assert_equal 2, aggregations['categories']['categories']['buckets'].size
91
+ assert_equal 2, aggregations['authors']['authors']['buckets'].size
92
+ assert_equal 2, aggregations['published']['published']['buckets'].size
93
+
94
+ assert_equal 'One', aggregations['categories']['categories']['buckets'][0]['key']
95
+ assert_equal 'John Smith', aggregations['authors']['authors']['buckets'][0]['key']
96
+ assert_equal 1425254400000, aggregations['published']['published']['buckets'][0]['key']
97
+ end
98
+
99
+ test "should sort on the published date" do
100
+ get :index, params: { q: 'one', s: 'published_on' }
101
+ assert_response :success
102
+
103
+ assert_equal 3, assigns(:articles).size
104
+ assert_equal '2015-03-15', assigns(:articles)[0].published_on
105
+ assert_equal '2015-03-14', assigns(:articles)[1].published_on
106
+ assert_equal '2015-03-06', assigns(:articles)[2].published_on
107
+ end
108
+
109
+ test "should sort on the published date when no query is provided" do
110
+ get :index, params: { q: '' }
111
+ assert_response :success
112
+
113
+ assert_equal 5, assigns(:articles).size
114
+ assert_equal '2015-03-15', assigns(:articles)[0].published_on
115
+ assert_equal '2015-03-14', assigns(:articles)[1].published_on
116
+ assert_equal '2015-03-06', assigns(:articles)[2].published_on
117
+ end
118
+
119
+ test "should filter search results and the author and published date facets when user selects a category" do
120
+ get :index, params: { q: 'one', c: 'One' }
121
+ assert_response :success
122
+
123
+ assert_equal 2, assigns(:articles).size
124
+
125
+ aggregations = assigns(:articles).response.response['aggregations']
126
+
127
+ assert_equal 1, aggregations['authors']['authors']['buckets'].size
128
+ assert_equal 1, aggregations['published']['published']['buckets'].size
129
+
130
+ # Do NOT filter the category facet
131
+ assert_equal 2, aggregations['categories']['categories']['buckets'].size
132
+ end
133
+
134
+ test "should filter search results and the category and published date facets when user selects a category" do
135
+ get :index, params: { q: 'one', a: 'Mary Smith' }
136
+ assert_response :success
137
+
138
+ assert_equal 1, assigns(:articles).size
139
+
140
+ aggregations = assigns(:articles).response.response['aggregations']
141
+
142
+ assert_equal 1, aggregations['categories']['categories']['buckets'].size
143
+ assert_equal 1, aggregations['published']['published']['buckets'].size
144
+
145
+ # Do NOT filter the authors facet
146
+ assert_equal 2, aggregations['authors']['authors']['buckets'].size
147
+ end
148
+ end
@@ -0,0 +1,234 @@
1
+ # Licensed to Elasticsearch B.V. under one or more contributor
2
+ # license agreements. See the NOTICE file distributed with
3
+ # this work for additional information regarding copyright
4
+ # ownership. Elasticsearch B.V. licenses this file to you under
5
+ # the Apache License, Version 2.0 (the "License"); you may
6
+ # not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing,
12
+ # software distributed under the License is distributed on an
13
+ # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14
+ # KIND, either express or implied. See the License for the
15
+ # specific language governing permissions and limitations
16
+ # under the License.
17
+
18
+ module Searchable
19
+ extend ActiveSupport::Concern
20
+
21
+ included do
22
+ include Elasticsearch::Model
23
+
24
+ # Customize the index name
25
+ #
26
+ index_name [Rails.application.engine_name, Rails.env].join('_')
27
+
28
+ # Set up index configuration and mapping
29
+ #
30
+ settings index: { number_of_shards: 1, number_of_replicas: 0 } do
31
+ mapping do
32
+ indexes :title, type: 'text' do
33
+ indexes :title, analyzer: 'snowball'
34
+ indexes :tokenized, analyzer: 'simple'
35
+ end
36
+
37
+ indexes :content, type: 'text' do
38
+ indexes :content, analyzer: 'snowball'
39
+ indexes :tokenized, analyzer: 'simple'
40
+ end
41
+
42
+ indexes :published_on, type: 'date'
43
+
44
+ indexes :authors do
45
+ indexes :full_name, type: 'text' do
46
+ indexes :full_name
47
+ indexes :raw, type: 'keyword'
48
+ end
49
+ end
50
+
51
+ indexes :categories, type: 'keyword'
52
+
53
+ indexes :comments, type: 'nested' do
54
+ indexes :body, analyzer: 'snowball'
55
+ indexes :stars
56
+ indexes :pick
57
+ indexes :user, type: 'keyword'
58
+ indexes :user_location, type: 'text' do
59
+ indexes :user_location
60
+ indexes :raw, type: 'keyword'
61
+ end
62
+ end
63
+ end
64
+ end
65
+
66
+ # Set up callbacks for updating the index on model changes
67
+ #
68
+ after_commit lambda { Indexer.perform_async(:index, self.class.to_s, self.id) }, on: :create
69
+ after_commit lambda { Indexer.perform_async(:update, self.class.to_s, self.id) }, on: :update
70
+ after_commit lambda { Indexer.perform_async(:delete, self.class.to_s, self.id) }, on: :destroy
71
+ after_touch lambda { Indexer.perform_async(:update, self.class.to_s, self.id) }
72
+
73
+ # Customize the JSON serialization for Elasticsearch
74
+ #
75
+ def as_indexed_json(options={})
76
+ hash = self.as_json(
77
+ include: { authors: { methods: [:full_name], only: [:full_name] },
78
+ comments: { only: [:body, :stars, :pick, :user, :user_location] }
79
+ })
80
+ hash['categories'] = self.categories.map(&:title)
81
+ hash
82
+ end
83
+
84
+ # Return documents matching the user's query, include highlights and aggregations in response,
85
+ # and implement a "cross" faceted navigation
86
+ #
87
+ # @param q [String] The user query
88
+ # @return [Elasticsearch::Model::Response::Response]
89
+ #
90
+ def self.search(q, options={})
91
+ @search_definition = Elasticsearch::DSL::Search.search do
92
+ query do
93
+
94
+ # If a user query is present...
95
+ #
96
+ unless q.blank?
97
+ bool do
98
+
99
+ # ... search in `title`, `abstract` and `content`, boosting `title`
100
+ #
101
+ should do
102
+ multi_match do
103
+ query q
104
+ fields ['title^10', 'abstract^2', 'content']
105
+ operator 'and'
106
+ end
107
+ end
108
+
109
+ # ... search in comment body if user checked the comments checkbox
110
+ #
111
+ if q.present? && options[:comments]
112
+ should do
113
+ nested do
114
+ path :comments
115
+ query do
116
+ multi_match do
117
+ query q
118
+ fields 'comments.body'
119
+ operator 'and'
120
+ end
121
+ end
122
+ end
123
+ end
124
+ end
125
+ end
126
+
127
+ # ... otherwise, just return all articles
128
+ else
129
+ match_all
130
+ end
131
+ end
132
+
133
+ # Filter the search results based on user selection
134
+ #
135
+ post_filter do
136
+ bool do
137
+ must { term categories: options[:category] } if options[:category]
138
+ must { match_all } if options.keys.none? { |k| [:c, :a, :w].include? k }
139
+ must { term 'authors.full_name.raw' => options[:author] } if options[:author]
140
+ must { range published_on: { gte: options[:published_week], lte: "#{options[:published_week]}||+1w" } } if options[:published_week]
141
+ end
142
+ end
143
+
144
+ # Return top categories for faceted navigation
145
+ #
146
+ aggregation :categories do
147
+ # Filter the aggregation with any selected `author` and `published_week`
148
+ #
149
+ f = Elasticsearch::DSL::Search::Filters::Bool.new
150
+ f.must { match_all }
151
+ f.must { term 'authors.full_name.raw' => options[:author] } if options[:author]
152
+ f.must { range published_on: { gte: options[:published_week], lte: "#{options[:published_week]}||+1w" } } if options[:published_week]
153
+
154
+ filter f.to_hash do
155
+ aggregation :categories do
156
+ terms field: 'categories'
157
+ end
158
+ end
159
+ end
160
+
161
+ # Return top authors for faceted navigation
162
+ #
163
+ aggregation :authors do
164
+ # Filter the aggregation with any selected `category` and `published_week`
165
+ #
166
+ f = Elasticsearch::DSL::Search::Filters::Bool.new
167
+ f.must { match_all }
168
+ f.must { term categories: options[:category] } if options[:category]
169
+ f.must { range published_on: { gte: options[:published_week], lte: "#{options[:published_week]}||+1w" } } if options[:published_week]
170
+
171
+ filter f do
172
+ aggregation :authors do
173
+ terms field: 'authors.full_name.raw'
174
+ end
175
+ end
176
+ end
177
+
178
+ # Return the published date ranges for faceted navigation
179
+ #
180
+ aggregation :published do
181
+ # Filter the aggregation with any selected `author` and `category`
182
+ #
183
+ f = Elasticsearch::DSL::Search::Filters::Bool.new
184
+ f.must { match_all }
185
+ f.must { term 'authors.full_name.raw' => options[:author] } if options[:author]
186
+ f.must { term categories: options[:category] } if options[:category]
187
+
188
+ filter f do
189
+ aggregation :published do
190
+ date_histogram do
191
+ field 'published_on'
192
+ interval 'week'
193
+ end
194
+ end
195
+ end
196
+ end
197
+
198
+ # Highlight the snippets in results
199
+ #
200
+ highlight do
201
+ fields title: { number_of_fragments: 0 },
202
+ abstract: { number_of_fragments: 0 },
203
+ content: { fragment_size: 50 }
204
+
205
+ field 'comments.body', fragment_size: 50 if q.present? && options[:comments]
206
+
207
+ pre_tags '<em class="label label-highlight">'
208
+ post_tags '</em>'
209
+ end
210
+
211
+ case
212
+ # By default, sort by relevance, but when a specific sort option is present, use it ...
213
+ #
214
+ when options[:sort]
215
+ sort options[:sort].to_sym => 'desc'
216
+ track_scores true
217
+ #
218
+ # ... when there's no user query, sort on published date
219
+ #
220
+ when q.blank?
221
+ sort published_on: 'desc'
222
+ end
223
+
224
+ # Return suggestions unless there's no query from the user
225
+ unless q.blank?
226
+ suggest :suggest_title, text: q, term: { field: 'title.tokenized', suggest_mode: 'always' }
227
+ suggest :suggest_body, text: q, term: { field: 'content.tokenized', suggest_mode: 'always' }
228
+ end
229
+ end
230
+
231
+ __elasticsearch__.search(@search_definition)
232
+ end
233
+ end
234
+ end
@@ -0,0 +1,224 @@
1
+ # Licensed to Elasticsearch B.V. under one or more contributor
2
+ # license agreements. See the NOTICE file distributed with
3
+ # this work for additional information regarding copyright
4
+ # ownership. Elasticsearch B.V. licenses this file to you under
5
+ # the Apache License, Version 2.0 (the "License"); you may
6
+ # not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing,
12
+ # software distributed under the License is distributed on an
13
+ # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14
+ # KIND, either express or implied. See the License for the
15
+ # specific language governing permissions and limitations
16
+ # under the License.
17
+
18
+ module Searchable
19
+ extend ActiveSupport::Concern
20
+
21
+ included do
22
+ include Elasticsearch::Model
23
+
24
+ # Customize the index name
25
+ #
26
+ index_name [Rails.application.engine_name, Rails.env].join('_')
27
+
28
+ # Set up index configuration and mapping
29
+ #
30
+ settings index: { number_of_shards: 1, number_of_replicas: 0 } do
31
+ mapping do
32
+ indexes :title, type: 'text' do
33
+ indexes :title, analyzer: 'snowball'
34
+ indexes :tokenized, analyzer: 'simple'
35
+ end
36
+
37
+ indexes :content, type: 'text' do
38
+ indexes :content, analyzer: 'snowball'
39
+ indexes :tokenized, analyzer: 'simple'
40
+ end
41
+
42
+ indexes :published_on, type: 'date'
43
+
44
+ indexes :authors do
45
+ indexes :full_name, type: 'text' do
46
+ indexes :full_name
47
+ indexes :raw, type: 'keyword'
48
+ end
49
+ end
50
+
51
+ indexes :categories, type: 'keyword'
52
+
53
+ indexes :comments, type: 'nested' do
54
+ indexes :body, analyzer: 'snowball'
55
+ indexes :stars
56
+ indexes :pick
57
+ indexes :user, type: 'keyword'
58
+ indexes :user_location, type: 'text' do
59
+ indexes :user_location
60
+ indexes :raw, type: 'keyword'
61
+ end
62
+ end
63
+ end
64
+ end
65
+
66
+ # Set up callbacks for updating the index on model changes
67
+ #
68
+ after_commit lambda { Indexer.perform_async(:index, self.class.to_s, self.id) }, on: :create
69
+ after_commit lambda { Indexer.perform_async(:update, self.class.to_s, self.id) }, on: :update
70
+ after_commit lambda { Indexer.perform_async(:delete, self.class.to_s, self.id) }, on: :destroy
71
+ after_touch lambda { Indexer.perform_async(:update, self.class.to_s, self.id) }
72
+
73
+ # Customize the JSON serialization for Elasticsearch
74
+ #
75
+ def as_indexed_json(options={})
76
+ hash = self.as_json(
77
+ include: { authors: { methods: [:full_name], only: [:full_name] },
78
+ comments: { only: [:body, :stars, :pick, :user, :user_location] }
79
+ })
80
+ hash['categories'] = self.categories.map(&:title)
81
+ hash
82
+ end
83
+
84
+ # Search in title and content fields for `query`, include highlights in response
85
+ #
86
+ # @param query [String] The user query
87
+ # @return [Elasticsearch::Model::Response::Response]
88
+ #
89
+ def self.search(query, options={})
90
+
91
+ # Prefill and set the filters (top-level `post_filter` and aggregation `filter` elements)
92
+ #
93
+ __set_filters = lambda do |key, f|
94
+ @search_definition[:post_filter][:bool] ||= {}
95
+ @search_definition[:post_filter][:bool][:must] ||= []
96
+ @search_definition[:post_filter][:bool][:must] |= [f]
97
+
98
+ @search_definition[:aggregations][key.to_sym][:filter][:bool][:must] ||= []
99
+ @search_definition[:aggregations][key.to_sym][:filter][:bool][:must] |= [f]
100
+ end
101
+
102
+ @search_definition = {
103
+ query: {},
104
+
105
+ highlight: {
106
+ pre_tags: ['<em class="label label-highlight">'],
107
+ post_tags: ['</em>'],
108
+ fields: {
109
+ title: { number_of_fragments: 0 },
110
+ abstract: { number_of_fragments: 0 },
111
+ content: { fragment_size: 50 }
112
+ }
113
+ },
114
+
115
+ post_filter: { bool: { must: [ match_all: {} ] } },
116
+
117
+ aggregations: {
118
+ categories: {
119
+ filter: { bool: { must: [ match_all: {} ] } },
120
+ aggregations: { categories: { terms: { field: 'categories' } } }
121
+ },
122
+ authors: {
123
+ filter: { bool: { must: [ match_all: {} ] } },
124
+ aggregations: { authors: { terms: { field: 'authors.full_name.raw' } } }
125
+ },
126
+ published: {
127
+ filter: { bool: { must: [ match_all: {} ] } },
128
+ aggregations: {
129
+ published: { date_histogram: { field: 'published_on', interval: 'week' } }
130
+ }
131
+ }
132
+ }
133
+ }
134
+
135
+ unless query.blank?
136
+ @search_definition[:query] = {
137
+ bool: {
138
+ should: [
139
+ { multi_match: {
140
+ query: query,
141
+ fields: ['title^10', 'abstract^2', 'content'],
142
+ operator: 'and'
143
+ }
144
+ }
145
+ ]
146
+ }
147
+ }
148
+ else
149
+ @search_definition[:query] = { match_all: {} }
150
+ @search_definition[:sort] = { published_on: 'desc' }
151
+ end
152
+
153
+ if options[:category]
154
+ f = { term: { categories: options[:category] } }
155
+
156
+ __set_filters.(:authors, f)
157
+ __set_filters.(:published, f)
158
+ end
159
+
160
+ if options[:author]
161
+ f = { term: { 'authors.full_name.raw' => options[:author] } }
162
+
163
+ __set_filters.(:categories, f)
164
+ __set_filters.(:published, f)
165
+ end
166
+
167
+ if options[:published_week]
168
+ f = {
169
+ range: {
170
+ published_on: {
171
+ gte: options[:published_week],
172
+ lte: "#{options[:published_week]}||+1w"
173
+ }
174
+ }
175
+ }
176
+
177
+ __set_filters.(:categories, f)
178
+ __set_filters.(:authors, f)
179
+ end
180
+
181
+ if query.present? && options[:comments]
182
+ @search_definition[:query][:bool][:should] ||= []
183
+ @search_definition[:query][:bool][:should] << {
184
+ nested: {
185
+ path: 'comments',
186
+ query: {
187
+ multi_match: {
188
+ query: query,
189
+ fields: ['comments.body'],
190
+ operator: 'and'
191
+ }
192
+ }
193
+ }
194
+ }
195
+ @search_definition[:highlight][:fields].update 'comments.body' => { fragment_size: 50 }
196
+ end
197
+
198
+ if options[:sort]
199
+ @search_definition[:sort] = { options[:sort] => 'desc' }
200
+ @search_definition[:track_scores] = true
201
+ end
202
+
203
+ unless query.blank?
204
+ @search_definition[:suggest] = {
205
+ text: query,
206
+ suggest_title: {
207
+ term: {
208
+ field: 'title.tokenized',
209
+ suggest_mode: 'always'
210
+ }
211
+ },
212
+ suggest_body: {
213
+ term: {
214
+ field: 'content.tokenized',
215
+ suggest_mode: 'always'
216
+ }
217
+ }
218
+ }
219
+ end
220
+
221
+ __elasticsearch__.search(@search_definition)
222
+ end
223
+ end
224
+ end
@@ -0,0 +1,75 @@
1
+ # Licensed to Elasticsearch B.V. under one or more contributor
2
+ # license agreements. See the NOTICE file distributed with
3
+ # this work for additional information regarding copyright
4
+ # ownership. Elasticsearch B.V. licenses this file to you under
5
+ # the Apache License, Version 2.0 (the "License"); you may
6
+ # not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing,
12
+ # software distributed under the License is distributed on an
13
+ # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14
+ # KIND, either express or implied. See the License for the
15
+ # specific language governing permissions and limitations
16
+ # under the License.
17
+
18
+ require 'zlib'
19
+ require 'yaml'
20
+
21
+ Zlib::GzipReader.open(File.expand_path('../articles.yml.gz', __FILE__)) do |gzip|
22
+ puts "Reading articles from gzipped YAML..."
23
+ @documents = YAML.respond_to?(:load_documents) ? YAML.load_documents(gzip.read) :
24
+ YAML.load_stream(gzip.read)
25
+ end
26
+
27
+ # Truncate the default ActiveRecord logger output
28
+ ActiveRecord::Base.logger = ActiveSupport::Logger.new(STDERR)
29
+ ActiveRecord::Base.logger.instance_eval do
30
+ @formatter = lambda do |s, d, p, message|
31
+ message
32
+ .gsub(/\[("content", ".*?")\]/m) { |match| match[0..100] + '..."]' }
33
+ .gsub(/\[("body", ".*?")\]/m ) { |match| match[0..100] + '..."]' }
34
+ .strip
35
+ .concat("\n")
36
+ end
37
+ end
38
+
39
+ # Reduce verbosity and truncate the request body of Elasticsearch logger
40
+ Article.__elasticsearch__.client.transport.tracer.level = Logger::INFO
41
+ Article.__elasticsearch__.client.transport.tracer.formatter = lambda do |s, d, p, message|
42
+ "\n\n" + (message.size > 105 ? message[0..105].concat("...}'") : message) + "\n\n"
43
+ end
44
+
45
+ # Skip model callbacks
46
+ %w| _touch_callbacks
47
+ _commit_callbacks
48
+ after_add_for_categories
49
+ after_add_for_authorships
50
+ after_add_for_authors
51
+ after_add_for_comments |.each do |c|
52
+ Article.class.__send__ :define_method, c do; []; end
53
+ end
54
+
55
+ @documents.each do |document|
56
+ article = Article.create! document.slice(:title, :abstract, :content, :url, :shares, :published_on)
57
+
58
+ article.categories = document[:categories].map do |d|
59
+ Category.find_or_create_by! title: d
60
+ end
61
+
62
+ article.authors = document[:authors].map do |d|
63
+ first_name, last_name = d.split(' ').compact.map(&:strip)
64
+ Author.find_or_create_by! first_name: first_name, last_name: last_name
65
+ end
66
+
67
+ document[:comments].each { |d| article.comments.create! d }
68
+
69
+ article.save!
70
+ end
71
+
72
+ # Remove any jobs from the "elasticsearch" Sidekiq queue
73
+ #
74
+ require 'sidekiq/api'
75
+ Sidekiq::Queue.new("elasticsearch").clear