search_flip 3.8.0 → 4.0.0.beta

Sign up to get free protection for your applications and to get access to all the features.
@@ -16,28 +16,20 @@ module SearchFlip
16
16
  @base_url = options[:base_url] || SearchFlip::Config[:base_url]
17
17
  @http_client = options[:http_client] || SearchFlip::HTTPClient.new
18
18
  @bulk_limit = options[:bulk_limit] || SearchFlip::Config[:bulk_limit]
19
- end
20
-
21
- # Queries and returns the Elasticsearch distribution used.
22
- #
23
- # @example
24
- # connection.distribution # => e.g. "opensearch"
25
- #
26
- # @return [String] The Elasticsearch distribution
27
-
28
- def distribution
29
- @distribution ||= SearchFlip::JSON.parse(version_response.to_s)["version"]["distribution"]
19
+ @version_mutex = Mutex.new
30
20
  end
31
21
 
32
22
  # Queries and returns the Elasticsearch version used.
33
23
  #
34
24
  # @example
35
- # connection.version # => e.g. "2.4.1"
25
+ # connection.version # => e.g. 2.4.1
36
26
  #
37
27
  # @return [String] The Elasticsearch version
38
28
 
39
29
  def version
40
- @version ||= SearchFlip::JSON.parse(version_response.to_s)["version"]["number"]
30
+ @version_mutex.synchronize do
31
+ @version ||= http_client.headers(accept: "application/json").get("#{base_url}/").parse["version"]["number"]
32
+ end
41
33
  end
42
34
 
43
35
  # Queries and returns the Elasticsearch cluster health.
@@ -48,9 +40,7 @@ module SearchFlip
48
40
  # @return [Hash] The raw response
49
41
 
50
42
  def cluster_health
51
- response = http_client.headers(accept: "application/json").get("#{base_url}/_cluster/health")
52
-
53
- SearchFlip::JSON.parse(response.to_s)
43
+ http_client.headers(accept: "application/json").get("#{base_url}/_cluster/health").parse
54
44
  end
55
45
 
56
46
  # Uses the Elasticsearch Multi Search API to execute multiple search requests
@@ -68,7 +58,7 @@ module SearchFlip
68
58
  def msearch(criterias)
69
59
  payload = criterias.flat_map do |criteria|
70
60
  [
71
- SearchFlip::JSON.generate(index: criteria.target.index_name_with_prefix, **(distribution.nil? && version.to_i < 8 ? { type: criteria.target.type_name } : {})),
61
+ SearchFlip::JSON.generate(index: criteria.target.index_name_with_prefix, type: criteria.target.type_name),
72
62
  SearchFlip::JSON.generate(criteria.request)
73
63
  ]
74
64
  end
@@ -100,11 +90,10 @@ module SearchFlip
100
90
  # @return [Hash] The raw response
101
91
 
102
92
  def update_aliases(payload)
103
- response = http_client
93
+ http_client
104
94
  .headers(accept: "application/json", content_type: "application/json")
105
95
  .post("#{base_url}/_aliases", body: SearchFlip::JSON.generate(payload))
106
-
107
- SearchFlip::JSON.parse(response.to_s)
96
+ .parse
108
97
  end
109
98
 
110
99
  # Sends an analyze request to Elasticsearch. Raises
@@ -116,11 +105,10 @@ module SearchFlip
116
105
  # @return [Hash] The raw response
117
106
 
118
107
  def analyze(request, params = {})
119
- response = http_client
108
+ http_client
120
109
  .headers(accept: "application/json")
121
110
  .post("#{base_url}/_analyze", json: request, params: params)
122
-
123
- SearchFlip::JSON.parse(response.to_s)
111
+ .parse
124
112
  end
125
113
 
126
114
  # Fetches information about the specified index aliases. Raises
@@ -136,11 +124,10 @@ module SearchFlip
136
124
  # @return [Hash] The raw response
137
125
 
138
126
  def get_aliases(index_name: "*", alias_name: "*")
139
- response = http_client
127
+ http_client
140
128
  .headers(accept: "application/json", content_type: "application/json")
141
129
  .get("#{base_url}/#{index_name}/_alias/#{alias_name}")
142
-
143
- SearchFlip::JSON.parse(response.to_s)
130
+ .parse
144
131
  end
145
132
 
146
133
  # Returns whether or not the associated Elasticsearch alias already
