searchkick 4.0.0 → 4.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7e1a7c30f19f80bab77d1c44dc8778877881d8178ab52c2c3e48cb0fe1f31ac4
4
- data.tar.gz: 3091f32cea77c628e629e28dc865d700203f2265393ff44526e86b8ce899ae46
3
+ metadata.gz: bf57aedd7e85672dd9bc5f60f22719af2f5cb1797e1afa8f16c426ea291dd4db
4
+ data.tar.gz: 54542375eb2cb3c5b7917304cd7e68530a826641c8f1a556af551083964a8849
5
5
  SHA512:
6
- metadata.gz: 8d25cc6e9ac21a3f62199255aa5ab9911f2ab3fd970d79417a29063eeb944497da84e1db5ba840cc75eba549bee524c2a9b5b68d624dcadf0d4aa9bfcbc6f4fd
7
- data.tar.gz: 9fff1372bb24c8c8d2a59a1821d5ad299c9fcc1e2a73e53bbda2f3fec6b8aeddff0e00692451ec3ece0708762b9ccafe370b0ca7fc2d1e623a5bf12688a78ecd
6
+ metadata.gz: 5029c0fc16c139f4358786400aad6c36751c6335221e9985f66e5ac308152817db9cd41e709ff6f2b18e731842c85b1a17b9aaeb649b7895393e7001aad8426d
7
+ data.tar.gz: 63bd24be4b7b41c539c0acd0c0c8bc6502095707539d235f8ddc9fc49aa2634c2fb602e40ce77e771d151945118d2c4da73d26bcda8c4290d61eae4b6c44b468
data/CHANGELOG.md CHANGED
@@ -1,3 +1,10 @@
1
+ ## 4.0.1
2
+
3
+ - Added support for scroll API
4
+ - Made type optional for custom mapping for Elasticsearch 6
5
+ - Fixed error when suggestions empty
6
+ - Fixed `models` option with inheritance
7
+
1
8
  ## 4.0.0
2
9
 
3
10
  - Added support for Elasticsearch 7
data/README.md CHANGED
@@ -1044,10 +1044,10 @@ Visit the Shield page and reset your password. You’ll need to add the username
1044
1044
  heroku config:get FOUNDELASTICSEARCH_URL
1045
1045
  ```
1046
1046
 
1047
- And add `elastic:password@` right after `https://`:
1047
+ And add `elastic:password@` right after `https://` and add port `9243` at the end:
1048
1048
 
1049
1049
  ```sh
1050
- heroku config:set ELASTICSEARCH_URL=https://elastic:password@12345.us-east-1.aws.found.io
1050
+ heroku config:set ELASTICSEARCH_URL=https://elastic:password@12345.us-east-1.aws.found.io:9243
1051
1051
  ```
1052
1052
 
1053
1053
  Then deploy and reindex:
@@ -1396,10 +1396,8 @@ Create a custom mapping:
1396
1396
  ```ruby
1397
1397
  class Product < ApplicationRecord
1398
1398
  searchkick mappings: {
1399
- product: {
1400
- properties: {
1401
- name: {type: "keyword"}
1402
- }
1399
+ properties: {
1400
+ name: {type: "keyword"}
1403
1401
  }
1404
1402
  }
1405
1403
  end
@@ -1479,6 +1477,21 @@ Boost specific models with:
1479
1477
  indices_boost: {Category => 2, Product => 1}
1480
1478
  ```
1481
1479
 
1480
+ ## Scroll API
1481
+
1482
+ To retrieve a very large number of results, use the [scroll API](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-scroll.html).
1483
+
1484
+ ```ruby
1485
+ products = Product.search "*", scroll: "1m"
1486
+ while products.any?
1487
+ # do something ...
1488
+
1489
+ products = products.scroll
1490
+ end
1491
+ ```
1492
+
1493
+ You should call `scroll` on each new set of results, not the original result.
1494
+
1482
1495
  ## Nested Data
1483
1496
 
1484
1497
  To query nested data, use dot notation.
data/lib/searchkick.rb CHANGED
@@ -67,7 +67,7 @@ module Searchkick
67
67
  end
68
68
 
69
69
  def self.search_timeout
