search_flip 1.0.0
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 +7 -0
- data/.gitignore +18 -0
- data/.travis.yml +34 -0
- data/Gemfile +7 -0
- data/LICENSE.txt +22 -0
- data/README.md +606 -0
- data/Rakefile +9 -0
- data/irb.rb +7 -0
- data/lib/search_flip/aggregatable.rb +69 -0
- data/lib/search_flip/aggregation.rb +57 -0
- data/lib/search_flip/bulk.rb +152 -0
- data/lib/search_flip/config.rb +21 -0
- data/lib/search_flip/criteria.rb +737 -0
- data/lib/search_flip/filterable.rb +240 -0
- data/lib/search_flip/http_client.rb +49 -0
- data/lib/search_flip/index.rb +545 -0
- data/lib/search_flip/json.rb +18 -0
- data/lib/search_flip/model.rb +21 -0
- data/lib/search_flip/post_filterable.rb +252 -0
- data/lib/search_flip/response.rb +319 -0
- data/lib/search_flip/result.rb +12 -0
- data/lib/search_flip/to_json.rb +31 -0
- data/lib/search_flip/version.rb +5 -0
- data/lib/search_flip.rb +82 -0
- data/search_flip.gemspec +35 -0
- data/test/database.yml +4 -0
- data/test/search_flip/aggregation_test.rb +212 -0
- data/test/search_flip/bulk_test.rb +55 -0
- data/test/search_flip/criteria_test.rb +825 -0
- data/test/search_flip/http_client_test.rb +35 -0
- data/test/search_flip/index_test.rb +350 -0
- data/test/search_flip/model_test.rb +39 -0
- data/test/search_flip/response_test.rb +136 -0
- data/test/search_flip/to_json_test.rb +30 -0
- data/test/search_flip_test.rb +26 -0
- data/test/test_helper.rb +243 -0
- metadata +258 -0
@@ -0,0 +1,737 @@
|
|
1
|
+
|
2
|
+
module SearchFlip
|
3
|
+
# The SearchFlip::Criteria class serves the purpose of chaining various
|
4
|
+
# filtering and aggregation methods. Each chainable method creates a new
|
5
|
+
# criteria object until a method is called that finally sends the respective
|
6
|
+
# request to ElasticSearch and returns the result.
|
7
|
+
#
|
8
|
+
# @example
|
9
|
+
# CommentIndex.where(public: true).sort(id: "desc").limit(1_000).records
|
10
|
+
# CommentIndex.range(:created_at, lt: Time.parse("2014-01-01").delete
|
11
|
+
# CommentIndex.search("hello world").total_entries
|
12
|
+
# CommentIndex.query(more_like_this: { "...", fields: ["description"] })]
|
13
|
+
# CommentIndex.exists(:user_id).paginate(page: 1, per_page: 100)
|
14
|
+
# CommentIndex.sort("_doc").find_each { |comment| "..." }
|
15
|
+
|
16
|
+
class Criteria
|
17
|
+
include SearchFlip::Filterable
|
18
|
+
include SearchFlip::PostFilterable
|
19
|
+
include SearchFlip::Aggregatable
|
20
|
+
extend Forwardable
|
21
|
+
|
22
|
+
attr_accessor :target, :profile_value, :source_value, :sort_values, :highlight_values, :suggest_values, :offset_value, :limit_value,
|
23
|
+
:includes_values, :eager_load_values, :preload_values, :failsafe_value, :scroll_args, :custom_value, :terminate_after_value, :timeout_value
|
24
|
+
|
25
|
+
# Creates a new criteria while merging the attributes (constraints,
|
26
|
+
# settings, etc) of the current criteria with the attributes of another one
|
27
|
+
# passed as argument. For multi-value contstraints the resulting criteria
|
28
|
+
# will include constraints of both criterias. For single-value constraints,
|
29
|
+
# the values of the criteria passed as an argument are used.
|
30
|
+
#
|
31
|
+
# @example
|
32
|
+
# CommentIndex.where(approved: true).merge(CommentIndex.range(:created_at, gt: Time.parse("2015-01-01")))
|
33
|
+
# CommentIndex.aggregate(:user_id).merge(CommentIndex.where(admin: true))
|
34
|
+
#
|
35
|
+
# @return [SearchFlip::Criteria] A newly created extended criteria
|
36
|
+
|
37
|
+
def merge(other)
|
38
|
+
other = other.criteria
|
39
|
+
|
40
|
+
fresh.tap do |criteria|
|
41
|
+
criteria.profile_value = other.profile_value if other.profile_value != nil
|
42
|
+
criteria.source_value = (criteria.source_value || []) + other.source_value if other.source_value
|
43
|
+
criteria.sort_values = (criteria.sort_values || []) + other.sort_values if other.sort_values
|
44
|
+
criteria.highlight_values = (criteria.highlight_values || {}).merge(other.highlight_values) if other.highlight_values
|
45
|
+
criteria.suggest_values = (criteria.suggest_values || {}).merge(other.suggest_values) if other.suggest_values
|
46
|
+
criteria.offset_value = other.offset_value if other.offset_value
|
47
|
+
criteria.limit_value = other.limit_value if other.limit_value
|
48
|
+
criteria.includes_values = (criteria.includes_values || []) + other.includes_values if other.includes_values
|
49
|
+
criteria.preload_values = (criteria.preload_values || []) + other.preload_values if other.preload_values
|
50
|
+
criteria.eager_load_values = (criteria.eager_load_values || []) + other.eager_load_values if other.eager_load_values
|
51
|
+
criteria.failsafe_value = other.failsafe_value if other.failsafe_value != nil
|
52
|
+
criteria.scroll_args = other.scroll_args if other.scroll_args
|
53
|
+
criteria.custom_value = (criteria.custom_value || {}).merge(other.custom_value) if other.custom_value
|
54
|
+
criteria.search_values = (criteria.search_values || []) + other.search_values if other.search_values
|
55
|
+
criteria.must_values = (criteria.must_values || []) + other.must_values if other.must_values
|
56
|
+
criteria.must_not_values = (criteria.must_not_values || []) + other.must_not_values if other.must_not_values
|
57
|
+
criteria.should_values = (criteria.should_values || []) + other.should_values if other.should_values
|
58
|
+
criteria.filter_values = (criteria.filter_values || []) + other.filter_values if other.filter_values
|
59
|
+
criteria.post_search_values = (criteria.post_search_values || []) + other.post_search_values if other.post_search_values
|
60
|
+
criteria.post_must_values = (criteria.post_must_values || []) + other.post_must_values if other.post_must_values
|
61
|
+
criteria.post_must_not_values = (criteria.post_must_not_values || []) + other.post_must_not_values if other.post_must_not_values
|
62
|
+
criteria.post_should_values = (criteria.post_should_values || []) + other.post_should_values if other.post_should_values
|
63
|
+
criteria.post_filter_values = (criteria.post_filter_vales || []) + other.post_filter_values if other.post_filter_values
|
64
|
+
criteria.aggregation_values = (criteria.aggregation_values || {}).merge(other.aggregation_values) if other.aggregation_values
|
65
|
+
criteria.terminate_after_value = other.terminate_after_value if other.terminate_after_value != nil
|
66
|
+
criteria.timeout_value = other.timeout_value if other.timeout_value != nil
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
# Specifies a query timeout, such that the processing will be stopped after
|
71
|
+
# that timeout and only the results calculated up to that point will be
|
72
|
+
# processed and returned.
|
73
|
+
#
|
74
|
+
# @example
|
75
|
+
# ProductIndex.timeout("3s").search("hello world")
|
76
|
+
#
|
77
|
+
# @return [SearchFlip::Criteria] A newly created extended criteria
|
78
|
+
|
79
|
+
def timeout(n)
|
80
|
+
fresh.tap do |criteria|
|
81
|
+
criteria.timeout_value = n
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
# Specifies early query termination, such that the processing will be
|
86
|
+
# stopped after the specified number of results has been accumulated.
|
87
|
+
#
|
88
|
+
# @example
|
89
|
+
# ProductIndex.terminate_after(10_000).search("hello world")
|
90
|
+
#
|
91
|
+
# @return [SearchFlip::Criteria] A newly created extended criteria
|
92
|
+
|
93
|
+
def terminate_after(n)
|
94
|
+
fresh.tap do |criteria|
|
95
|
+
criteria.terminate_after_value = n
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
# Creates a new criteria while removing all specified scopes. Currently,
|
100
|
+
# you can unscope :search, :post_search, :sort, :highlight, :suggest, :custom
|
101
|
+
# and :aggregate.
|
102
|
+
#
|
103
|
+
# @example
|
104
|
+
# CommentIndex.search("hello world").aggregate(:username).unscope(:search, :aggregate)
|
105
|
+
#
|
106
|
+
# @param scopes [Symbol] All scopes that you want to remove
|
107
|
+
#
|
108
|
+
# @return [SearchFlip::Criteria] A newly created extended criteria
|
109
|
+
|
110
|
+
def unscope(*scopes)
|
111
|
+
unknown = scopes - [:search, :post_search, :sort, :highlight, :suggest, :custom, :aggregate]
|
112
|
+
|
113
|
+
raise(ArgumentError, "Can't unscope #{unknown.join(", ")}") if unknown.size > 0
|
114
|
+
|
115
|
+
scopes = scopes.to_set
|
116
|
+
|
117
|
+
fresh.tap do |criteria|
|
118
|
+
criteria.search_values = nil if scopes.include?(:search)
|
119
|
+
criteria.post_search_values = nil if scopes.include?(:search)
|
120
|
+
criteria.sort_values = nil if scopes.include?(:sort)
|
121
|
+
criteria.hightlight_values = nil if scopes.include?(:highlight)
|
122
|
+
criteria.suggest_values = nil if scopes.include?(:suggest)
|
123
|
+
criteria.custom_values = nil if scopes.include?(:custom)
|
124
|
+
criteria.aggregation_values = nil if scopes.include?(:aggregate)
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
# @api private
|
129
|
+
#
|
130
|
+
# Convenience method to have a unified conversion api.
|
131
|
+
#
|
132
|
+
# @return [SearchFlip::Criteria] Simply returns self
|
133
|
+
|
134
|
+
def criteria
|
135
|
+
self
|
136
|
+
end
|
137
|
+
|
138
|
+
# Creates a new SearchFlip::Criteria.
|
139
|
+
#
|
140
|
+
# @param attributes [Hash] Attributes to initialize the Criteria with
|
141
|
+
|
142
|
+
def initialize(attributes = {})
|
143
|
+
attributes.each do |key, value|
|
144
|
+
self.send "#{key}=", value
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
# Generates the request object from the attributes specified via chaining,
|
149
|
+
# like eg offset, limit, query, filters, aggregations, etc and returns a
|
150
|
+
# Hash that later gets serialized as JSON.
|
151
|
+
#
|
152
|
+
# @return [Hash] The generated request object
|
153
|
+
|
154
|
+
def request
|
155
|
+
res = {}
|
156
|
+
|
157
|
+
if must_values || search_values || must_not_values || should_values || filter_values
|
158
|
+
if SearchFlip.version.to_i >= 2
|
159
|
+
res[:query] = {
|
160
|
+
bool: {}.
|
161
|
+
merge(must_values || search_values ? { must: (must_values || []) + (search_values || [])} : {}).
|
162
|
+
merge(must_not_values ? { must_not: must_not_values } : {}).
|
163
|
+
merge(should_values ? { should: should_values } : {}).
|
164
|
+
merge(filter_values ? { filter: filter_values } : {})
|
165
|
+
}
|
166
|
+
else
|
167
|
+
filters = (filter_values || []) + (must_not_values || []).map { |must_not_value| { not: must_not_value } }
|
168
|
+
|
169
|
+
queries = {}.
|
170
|
+
merge(must_values || search_values ? { must: (must_values || []) + (search_values || []) } : {}).
|
171
|
+
merge(should_values ? { should: should_values } : {})
|
172
|
+
|
173
|
+
if filters.size > 0
|
174
|
+
res[:query] = {
|
175
|
+
filtered: {}.
|
176
|
+
merge(queries.size > 0 ? { query: { bool: queries } } : {}).
|
177
|
+
merge(filter: filters.size > 1 ? { and: filters } : filters.first)
|
178
|
+
}
|
179
|
+
else
|
180
|
+
res[:query] = { bool: queries }
|
181
|
+
end
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
res.update from: offset_value_with_default, size: limit_value_with_default
|
186
|
+
|
187
|
+
res[:timeout] = timeout_value if timeout_value
|
188
|
+
res[:terminate_after] = terminate_after_value if terminate_after_value
|
189
|
+
res[:highlight] = highlight_values if highlight_values
|
190
|
+
res[:suggest] = suggest_values if suggest_values
|
191
|
+
res[:sort] = sort_values if sort_values
|
192
|
+
res[:aggregations] = aggregation_values if aggregation_values
|
193
|
+
|
194
|
+
if post_must_values || post_search_values || post_must_not_values || post_should_values || post_filter_values
|
195
|
+
if SearchFlip.version.to_i >= 2
|
196
|
+
res[:post_filter] = {
|
197
|
+
bool: {}.
|
198
|
+
merge(post_must_values || post_search_values ? { must: (post_must_values || []) + (post_search_values || []) } : {}).
|
199
|
+
merge(post_must_not_values ? { must_not: post_must_not_values } : {}).
|
200
|
+
merge(post_should_values ? { should: post_should_values } : {}).
|
201
|
+
merge(post_filter_values ? { filter: post_filter_values } : {})
|
202
|
+
}
|
203
|
+
else
|
204
|
+
post_filters = (post_filter_values || []) + (post_must_not_values || []).map { |post_must_not_value| { not: post_must_not_value } }
|
205
|
+
|
206
|
+
post_queries = {}.
|
207
|
+
merge(post_must_values || post_search_values ? { must: (post_must_values || []) + (post_search_values || []) } : {}).
|
208
|
+
merge(post_should_values ? { should: post_should_values } : {})
|
209
|
+
|
210
|
+
post_filters_and_queries = post_filters + (post_queries.size > 0 ? [bool: post_queries] : [])
|
211
|
+
|
212
|
+
res[:post_filter] = post_filters_and_queries.size > 1 ? { and: post_filters_and_queries } : post_filters_and_queries.first
|
213
|
+
end
|
214
|
+
end
|
215
|
+
|
216
|
+
res[:_source] = source_value unless source_value.nil?
|
217
|
+
res[:profile] = true if profile_value
|
218
|
+
|
219
|
+
res.update(custom_value) if custom_value
|
220
|
+
|
221
|
+
res
|
222
|
+
end
|
223
|
+
|
224
|
+
# Adds highlighting of the given fields to the request.
|
225
|
+
#
|
226
|
+
# @example
|
227
|
+
# CommentIndex.highlight([:title, :message])
|
228
|
+
# CommentIndex.highlight(:title).highlight(:description)
|
229
|
+
# CommentIndex.highlight(:title, require_field_match: false)
|
230
|
+
# CommentIndex.highlight(title: { type: "fvh" })
|
231
|
+
#
|
232
|
+
# @example
|
233
|
+
# query = CommentIndex.highlight(:title).search("hello")
|
234
|
+
# query.results[0].highlight.title # => "<em>hello</em> world"
|
235
|
+
#
|
236
|
+
# @param fields [Hash, Array, String, Symbol] The fields to highligt.
|
237
|
+
# Supports raw ElasticSearch values by passing a Hash.
|
238
|
+
#
|
239
|
+
# @param options [Hash] Extra highlighting options. Check out the ElasticSearch
|
240
|
+
# docs for further details.
|
241
|
+
#
|
242
|
+
# @return [SearchFlip::Criteria] A new criteria including the highlighting
|
243
|
+
|
244
|
+
def highlight(fields, options = {})
|
245
|
+
fresh.tap do |criteria|
|
246
|
+
criteria.highlight_values = (criteria.highlight_values || {}).merge(options)
|
247
|
+
|
248
|
+
hash = if fields.is_a?(Hash)
|
249
|
+
fields
|
250
|
+
elsif fields.is_a?(Array)
|
251
|
+
fields.each_with_object({}) { |field, h| h[field] = {} }
|
252
|
+
else
|
253
|
+
{ fields => {} }
|
254
|
+
end
|
255
|
+
|
256
|
+
criteria.highlight_values[:fields] = (criteria.highlight_values[:fields] || {}).merge(hash)
|
257
|
+
end
|
258
|
+
end
|
259
|
+
|
260
|
+
# Adds a suggestion section with the given name to the request.
|
261
|
+
#
|
262
|
+
# @example
|
263
|
+
# query = CommentIndex.suggest(:suggestion, text: "helo", term: { field: "message" })
|
264
|
+
# query.suggestions(:suggestion).first["text"] # => "hello"
|
265
|
+
#
|
266
|
+
# @param name [String, Symbol] The name of the suggestion section
|
267
|
+
#
|
268
|
+
# @param options [Hash] Additional suggestion options. Check out the ElasticSearch
|
269
|
+
# docs for further details.
|
270
|
+
#
|
271
|
+
# @return [SearchFlip::Criteria] A new criteria including the suggestion section
|
272
|
+
|
273
|
+
def suggest(name, options = {})
|
274
|
+
fresh.tap do |criteria|
|
275
|
+
criteria.suggest_values = (criteria.suggest_values || {}).merge(name => options)
|
276
|
+
end
|
277
|
+
end
|
278
|
+
|
279
|
+
# Sets whether or not query profiling should be enabled.
|
280
|
+
#
|
281
|
+
# @example
|
282
|
+
# query = CommentIndex.profile(true)
|
283
|
+
# query.raw_response["profile"] # => { "shards" => ... }
|
284
|
+
#
|
285
|
+
# @param value [Boolean] Whether query profiling should be enabled or not
|
286
|
+
#
|
287
|
+
# @return [SearchFlip::Criteria] A newly created extended criteria
|
288
|
+
|
289
|
+
def profile(value)
|
290
|
+
fresh.tap do |criteria|
|
291
|
+
criteria.profile_value = value
|
292
|
+
end
|
293
|
+
end
|
294
|
+
|
295
|
+
# Adds scrolling to the request with or without an already existing scroll
|
296
|
+
# id and using the specified timeout.
|
297
|
+
#
|
298
|
+
# @example
|
299
|
+
# query = CommentIndex.scroll(timeout: "5m")
|
300
|
+
#
|
301
|
+
# until query.records.empty?
|
302
|
+
# # ...
|
303
|
+
#
|
304
|
+
# query = query.scroll(id: query.scroll_id, timeout: "5m")
|
305
|
+
# end
|
306
|
+
#
|
307
|
+
# @param id [String, nil] The scroll id of the last request returned by
|
308
|
+
# SearchFlip or nil
|
309
|
+
#
|
310
|
+
# @param timeout [String] The timeout of the scroll request, ie. how long
|
311
|
+
# SearchFlip should keep the scroll handle open
|
312
|
+
#
|
313
|
+
# @return [SearchFlip::Criteria] A newly created extended criteria
|
314
|
+
|
315
|
+
def scroll(id: nil, timeout: "1m")
|
316
|
+
fresh.tap do |criteria|
|
317
|
+
criteria.scroll_args = { id: id, timeout: timeout }
|
318
|
+
end
|
319
|
+
end
|
320
|
+
|
321
|
+
# Sends a delete by query request to ElasticSearch, such that all documents
|
322
|
+
# matching the query get deleted. Please note, for certain ElasticSearch
|
323
|
+
# versions you need to install the delete-by-query plugin to get support
|
324
|
+
# for this feature. Refreshes the index if the auto_refresh is enabled.
|
325
|
+
# Raises SearchFlip::ResponseError in case any errors occur.
|
326
|
+
#
|
327
|
+
# @see SearchFlip::Config See SearchFlip::Config for auto_refresh
|
328
|
+
#
|
329
|
+
# @example
|
330
|
+
# CommentIndex.range(lt: Time.parse("2014-01-01")).delete
|
331
|
+
# CommentIndex.where(public: false).delete
|
332
|
+
|
333
|
+
def delete
|
334
|
+
_request = request.dup
|
335
|
+
_request.delete(:from)
|
336
|
+
_request.delete(:size)
|
337
|
+
|
338
|
+
if SearchFlip.version.to_i >= 5
|
339
|
+
SearchFlip::HTTPClient.post("#{target.type_url}/_delete_by_query", json: _request)
|
340
|
+
else
|
341
|
+
SearchFlip::HTTPClient.delete("#{target.type_url}/_query", json: _request)
|
342
|
+
end
|
343
|
+
|
344
|
+
target.refresh if SearchFlip::Config[:auto_refresh]
|
345
|
+
|
346
|
+
true
|
347
|
+
end
|
348
|
+
|
349
|
+
# Use to specify which fields of the source document you want ElasticSearch
|
350
|
+
# to return for each matching result.
|
351
|
+
#
|
352
|
+
# @example
|
353
|
+
# CommentIndex.source([:id, :message]).search("hello world")
|
354
|
+
#
|
355
|
+
# @param value [Array] Array listing the field names of the source document
|
356
|
+
#
|
357
|
+
# @return [SearchFlip::Criteria] A newly created extended criteria
|
358
|
+
|
359
|
+
def source(value)
|
360
|
+
fresh.tap do |criteria|
|
361
|
+
criteria.source_value = value
|
362
|
+
end
|
363
|
+
end
|
364
|
+
|
365
|
+
# Specify associations of the target model you want to include via
|
366
|
+
# ActiveRecord's or other ORM's mechanisms when records get fetched from
|
367
|
+
# the database.
|
368
|
+
#
|
369
|
+
# @example
|
370
|
+
# CommentIndex.includes(:user, :post).records
|
371
|
+
# PostIndex.includes(:comments => :user).records
|
372
|
+
#
|
373
|
+
# @param args The args that get passed to the includes method of
|
374
|
+
# ActiveRecord or other ORMs
|
375
|
+
#
|
376
|
+
# @return [SearchFlip::Criteria] A newly created extended criteria
|
377
|
+
|
378
|
+
def includes(*args)
|
379
|
+
fresh.tap do |criteria|
|
380
|
+
criteria.includes_values = (includes_values || []) + args
|
381
|
+
end
|
382
|
+
end
|
383
|
+
|
384
|
+
# Specify associations of the target model you want to eager load via
|
385
|
+
# ActiveRecord's or other ORM's mechanisms when records get fetched from
|
386
|
+
# the database.
|
387
|
+
#
|
388
|
+
# @example
|
389
|
+
# CommentIndex.eager_load(:user, :post).records
|
390
|
+
# PostIndex.eager_load(:comments => :user).records
|
391
|
+
#
|
392
|
+
# @param args The args that get passed to the eager load method of
|
393
|
+
# ActiveRecord or other ORMs
|
394
|
+
#
|
395
|
+
# @return [SearchFlip::Criteria] A newly created extended criteria
|
396
|
+
|
397
|
+
def eager_load(*args)
|
398
|
+
fresh.tap do |criteria|
|
399
|
+
criteria.eager_load_values = (eager_load_values || []) + args
|
400
|
+
end
|
401
|
+
end
|
402
|
+
|
403
|
+
# Specify associations of the target model you want to preload via
|
404
|
+
# ActiveRecord's or other ORM's mechanisms when records get fetched from
|
405
|
+
# the database.
|
406
|
+
#
|
407
|
+
# @example
|
408
|
+
# CommentIndex.preload(:user, :post).records
|
409
|
+
# PostIndex.includes(:comments => :user).records
|
410
|
+
#
|
411
|
+
# @param args The args that get passed to the preload method of
|
412
|
+
# ActiveRecord or other ORMs
|
413
|
+
#
|
414
|
+
# @return [SearchFlip::Criteria] A newly created extended criteria
|
415
|
+
|
416
|
+
def preload(*args)
|
417
|
+
fresh.tap do |criteria|
|
418
|
+
criteria.preload_values = (preload_values || []) + args
|
419
|
+
end
|
420
|
+
end
|
421
|
+
|
422
|
+
# Specify the sort order you want ElasticSearch to use for sorting the
|
423
|
+
# results. When you call this multiple times, the sort orders are appended
|
424
|
+
# to the already existing ones. The sort arguments get passed to
|
425
|
+
# ElasticSearch without modifications, such that you can use sort by
|
426
|
+
# script, etc here as well.
|
427
|
+
#
|
428
|
+
# @example Default usage
|
429
|
+
# CommentIndex.sort(:user_id, :id)
|
430
|
+
#
|
431
|
+
# # Same as
|
432
|
+
#
|
433
|
+
# CommentIndex.sort(:user_id).sort(:id)
|
434
|
+
#
|
435
|
+
# @example Default hash usage
|
436
|
+
# CommentIndex.sort(user_id: "asc").sort(id: "desc")
|
437
|
+
#
|
438
|
+
# # Same as
|
439
|
+
#
|
440
|
+
# CommentIndex.sort({ user_id: "asc" }, { id: "desc" })
|
441
|
+
#
|
442
|
+
# @example Sort by native script
|
443
|
+
# CommentIndex.sort("_script" => "sort_script", lang: "native", order: "asc", type: "number")
|
444
|
+
#
|
445
|
+
# @param args The sort values that get passed to ElasticSearch
|
446
|
+
#
|
447
|
+
# @return [SearchFlip::Criteria] A newly created extended criteria
|
448
|
+
|
449
|
+
def sort(*args)
|
450
|
+
fresh.tap do |criteria|
|
451
|
+
criteria.sort_values = (sort_values || []) + args
|
452
|
+
end
|
453
|
+
end
|
454
|
+
|
455
|
+
alias_method :order, :sort
|
456
|
+
|
457
|
+
# Specify the sort order you want ElasticSearch to use for sorting the
|
458
|
+
# results with already existing sort orders being removed.
|
459
|
+
#
|
460
|
+
# @example
|
461
|
+
# CommentIndex.sort(user_id: "asc").resort(id: "desc")
|
462
|
+
#
|
463
|
+
# # Same as
|
464
|
+
#
|
465
|
+
# CommentIndex.sort(id: "desc")
|
466
|
+
#
|
467
|
+
# @return [SearchFlip::Criteria] A newly created extended criteria
|
468
|
+
#
|
469
|
+
# @see #sort See #sort for more details
|
470
|
+
|
471
|
+
def resort(*args)
|
472
|
+
fresh.tap do |criteria|
|
473
|
+
criteria.sort_values = args
|
474
|
+
end
|
475
|
+
end
|
476
|
+
|
477
|
+
alias_method :reorder, :resort
|
478
|
+
|
479
|
+
# Adds a fully custom field/section to the request, such that upcoming or
|
480
|
+
# minor ElasticSearch features as well as other custom requirements can be
|
481
|
+
# used without having yet specialized criteria methods.
|
482
|
+
#
|
483
|
+
# @note Use with caution, because using #custom will potentiall override
|
484
|
+
# other sections like +aggregations+, +query+, +sort+, etc if you use the
|
485
|
+
# the same section names.
|
486
|
+
#
|
487
|
+
# @example
|
488
|
+
# CommentIndex.custom(section: { argument: "value" }).request
|
489
|
+
# => {:section=>{:argument=>"value"},...}
|
490
|
+
#
|
491
|
+
# @param hash [Hash] The custom section that is added to the request
|
492
|
+
#
|
493
|
+
# @return [SearchFlip::Criteria] A newly created extended criteria
|
494
|
+
|
495
|
+
def custom(hash)
|
496
|
+
fresh.tap do |criteria|
|
497
|
+
criteria.custom_value = (custom_value || {}).merge(hash)
|
498
|
+
end
|
499
|
+
end
|
500
|
+
|
501
|
+
# Sets the request offset, ie SearchFlip's from parameter that is used
|
502
|
+
# to skip results in the result set from being returned.
|
503
|
+
#
|
504
|
+
# @example
|
505
|
+
# CommentIndex.offset(100)
|
506
|
+
#
|
507
|
+
# @param n [Fixnum] The offset value, ie the number of results that are
|
508
|
+
# skipped in the result set
|
509
|
+
#
|
510
|
+
# @return [SearchFlip::Criteria] A newly created extended criteria
|
511
|
+
|
512
|
+
def offset(n)
|
513
|
+
fresh.tap do |criteria|
|
514
|
+
criteria.offset_value = n.to_i
|
515
|
+
end
|
516
|
+
end
|
517
|
+
|
518
|
+
# Returns the offset value or, if not yet set, the default limit value (0).
|
519
|
+
#
|
520
|
+
# @return [Fixnum] The offset value
|
521
|
+
|
522
|
+
def offset_value_with_default
|
523
|
+
(offset_value || 0).to_i
|
524
|
+
end
|
525
|
+
|
526
|
+
# Sets the request limit, ie ElasticSearch's size parameter that is used
|
527
|
+
# to restrict the results that get returned.
|
528
|
+
#
|
529
|
+
# @example
|
530
|
+
# CommentIndex.limit(100)
|
531
|
+
#
|
532
|
+
# @param n [Fixnum] The limit value, ie the max number of results that
|
533
|
+
# should be returned
|
534
|
+
#
|
535
|
+
# @return [SearchFlip::Criteria] A newly created extended criteria
|
536
|
+
|
537
|
+
def limit(n)
|
538
|
+
fresh.tap do |criteria|
|
539
|
+
criteria.limit_value = n.to_i
|
540
|
+
end
|
541
|
+
end
|
542
|
+
|
543
|
+
# Returns the limit value or, if not yet set, the default limit value (30).
|
544
|
+
#
|
545
|
+
# @return [Fixnum] The limit value
|
546
|
+
|
547
|
+
def limit_value_with_default
|
548
|
+
(limit_value || 30).to_i
|
549
|
+
end
|
550
|
+
|
551
|
+
# Sets pagination parameters for the criteria by using offset and limit,
|
552
|
+
# ie ElasticSearch's from and size parameters.
|
553
|
+
#
|
554
|
+
# @example
|
555
|
+
# CommentIndex.paginate(page: 3)
|
556
|
+
# CommentIndex.paginate(page: 5, per_page: 60)
|
557
|
+
#
|
558
|
+
# @param page [#to_i] The current page
|
559
|
+
# @param per_page [#to_i] The number of results per page
|
560
|
+
#
|
561
|
+
# @return [SearchFlip::Criteria] A newly created extended criteria
|
562
|
+
|
563
|
+
def paginate(page:, per_page: limit_value_with_default)
|
564
|
+
page = [page.to_i, 1].max
|
565
|
+
per_page = per_page.to_i
|
566
|
+
|
567
|
+
offset((page - 1) * per_page).limit(per_page)
|
568
|
+
end
|
569
|
+
|
570
|
+
def page(n)
|
571
|
+
paginate(page: n)
|
572
|
+
end
|
573
|
+
|
574
|
+
def per(n)
|
575
|
+
paginate(page: offset_value_with_default / limit_value_with_default + 1, per_page: n)
|
576
|
+
end
|
577
|
+
|
578
|
+
# Fetches the records specified by the criteria in batches using the
|
579
|
+
# ElasicSearch scroll API and yields each batch. The batch size and scroll
|
580
|
+
# API timeout can be specified. Check out the ElasticSearch docs for
|
581
|
+
# further details.
|
582
|
+
#
|
583
|
+
# @example
|
584
|
+
# CommentIndex.search("hello world").find_in_batches(batch_size: 100) do |batch|
|
585
|
+
# # ...
|
586
|
+
# end
|
587
|
+
#
|
588
|
+
# @param options [Hash] The options to control the fetching of batches
|
589
|
+
# @option options batch_size [Fixnum] The number of records to fetch per
|
590
|
+
# batch. Uses #limit to control the batch size.
|
591
|
+
# @option options timeout [String] The timeout per scroll request, ie how
|
592
|
+
# long ElasticSearch will keep the request handle open.
|
593
|
+
#
|
594
|
+
# @return [SearchFlip::Criteria] A newly created extended criteria
|
595
|
+
|
596
|
+
def find_in_batches(options = {})
|
597
|
+
return enum_for(:find_in_batches, options) unless block_given?
|
598
|
+
|
599
|
+
batch_size = options[:batch_size] || 1_000
|
600
|
+
timeout = options[:timeout] || "1m"
|
601
|
+
|
602
|
+
criteria = limit(batch_size).scroll(timeout: timeout)
|
603
|
+
|
604
|
+
until criteria.records.empty?
|
605
|
+
yield criteria.records
|
606
|
+
|
607
|
+
criteria = criteria.scroll(id: criteria.scroll_id, timeout: timeout)
|
608
|
+
end
|
609
|
+
end
|
610
|
+
|
611
|
+
# Fetches the records specified by the relatin in batches using the
|
612
|
+
# ElasticSearch scroll API and yields each record. The batch size and
|
613
|
+
# scroll API timeout can be specified. Check out the ElasticSearch docs for
|
614
|
+
# further details.
|
615
|
+
#
|
616
|
+
# @example
|
617
|
+
# CommentIndex.search("hello world").find_each(batch_size: 100) do |record|
|
618
|
+
# # ...
|
619
|
+
# end
|
620
|
+
#
|
621
|
+
# @param options [Hash] The options to control the fetching of batches
|
622
|
+
# @option options batch_size [Fixnum] The number of records to fetch per
|
623
|
+
# batch. Uses #limit to control the batch size.
|
624
|
+
# @option options timeout [String] The timeout per scroll request, ie how
|
625
|
+
# long ElasticSearch will keep the request handle open.
|
626
|
+
#
|
627
|
+
# @return [SearchFlip::Criteria] A newly created extended criteria
|
628
|
+
|
629
|
+
def find_each(options = {})
|
630
|
+
return enum_for(:find_each, options) unless block_given?
|
631
|
+
|
632
|
+
find_in_batches options do |batch|
|
633
|
+
batch.each do |record|
|
634
|
+
yield record
|
635
|
+
end
|
636
|
+
end
|
637
|
+
end
|
638
|
+
|
639
|
+
alias_method :each, :find_each
|
640
|
+
|
641
|
+
# Executes the search request for the current criteria, ie sends the
|
642
|
+
# request to ElasticSearch and returns the response. Connection and
|
643
|
+
# response errors will be rescued if you specify the criteria to be
|
644
|
+
# #failsafe, such that an empty response is returned instead.
|
645
|
+
#
|
646
|
+
# @param base_url An optional alternative base_url to send the request
|
647
|
+
# to for e.g. proxying
|
648
|
+
#
|
649
|
+
# @example
|
650
|
+
# response = CommentIndex.search("hello world").execute
|
651
|
+
#
|
652
|
+
# @return [SearchFlip::Response] The response object
|
653
|
+
|
654
|
+
def execute(base_url: target.base_url)
|
655
|
+
@response ||= begin
|
656
|
+
http_request = SearchFlip::HTTPClient.headers(accept: "application/json")
|
657
|
+
|
658
|
+
http_response =
|
659
|
+
if scroll_args && scroll_args[:id]
|
660
|
+
if SearchFlip.version.to_i >= 2
|
661
|
+
http_request.post("#{base_url}/_search/scroll", json: { scroll: scroll_args[:timeout], scroll_id: scroll_args[:id] })
|
662
|
+
else
|
663
|
+
http_request.headers(content_type: "text/plain").post("#{base_url}/_search/scroll", params: { scroll: scroll_args[:timeout] }, body: scroll_args[:id])
|
664
|
+
end
|
665
|
+
elsif scroll_args
|
666
|
+
http_request.post("#{target.type_url(base_url: base_url)}/_search", params: { scroll: scroll_args[:timeout] }, json: request)
|
667
|
+
else
|
668
|
+
http_request.post("#{target.type_url(base_url: base_url)}/_search", json: request)
|
669
|
+
end
|
670
|
+
|
671
|
+
SearchFlip::Response.new(self, http_response.parse)
|
672
|
+
rescue SearchFlip::ConnectionError, SearchFlip::ResponseError => e
|
673
|
+
raise e unless failsafe_value
|
674
|
+
|
675
|
+
SearchFlip::Response.new(self, "took" => 0, "hits" => { "total" => 0, "hits" => [] })
|
676
|
+
end
|
677
|
+
end
|
678
|
+
|
679
|
+
alias_method :response, :execute
|
680
|
+
|
681
|
+
# Marks the criteria to be failsafe, ie certain exceptions raised due to
|
682
|
+
# invalid queries, inavailability of ElasticSearch, etc get rescued and an
|
683
|
+
# empty criteria is returned instead.
|
684
|
+
#
|
685
|
+
# @see #execute See #execute for further details
|
686
|
+
#
|
687
|
+
# @example
|
688
|
+
# CommentIndex.search("invalid/request").execute
|
689
|
+
# # raises SearchFlip::ResponseError
|
690
|
+
#
|
691
|
+
# # ...
|
692
|
+
#
|
693
|
+
# CommentIndex.search("invalid/request").failsafe(true).execute
|
694
|
+
# # => #<SearchFlip::Response ...>
|
695
|
+
#
|
696
|
+
# @param value [Boolean] Whether or not the criteria should be failsafe
|
697
|
+
#
|
698
|
+
# @return [SearchFlip::Response] A newly created extended criteria
|
699
|
+
|
700
|
+
def failsafe(value)
|
701
|
+
fresh.tap do |criteria|
|
702
|
+
criteria.failsafe_value = value
|
703
|
+
end
|
704
|
+
end
|
705
|
+
|
706
|
+
# Returns a fresh, ie dupped, criteria with the response cache being
|
707
|
+
# cleared.
|
708
|
+
#
|
709
|
+
# @example
|
710
|
+
# CommentIndex.search("hello world").fresh
|
711
|
+
#
|
712
|
+
# @return [SearchFlip::Response] A dupped criteria with the response
|
713
|
+
# cache being cleared
|
714
|
+
|
715
|
+
def fresh
|
716
|
+
dup.tap do |criteria|
|
717
|
+
criteria.instance_variable_set(:@response, nil)
|
718
|
+
end
|
719
|
+
end
|
720
|
+
|
721
|
+
def respond_to?(name, *args)
|
722
|
+
super || target.respond_to?(name, *args)
|
723
|
+
end
|
724
|
+
|
725
|
+
def method_missing(name, *args, &block)
|
726
|
+
if target.respond_to?(name)
|
727
|
+
merge(target.send(name, *args, &block))
|
728
|
+
else
|
729
|
+
super
|
730
|
+
end
|
731
|
+
end
|
732
|
+
|
733
|
+
def_delegators :response, :total_entries, :total_count, :current_page, :previous_page, :prev_page, :next_page, :first_page?, :last_page?, :out_of_range?, :total_pages,
|
734
|
+
:hits, :ids, :count, :size, :length, :took, :aggregations, :suggestions, :scope, :results, :records, :scroll_id, :raw_response
|
735
|
+
end
|
736
|
+
end
|
737
|
+
|