@@ -172,11 +159,10 @@ module SearchFlip
172
159
  # @return [Array] The raw response
173
160
 
174
161
  def get_indices(name = "*", params: {})
175
- response = http_client
162
+ http_client
176
163
  .headers(accept: "application/json", content_type: "application/json")
177
164
  .get("#{base_url}/_cat/indices/#{name}", params: params)
178
-
179
- SearchFlip::JSON.parse(response.to_s)
165
+ .parse
180
166
  end
181
167
 
182
168
  alias_method :cat_indices, :get_indices
@@ -273,11 +259,10 @@ module SearchFlip
273
259
  # @return [Hash] The index settings
274
260
 
275
261
  def get_index_settings(index_name)
276
- response = http_client
262
+ http_client
277
263
  .headers(accept: "application/json")
278
264
  .get("#{index_url(index_name)}/_settings")
279
-
280
- SearchFlip::JSON.parse(response.to_s)
265
+ .parse
281
266
  end
282
267
 
283
268
  # Sends a refresh request to Elasticsearch. Raises
@@ -287,7 +272,7 @@ module SearchFlip
287
272
  # @return [Boolean] Returns true or raises SearchFlip::ResponseError
288
273
 
289
274
  def refresh(index_names = nil)
290
- http_client.post("#{index_names ? index_url(Array(index_names).join(",")) : base_url}/_refresh")
275
+ http_client.post("#{index_names ? index_url(Array(index_names).join(",")) : base_url}/_refresh", json: {})
291
276
 
292
277
  true
293
278
  end
@@ -304,8 +289,8 @@ module SearchFlip
304
289
  # @return [Boolean] Returns true or raises SearchFlip::ResponseError
305
290
 
306
291
  def update_mapping(index_name, mapping, type_name: nil)
307
- url = type_name && distribution.nil? && version.to_i < 8 ? type_url(index_name, type_name) : index_url(index_name)
308
- params = type_name && distribution.nil? && version.to_f >= 6.7 && version.to_i < 8 ? { include_type_name: true } : {}
292
+ url = type_name ? type_url(index_name, type_name) : index_url(index_name)
293
+ params = type_name && version.to_f >= 6.7 ? { include_type_name: true } : {}
309
294
 
310
295
  http_client.put("#{url}/_mapping", params: params, json: mapping)
311
296
 
@@ -322,12 +307,10 @@ module SearchFlip
322
307
  # @return [Hash] The current type mapping
323
308
 
324
309
  def get_mapping(index_name, type_name: nil)
325
- url = type_name && distribution.nil? && version.to_i < 8 ? type_url(index_name, type_name) : index_url(index_name)
326
- params = type_name && distribution.nil? && version.to_f >= 6.7 && version.to_i < 8 ? { include_type_name: true } : {}
327
-
328
- response = http_client.headers(accept: "application/json").get("#{url}/_mapping", params: params)
310
+ url = type_name ? type_url(index_name, type_name) : index_url(index_name)
311
+ params = type_name && version.to_f >= 6.7 ? { include_type_name: true } : {}
329
312
 
330
- SearchFlip::JSON.parse(response.to_s)
313
+ http_client.headers(accept: "application/json").get("#{url}/_mapping", params: params).parse
331
314
  end
332
315
 
333
316
  # Deletes the specified index from Elasticsearch. Raises
@@ -359,51 +342,6 @@ module SearchFlip
359
342
  raise e
360
343
  end
361
344
 