70
- @search_timeout || timeout
70
+ (defined?(@search_timeout) && @search_timeout) || timeout
71
71
  end
72
72
 
73
73
  def self.server_version
@@ -5,18 +5,29 @@ module Searchkick
5
5
  language = options[:language]
6
6
  language = language.call if language.respond_to?(:call)
7
7
 
8
+ below62 = Searchkick.server_below?("6.2.0")
9
+ below70 = Searchkick.server_below?("7.0.0")
10
+
11
+ if below70
12
+ index_type = options[:_type]
13
+ index_type = index_type.call if index_type.respond_to?(:call)
14
+ end
15
+
16
+ custom_mapping = options[:mapping] || {}
17
+ if below70 && custom_mapping.keys.map(&:to_sym).include?(:properties)
18
+ # add type
19
+ custom_mapping = {index_type => custom_mapping}
20
+ end
21
+
8
22
  if options[:mappings] && !options[:merge_mappings]
9
23
  settings = options[:settings] || {}
10
- mappings = options[:mappings]
24
+ mappings = custom_mapping
11
25
  else
12
- below62 = Searchkick.server_below?("6.2.0")
13
- below70 = Searchkick.server_below?("7.0.0")
14
26
 
15
27
  default_type = "text"
16
28
  default_analyzer = :searchkick_index
17
29
  keyword_mapping = {type: "keyword"}
18
30
 
19
- all = options.key?(:_all) ? options[:_all] : false
20
31
  index_true_value = true
21
32
  index_false_value = false
22
33
 
@@ -412,12 +423,10 @@ module Searchkick
412
423
  }
413
424
 
414
425
  if below70
415
- index_type = options[:_type]
416
- index_type = index_type.call if index_type.respond_to?(:call)
417
426
  mappings = {index_type => mappings}
418
427
  end
419
428
 
420
- mappings = mappings.symbolize_keys.deep_merge((options[:mappings] || {}).symbolize_keys)
429
+ mappings = mappings.symbolize_keys.deep_merge(custom_mapping.symbolize_keys)
421
430
  end
422
431
 
423
432
  {
@@ -167,7 +167,9 @@ module Searchkick
167
167
 
168
168
  # no easy way to tell which host the client will use
169
169
  host = Searchkick.client.transport.hosts.first
170
- debug " #{color(name, YELLOW, true)} curl #{host[:protocol]}://#{host[:host]}:#{host[:port]}/#{CGI.escape(index)}#{type ? "/#{type.map { |t| CGI.escape(t) }.join(',')}" : ''}/_search?pretty -H 'Content-Type: application/json' -d '#{payload[:query][:body].to_json}'"
170
+ params = ["pretty"]
171
+ params << "scroll=#{payload[:query][:scroll]}" if payload[:query][:scroll]
172
+ debug " #{color(name, YELLOW, true)} curl #{host[:protocol]}://#{host[:host]}:#{host[:port]}/#{CGI.escape(index)}#{type ? "/#{type.map { |t| CGI.escape(t) }.join(',')}" : ''}/_search?#{params.join('&')} -H 'Content-Type: application/json' -d '#{payload[:query][:body].to_json}'"
171
173
  end
172
174
 
173
175
  def request(event)
@@ -12,14 +12,14 @@ module Searchkick
12
12
  :took, :error, :model_name, :entry_name, :total_count, :total_entries,
13
13
  :current_page, :per_page, :limit_value, :padding, :total_pages, :num_pages,
14
14
  :offset_value, :offset, :previous_page, :prev_page, :next_page, :first_page?, :last_page?,
15
- :out_of_range?, :hits, :response, :to_a, :first
15
+ :out_of_range?, :hits, :response, :to_a, :first, :scroll
16
16
 
17
17
  def initialize(klass, term = "*", **options)
18
18
  unknown_keywords = options.keys - [:aggs, :block, :body, :body_options, :boost,
19
19
  :boost_by, :boost_by_distance, :boost_by_recency, :boost_where, :conversions, :conversions_term, :debug, :emoji, :exclude, :execute, :explain,
20
20
  :fields, :highlight, :includes, :index_name, :indices_boost, :limit, :load,
21
21
  :match, :misspellings, :models, :model_includes, :offset, :operator, :order, :padding, :page, :per_page, :profile,
22
- :request_params, :routing, :scope_results, :select, :similar, :smart_aggs, :suggest, :total_entries, :track, :type, :where]
22
+ :request_params, :routing, :scope_results, :scroll, :select, :similar, :smart_aggs, :suggest, :total_entries, :track, :type, :where]
23
23
  raise ArgumentError, "unknown keywords: #{unknown_keywords.join(", ")}" if unknown_keywords.any?
