searchkick 4.0.0 → 4.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.
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