362
- # Initiates and yields a bulk object, such that index, import, create,
363
- # update and delete requests can be appended to the bulk request. Please
364
- # note that you need to manually pass the desired index name as well as
365
- # type name (depending on the Elasticsearch version) when using #bulk on a
366
- # connection object or Elasticsearch will return an error. After the bulk
367
- # requests are successfully processed all existing indices will
368
- # subsequently be refreshed when auto_refresh is enabled.
369
- #
370
- # @see SearchFlip::Config See SearchFlip::Config for auto_refresh
371
- #
372
- # @example
373
- # connection = SearchFlip::Connection.new
374
- #
375
- # connection.bulk ignore_errors: [409] do |bulk|
376
- # bulk.create comment.id, CommentIndex.serialize(comment),
377
- # _index: CommentIndex.index_name, version: comment.version, version_type: "external_gte"
378
- #
379
- # bulk.delete product.id, _index: ProductIndex.index_name, routing: product.user_id
380
- #
381
- # # ...
382
- # end
383
- #
384
- # @param options [Hash] Specifies options regarding the bulk indexing
385
- # @option options ignore_errors [Array] Specifies an array of http status
386
- # codes that shouldn't raise any exceptions, like eg 409 for conflicts,
387
- # ie when optimistic concurrency control is used.
388
- # @option options raise [Boolean] Prevents any exceptions from being
389
- # raised. Please note that this only applies to the bulk response, not to
390
- # the request in general, such that connection errors, etc will still
391
- # raise.
392
-
393
- def bulk(options = {})
394
- default_options = {
395
- http_client: http_client,
396
- bulk_limit: bulk_limit,
397
- bulk_max_mb: bulk_max_mb
398
- }
399
-
400
- SearchFlip::Bulk.new("#{base_url}/_bulk", default_options.merge(options)) do |indexer|
401
- yield indexer
402
- end
403
-
404
- refresh if SearchFlip::Config[:auto_refresh]
405
- end
406
-
407
345
  # Returns the full Elasticsearch type URL, ie base URL, index name with
408
346
  # prefix and type name.
409
347
  #
@@ -426,11 +364,5 @@ module SearchFlip
426
364
  def index_url(index_name)
427
365
  "#{base_url}/#{index_name}"
428
366
  end
429
-
430
- private
431
-
432
- def version_response
433
- @version_response ||= http_client.headers(accept: "application/json").get("#{base_url}/")
434
- end
435
367
  end
436
368
  end
@@ -26,8 +26,7 @@ module SearchFlip
26
26
 
27
27
  attr_accessor :target, :profile_value, :source_value, :suggest_values, :includes_values,
28
28
  :eager_load_values, :preload_values, :failsafe_value, :scroll_args, :terminate_after_value,
29
- :timeout_value, :preference_value, :search_type_value, :routing_value, :track_total_hits_value,
30
- :http_timeout_value
29
+ :timeout_value, :preference_value, :search_type_value, :routing_value, :track_total_hits_value
31
30
 
32
31
  # Creates a new criteria while merging the attributes (constraints,
33
32
  # settings, etc) of the current criteria with the attributes of another one
@@ -48,7 +47,7 @@ module SearchFlip
48
47
  [
49
48
  :profile_value, :failsafe_value, :terminate_after_value, :timeout_value, :offset_value,
50
49
  :limit_value, :scroll_args, :source_value, :preference_value, :search_type_value,
51
- :routing_value, :track_total_hits_value, :explain_value, :http_timeout_value
50
+ :routing_value, :track_total_hits_value, :explain_value
52
51
  ].each do |name|
53
52
  criteria.send(:"#{name}=", other.send(name)) unless other.send(name).nil?
54
53
  end
@@ -149,22 +148,6 @@ module SearchFlip
149
148
  end
150
149
  end
151
150
 
152
- # Specifies a http timeout, such that a SearchFlip::TimeoutError will be
153
- # thrown when the request times out.
154
- #
155
- # @example
156
- # ProductIndex.http_timeout(3).search("hello world")
157
- #
158
- # @param value [Fixnum] The timeout value
159
- #
160
- # @return [SearchFlip::Criteria] A newly created extended criteria
161
-
162
- def http_timeout(value)
163
- fresh.tap do |criteria|
164
- criteria.http_timeout_value = value
165
- end
166
- end
167
-
168
151
  # Specifies early query termination, such that the processing will be
169
152
  # stopped after the specified number of results has been accumulated.
170
153
  #
@@ -347,15 +330,10 @@ module SearchFlip
347
330
  dupped_request.delete(:from)
348
331
  dupped_request.delete(:size)
349
332
 
350
- http_request = connection.http_client
351
- http_request = http_request.timeout(http_timeout_value) if http_timeout_value
352
-
353
- if connection.distribution || connection.version.to_i >= 5
354
- url = connection.distribution.nil? && connection.version.to_i < 8 ? target.type_url : target.index_url
355
-
356
- http_request.post("#{url}/_delete_by_query", params: request_params.merge(params), json: dupped_request)
333
+ if connection.version.to_i >= 5
334
+ connection.http_client.post("#{target.type_url}/_delete_by_query", params: request_params.merge(params), json: dupped_request)
357
335
  else