24
24
 
25
25
  term = term.to_s
@@ -60,7 +60,8 @@ module Searchkick
60
60
  if options[:models]
61
61
  @index_mapping = {}
62
62
  Array(options[:models]).each do |model|
63
- @index_mapping[model.searchkick_index.name] = model
63
+ # there can be multiple models per index name due to inheritance - see #1259
64
+ (@index_mapping[model.searchkick_index.name] ||= []) << model
64
65
  end
65
66
  end
66
67
 
@@ -81,6 +82,7 @@ module Searchkick
81
82
  }
82
83
  params[:type] = @type if @type
83
84
  params[:routing] = @routing if @routing
85
+ params[:scroll] = @scroll if @scroll
84
86
  params.merge!(options[:request_params]) if options[:request_params]
85
87
  params
86
88
  end
@@ -108,7 +110,9 @@ module Searchkick
108
110
  # no easy way to tell which host the client will use
109
111
  host = Searchkick.client.transport.hosts.first
110
112
  credentials = host[:user] || host[:password] ? "#{host[:user]}:#{host[:password]}@" : nil
111
- "curl #{host[:protocol]}://#{credentials}#{host[:host]}:#{host[:port]}/#{CGI.escape(index)}#{type ? "/#{type.map { |t| CGI.escape(t) }.join(',')}" : ''}/_search?pretty -H 'Content-Type: application/json' -d '#{query[:body].to_json}'"
113
+ params = ["pretty"]
114
+ params << "scroll=#{options[:scroll]}" if options[:scroll]
115
+ "curl #{host[:protocol]}://#{credentials}#{host[:host]}:#{host[:port]}/#{CGI.escape(index)}#{type ? "/#{type.map { |t| CGI.escape(t) }.join(',')}" : ''}/_search?#{params.join('&')} -H 'Content-Type: application/json' -d '#{query[:body].to_json}'"
112
116
  end
113
117
 
114
118
  def handle_response(response)
@@ -127,7 +131,9 @@ module Searchkick
127
131
  term: term,
128
132
  scope_results: options[:scope_results],
129
133
  total_entries: options[:total_entries],
130
- index_mapping: @index_mapping
134
+ index_mapping: @index_mapping,
135
+ suggest: options[:suggest],
136
+ scroll: options[:scroll]
131
137
  }
132
138
 
133
139
  if options[:debug]
@@ -228,6 +234,7 @@ module Searchkick
228
234
  per_page = (options[:limit] || options[:per_page] || 10_000).to_i
229
235
  padding = [options[:padding].to_i, 0].max
230
236
  offset = options[:offset] || (page - 1) * per_page + padding
237
+ scroll = options[:scroll]
231
238
 
232
239
  # model and eager loading
233
240
  load = options[:load].nil? ? true : options[:load]
@@ -423,6 +430,21 @@ module Searchkick
423
430
  where[:type] = [options[:type] || klass].flatten.map { |v| searchkick_index.klass_document_type(v, true) }
424
431
  end
425
432
 
433
+ models = Array(options[:models])
434
+ if models.any? { |m| m != m.searchkick_klass }
435
+ warn "[searchkick] WARNING: Passing child models to models option throws off hits and pagination - use type option instead"
436
+
437
+ # uncomment once aliases are supported with _index
438
+ # index_type_or =
439
+ # models.map do |m|
440
+ # v = {_index: m.searchkick_index.name}
441
+ # v[:type] = m.searchkick_index.klass_document_type(m, true) if m != m.searchkick_klass
442
+ # v
443
+ # end
444
+
445
+ # where[:or] = Array(where[:or]) + [index_type_or]
446
+ end
447
+
426
448
  # start everything as efficient filters