358
- http_request.delete("#{target.type_url}/_query", params: request_params.merge(params), json: dupped_request)
336
+ connection.http_client.delete("#{target.type_url}/_query", params: request_params.merge(params), json: dupped_request)
359
337
  end
360
338
 
361
339
  target.refresh if SearchFlip::Config[:auto_refresh]
@@ -523,8 +501,8 @@ module SearchFlip
523
501
  end
524
502
 
525
503
  # Executes the search request for the current criteria, ie sends the
526
- # request to Elasticsearch and returns the response. Connection, timeout
527
- # and response errors will be rescued if you specify the criteria to be
504
+ # request to Elasticsearch and returns the response. Connection and
505
+ # response errors will be rescued if you specify the criteria to be
528
506
  # #failsafe, such that an empty response is returned instead.
529
507
  #
530
508
  # @example
@@ -612,7 +590,6 @@ module SearchFlip
612
590
 
613
591
  def execute!
614
592
  http_request = connection.http_client.headers(accept: "application/json")
615
- http_request = http_request.timeout(http_timeout_value) if http_timeout_value
616
593
 
617
594
  http_response =
618
595
  if scroll_args && scroll_args[:id]
@@ -622,21 +599,17 @@ module SearchFlip
622
599
  json: { scroll: scroll_args[:timeout], scroll_id: scroll_args[:id] }
623
600
  )
624
601
  elsif scroll_args
625
- url = connection.distribution.nil? && connection.version.to_i < 8 ? target.type_url : target.index_url
626
-
627
602
  http_request.post(
628
- "#{url}/_search",
603
+ "#{target.type_url}/_search",
629
604
  params: request_params.merge(scroll: scroll_args[:timeout]),
630
605
  json: request
631
606
  )
632
607
  else
633
- url = connection.distribution.nil? && connection.version.to_i < 8 ? target.type_url : target.index_url
634
-
635
- http_request.post("#{url}/_search", params: request_params, json: request)
608
+ http_request.post("#{target.type_url}/_search", params: request_params, json: request)
636
609
  end
637
610
 
638
611
  SearchFlip::Response.new(self, SearchFlip::JSON.parse(http_response.to_s))
639
- rescue SearchFlip::ConnectionError, SearchFlip::TimeoutError, SearchFlip::ResponseError => e
612
+ rescue SearchFlip::ConnectionError, SearchFlip::ResponseError => e
640
613
  raise e unless failsafe_value
641
614
 
642
615
  SearchFlip::Response.new(self, "took" => 0, "hits" => { "total" => 0, "hits" => [] })
@@ -235,22 +235,6 @@ module SearchFlip
235
235
  filter(match_all: options)
236
236
  end
237
237
 
238
- # Adds a match none filter to the criteria, which simply matches none
239
- # documents at all. Check out the Elasticsearch docs for further details.
240
- #
241
- # @example Basic usage
242
- # CommentIndex.match_none
243
- #
244
- # @example Filter chaining
245
- # query = CommentIndex.search("...")
246
- # query = query.match_none unless current_user.admin?
247
- #
248
- # @return [SearchFlip::Criteria] A newly created extended criteria
249
-
250
- def match_none
251
- filter(match_none: {})
252
- end
253
-
254
238
  # Adds an exists filter to the criteria, which selects all documents for
255
239
  # which the specified field has a non-null value.
256
240
  #
@@ -1,28 +1,7 @@
1
1
  module SearchFlip
2
- # The SearchFlip::HTTPClient class wraps the http gem and responsible for the
3
- # http request/response handling, ie communicating with Elasticsearch. You
4
- # only need to use it directly if you need authentication to communicate with
5
- # Elasticsearch or if you want to set some custom http settings.
6
- #
7
- # @example
8
- # http_client = SearchFlip::HTTPClient.new
9
- #
10
- # # Basic Auth
11
- # http_client = http_client.basic_auth(user: "username", pass: "password")
12
- #
13
- # # Raw Auth Header
14
- # http_client = http_client.auth("Bearer VGhlIEhUVFAgR2VtLCBST0NLUw")
15
- #
16
- # # Proxy Settings
17
- # http_client = http_client.via("proxy.host", 8080)
18
- #
19
- # # Custom headers
20
- # http_client = http_client.headers(key: "value")
21
- #
22
- # # Timeouts
23
- # http_client = http_client.timeout(20)
24
- #
25
- # SearchFlip::Connection.new(base_url: "...", http_client: http_client)
2
+ # The SearchFlip::HTTPClient class wraps the http gem, is for internal use
3
+ # and responsible for the http request/response handling, ie communicating
4
+ # with Elasticsearch.
26
5
 
27
6
  class HTTPClient
28
7
  attr_accessor :request, :plugins
@@ -35,11 +14,11 @@ module SearchFlip
35
14
  class << self
36
15
  extend Forwardable
37
16
 
38
- def_delegators :new, :headers, :via, :basic_auth, :auth, :timeout
17
+ def_delegators :new, :headers, :via, :basic_auth, :auth
39
18
  def_delegators :new, :get, :post, :put, :delete, :head
40
19
  end
41
20
 
42
- [:headers, :via, :basic_auth, :auth, :timeout].each do |method|
21
+ [:headers, :via, :basic_auth, :auth].each do |method|
43
22
  define_method method do |*args|
44
23
  dup.tap do |client|
45
24
  client.request = request.send(method, *args)
@@ -58,32 +37,14 @@ module SearchFlip
58
37
  private
59
38
 
60
39
  def execute(method, uri, options = {})
61
- opts = options.dup
62
- final_request = self
63
-
64
- if opts[:json]
65
- # Manually generate and pass the json body to http-rb to guarantee that
66
- # we have the same json which is used for aws signatures and to
67
- # guarantee that json is always generated as stated in the config
68
-
69
- opts[:body] = JSON.generate(opts.delete(:json))
70
- final_request = final_request.headers(content_type: "application/json")
71
- end
72
-
73
- final_request = plugins.inject(final_request) { |res, cur| cur.call(res, method, uri, opts) }
74
- final_request = final_request.headers({}) # Prevent thread-safety issue of http-rb: https://github.com/httprb/http/issues/558
75
-
76
- response = final_request.request.send(method, uri, opts)
40
+ final_request = plugins.inject(self) { |res, cur| cur.call(res, method, uri, options) }
41
+ response = final_request.request.send(method, uri, options)
77
42
 
78
43
  raise SearchFlip::ResponseError.new(code: response.code, body: response.body.to_s) unless response.status.success?
79
44
 
80
45
  response
81
46
  rescue HTTP::ConnectionError => e
82
47
  raise SearchFlip::ConnectionError, e.message
83
- rescue HTTP::TimeoutError => e
84
- raise SearchFlip::TimeoutError, e.message
85
- rescue HTTP::Error => e
86
- raise SearchFlip::HttpError, e.message
87
48
  end
88
49
  end
89
50
  end
@@ -153,7 +153,7 @@ module SearchFlip
153
153
  # scope to be applied to the scope
154
154
 
155
155
  def each_record(scope, index_scope: false)
156
- return enum_for(:each_record, scope, index_scope: index_scope) unless block_given?
156
+ return enum_for(:each_record, scope) unless block_given?
157
157
 
158
158
  if scope.respond_to?(:find_each)
159
159
  (index_scope ? self.index_scope(scope) : scope).find_each do |record|
@@ -247,14 +247,14 @@ module SearchFlip
247
247
  SearchFlip::Criteria.new(target: self)
248
248
  end
249
249
 
250
- def_delegators :criteria, :all, :profile, :where, :where_not, :filter, :range, :match_all, :match_none,
251
- :exists, :exists_not, :post_where, :post_where_not, :post_range, :post_exists, :post_exists_not,
250
+ def_delegators :criteria, :all, :profile, :where, :where_not, :filter, :range, :match_all, :exists,
251
+ :exists_not, :post_where, :post_where_not, :post_range, :post_exists, :post_exists_not,
252
252
  :post_filter, :post_must, :post_must_not, :post_should, :aggregate, :scroll, :source,
253
253
  :includes, :eager_load, :preload, :sort, :resort, :order, :reorder, :offset, :limit, :paginate,
254
254
  :page, :per, :search, :highlight, :suggest, :custom, :find_in_batches, :find_results_in_batches,
255
255
  :find_each, :find_each_result, :failsafe, :total_entries, :total_count, :timeout, :terminate_after,
256
256
  :records, :results, :must, :must_not, :should, :preference, :search_type, :routing,
257
- :track_total_hits, :explain, :http_timeout
257
+ :track_total_hits, :explain
258
258
 
259
259
  # Override to specify the type name used within Elasticsearch. Recap,
260
260
  # this gem uses an individual index for each index class, because