427
449
  # move to post_filters as aggs demand
428
450
  filters = where_filters(where)
@@ -480,7 +502,7 @@ module Searchkick
480
502
  pagination_options = options[:page] || options[:limit] || options[:per_page] || options[:offset] || options[:padding]
481
503
  if !options[:body] || pagination_options
482
504
  payload[:size] = per_page
483
- payload[:from] = offset
505
+ payload[:from] = offset if offset > 0
484
506
  end
485
507
 
486
508
  # type
@@ -497,11 +519,18 @@ module Searchkick
497
519
  # run block
498
520
  options[:block].call(payload) if options[:block]
499
521
 
522
+ # scroll optimization when interating over all docs
523
+ # https://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-scroll.html
524
+ if options[:scroll] && payload[:query] == {match_all: {}}
525
+ payload[:sort] ||= ["_doc"]
526
+ end
527
+
500
528
  @body = payload
501
529
  @page = page
502
530
  @per_page = per_page
503
531
  @padding = padding
504
532
  @load = load
533
+ @scroll = scroll
505
534
  end
506
535
 
507
536
  def set_fields
@@ -26,15 +26,19 @@ module Searchkick
26
26
  results = {}
27
27
 
28
28
  hits.group_by { |hit, _| hit["_index"] }.each do |index, grouped_hits|
29
- klass =
29
+ klasses =
30
30
  if @klass
31
- @klass
31
+ [@klass]
32
32
  else
33
33
  index_alias = index.split("_")[0..-2].join("_")
34
- (options[:index_mapping] || {})[index_alias]
34
+ Array((options[:index_mapping] || {})[index_alias])
35
35
  end
36
- raise Searchkick::Error, "Unknown model for index: #{index}" unless klass
37
- results[index] = results_query(klass, grouped_hits).to_a.index_by { |r| r.id.to_s }
36
+ raise Searchkick::Error, "Unknown model for index: #{index}" unless klasses.any?
37
+
38
+ results[index] = {}
39
+ klasses.each do |klass|
40
+ results[index].merge!(results_query(klass, grouped_hits).to_a.index_by { |r| r.id.to_s })
41
+ end
38
42
  end
39
43
 
40
44
  missing_ids = []
@@ -90,7 +94,7 @@ module Searchkick
90
94
  def suggestions
91
95
  if response["suggest"]
92
96
  response["suggest"].values.flat_map { |v| v.first["options"] }.sort_by { |o| -o["score"] }.map { |o| o["text"] }.uniq
93
- elsif options[:term] == "*"
97
+ elsif options[:suggest] || options[:term] == "*" # TODO remove 2nd term
94
98
  []
95
99
  else
96
100
  raise "Pass `suggest: true` to the search method for suggestions"
@@ -217,6 +221,29 @@ module Searchkick
217
221
  @options[:misspellings]
218
222
  end
219
223
 
224
+ def scroll_id
225
+ @response["_scroll_id"]
226
+ end
227
+
228
+ def scroll
229
+ raise Searchkick::Error, "Pass `scroll` option to the search method for scrolling" unless scroll_id
230
+
231
+ params = {
232
+ scroll: options[:scroll],
233
+ scroll_id: scroll_id
234
+ }
235
+
236
+ begin
237
+ Searchkick::Results.new(@klass, Searchkick.client.scroll(params), @options)
238
+ rescue Elasticsearch::Transport::Transport::Errors::NotFound => e
239
+ if e.class.to_s =~ /NotFound/ && e.message =~ /search_context_missing_exception/i
240
+ raise Searchkick::Error, "Scroll id has expired"
241
+ else
242
+ raise e
243
+ end
244
+ end
245
+ end
246
+
220
247
  private
221
248
 
222
249
  def results_query(records, hits)
@@ -1,3 +1,3 @@
1
1
  module Searchkick
2
- VERSION = "4.0.0"
2
+ VERSION = "4.0.1"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: searchkick
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.0.0
4
+ version: 4.0.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrew Kane
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-04-12 00:00:00.000000000 Z
11
+ date: 2019-05-30 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activemodel