@@ -438,7 +438,7 @@ module SearchFlip
438
438
  # equal to _doc.
439
439
 
440
440
  def include_type_name?
441
- type_name != "_doc" || (connection.distribution.nil? && connection.version.to_i < 7)
441
+ type_name != "_doc" || connection.version.to_i < 7
442
442
  end
443
443
 
444
444
  # Retrieves the document specified by id from Elasticsearch. Raises
@@ -455,8 +455,7 @@ module SearchFlip
455
455
  # @return [Hash] The specified document
456
456
 
457
457
  def get(id, params = {})
458
- url = connection.distribution.nil? && connection.version.to_i < 8 ? type_url : "#{index_url}/_doc"
459
- response = connection.http_client.headers(accept: "application/json").get("#{url}/#{id}", params: params)
458
+ response = connection.http_client.headers(accept: "application/json").get("#{type_url}/#{id}", params: params)
460
459
 
461
460
  SearchFlip::JSON.parse(response.to_s)
462
461
  end
@@ -474,8 +473,7 @@ module SearchFlip
474
473
  # @return [Hash] The raw response
475
474
 
476
475
  def mget(request, params = {})
477
- url = connection.distribution.nil? && connection.version.to_i < 8 ? type_url : index_url
478
- response = connection.http_client.headers(accept: "application/json").post("#{url}/_mget", json: request, params: params)
476
+ response = connection.http_client.headers(accept: "application/json").post("#{type_url}/_mget", json: request, params: params)
479
477
 
480
478
  SearchFlip::JSON.parse(response.to_s)
481
479
  end
@@ -489,9 +487,7 @@ module SearchFlip
489
487
  # @return [Hash] The raw response
490
488
 
491
489
  def analyze(request, params = {})
492
- response = connection.http_client.headers(accept: "application/json").post("#{index_url}/_analyze", json: request, params: params)
493
-
494
- SearchFlip::JSON.parse(response.to_s)
490
+ connection.http_client.headers(accept: "application/json").post("#{index_url}/_analyze", json: request, params: params).parse
495
491
  end
496
492
 
497
493
  # Sends a index refresh request to Elasticsearch. Raises
@@ -601,7 +597,7 @@ module SearchFlip
601
597
  scope
602
598
  end
603
599
 
604
- # Initiates and yields a bulk object, such that index, import, create,
600
+ # Initiates and yields the bulk object, such that index, import, create,
605
601
  # update and delete requests can be appended to the bulk request. Sends a
606
602
  # refresh request afterwards if auto_refresh is enabled.
607
603
  #
@@ -633,9 +629,7 @@ module SearchFlip
633
629
  bulk_max_mb: connection.bulk_max_mb
634
630
  }
635
631
 
636
- url = connection.distribution.nil? && connection.version.to_i < 8 ? type_url : index_url
637
-
638
- SearchFlip::Bulk.new("#{url}/_bulk", default_options.merge(options)) do |indexer|
632
+ SearchFlip::Bulk.new("#{type_url}/_bulk", default_options.merge(options)) do |indexer|
639
633
  yield indexer
640
634
  end
641
635
 
@@ -1,11 +1,11 @@
1
1
  module SearchFlip
2
2
  class JSON
3
3
  def self.generate(obj)
4
- Oj.dump(obj, SearchFlip::Config[:json_options])
4
+ Oj.dump(obj, mode: :custom, use_to_json: true)
5
5
  end
6
6
 
7
- def self.parse(json)
8
- ::JSON.parse(json)
7
+ def self.parse(str)
8
+ Oj.load(str)
9
9
  end
10
10
  end
11
11
  end
@@ -156,8 +156,7 @@ module SearchFlip
156
156
  end
157
157
 
158
158
  # Returns the results, ie hits, wrapped in a SearchFlip::Result object
159
- # which basically is a Hashie::Mash. Check out the Hashie docs for further
160
- # details.
159
+ # which basically is a Hash with method-like access.
161
160
  #
162
161
  # @example
163
162
  # CommentIndex.search("hello world").results
@@ -166,7 +165,7 @@ module SearchFlip
166
165
  # @return [Array] An array of results
167
166
 
168
167
  def results
169
- @results ||= hits["hits"].map { |hit| Result.from_hit(hit) }
168
+ @results ||= hits["hits"].map { |hit| SearchFlip::Result.from_hit(hit) }
170
169
  end
171
170
 
172
171
  # Returns the named sugggetion, if a name is specified or alle suggestions.
@@ -224,7 +223,7 @@ module SearchFlip
224
223
 
225
224
  def records
226
225
  @records ||= begin
227
- sort_map = ids.each_with_index.with_object({}) { |(id, index), hash| hash[id.to_s] = index }
226
+ sort_map = ids.each_with_index.each_with_object({}) { |(id, index), hash| hash[id.to_s] = index }
228
227
 
229
228
  scope.to_a.sort_by { |record| sort_map[criteria.target.record_id(record).to_s] }
230
229
  end
@@ -304,13 +303,13 @@ module SearchFlip
304
303
 
305
304
  @aggregations[key] =
306
305
  if response["aggregations"].nil? || response["aggregations"][key].nil?
307
- Result.new
306
+ SearchFlip::Result.new
308
307
  elsif response["aggregations"][key]["buckets"].is_a?(Array)
309
- response["aggregations"][key]["buckets"].each_with_object({}) { |bucket, hash| hash[bucket["key"]] = Result.new(bucket) }
308
+ response["aggregations"][key]["buckets"].each_with_object({}) { |bucket, hash| hash[bucket["key"]] = SearchFlip::Result.convert(bucket) }
310
309
  elsif response["aggregations"][key]["buckets"].is_a?(Hash)
311
- Result.new response["aggregations"][key]["buckets"]
310
+ SearchFlip::Result.convert(response["aggregations"][key]["buckets"])
312
311
  else
313
- Result.new response["aggregations"][key]
312
+ SearchFlip::Result.convert(response["aggregations"][key])
314
313
  end
315
314
  end
316
315
  end
@@ -1,29 +1,55 @@
1
1
  module SearchFlip
2
- # The SearchFlip::Result class basically is a hash wrapper that uses
3
- # Hashie::Mash to provide convenient method access to the hash attributes.
2
+ # The SearchFlip::Result class is a simple Hash, but extended with
3
+ # method-like access. Keys assigned via methods are stored as strings.
4
+ #
5
+ # @example method access
6
+ # result = SearchFlip::Result.new
7
+ # result["some_key"] = "value"
8
+ # result.some_key # => "value"
4
9
 
5
- class Result < Hashie::Mash
6
- def self.disable_warnings?(*args)
7
- true
8
- end
10
+ class Result < Hash
11
+ def self.convert(hash)
12
+ res = self[hash]
9
13
 
10
- # Creates a SearchFlip::Result object from a raw hit. Useful for e.g.
11
- # top hits aggregations.
12
- #
13
- # @example
14
- # query = ProductIndex.aggregate(top_sales: { top_hits: "..." })
15
- # top_sales_hits = query.aggregations(:top_sales).top_hits.hits.hits
16
- #
17
- # SearchFlip::Result.from_hit(top_sales_hits.first)
14
+ res.each do |key, value|
15
+ if value.is_a?(Hash)
16
+ res[key] = convert(value)
17
+ elsif value.is_a?(Array)
18
+ res[key] = convert_array(value)
19
+ end
20
+ end
18
21
 
19
- def self.from_hit(hit)
20
- raw_result = (hit["_source"] || {}).dup
22
+ res
23
+ end
21
24
 
22
- raw_result["_hit"] = hit.each_with_object({}) do |(key, value), hash|
23
- hash[key] = value if key != "_source"
25
+ def self.convert_array(arr)
26
+ arr.map do |obj|
27
+ if obj.is_a?(Hash)
28
+ convert(obj)
29
+ elsif obj.is_a?(Array)
30
+ convert_array(obj)
31
+ else
32
+ obj
33
+ end
24
34
  end
35
+ end
36
+
37
+ # rubocop:disable Lint/MissingSuper
25
38
 
26
- new(raw_result)
39
+ def method_missing(name, *args, &block)
40
+ self[name.to_s]
41
+ end
42
+
43
+ # rubocop:enable Lint/MissingSuper
44
+
45
+ def respond_to_missing?(name, include_private = false)
46
+ key?(name.to_s) || super
47
+ end
48
+
49
+ def self.from_hit(hit)
50
+ res = convert(hit["_source"] || {})
51
+ res["_hit"] = convert(self[hit].tap { |hash| hash.delete("_source") })
52
+ res
27
53
  end
28
54
  end
29
55
  end