searchkick 3.1.2 → 4.4.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,8 +1,10 @@
1
- require "active_model"
1
+ # dependencies
2
+ require "active_support"
2
3
  require "active_support/core_ext/hash/deep_merge"
3
4
  require "elasticsearch"
4
5
  require "hashie"
5
6
 
7
+ # modules
6
8
  require "searchkick/bulk_indexer"
7
9
  require "searchkick/index"
8
10
  require "searchkick/indexer"
@@ -17,29 +19,18 @@ require "searchkick/record_indexer"
17
19
  require "searchkick/results"
18
20
  require "searchkick/version"
19
21
 
22
+ # integrations
23
+ require "searchkick/railtie" if defined?(Rails)
20
24
  require "searchkick/logging" if defined?(ActiveSupport::Notifications)
21
25
 
22
- begin
23
- require "rake"
24
- rescue LoadError
25
- # do nothing
26
- end
27
- require "searchkick/tasks" if defined?(Rake)
28
-
29
- # background jobs
30
- begin
31
- require "active_job"
32
- rescue LoadError
33
- # do nothing
34
- end
35
- if defined?(ActiveJob)
36
- require "searchkick/bulk_reindex_job"
37
- require "searchkick/process_batch_job"
38
- require "searchkick/process_queue_job"
39
- require "searchkick/reindex_v2_job"
40
- end
41
-
42
26
  module Searchkick
27
+ # background jobs
28
+ autoload :BulkReindexJob, "searchkick/bulk_reindex_job"
29
+ autoload :ProcessBatchJob, "searchkick/process_batch_job"
30
+ autoload :ProcessQueueJob, "searchkick/process_queue_job"
31
+ autoload :ReindexV2Job, "searchkick/reindex_v2_job"
32
+
33
+ # errors
43
34
  class Error < StandardError; end
44
35
  class MissingIndexError < Error; end
45
36
  class UnsupportedVersionError < Error; end
@@ -62,7 +53,7 @@ module Searchkick
62
53
 
63
54
  def self.client
64
55
  @client ||= begin
65
- require "typhoeus/adapters/faraday" if defined?(Typhoeus)
56
+ require "typhoeus/adapters/faraday" if defined?(Typhoeus) && Gem::Version.new(Faraday::VERSION) < Gem::Version.new("0.14.0")
66
57
 
67
58
  Elasticsearch::Client.new({
68
59
  url: ENV["ELASTICSEARCH_URL"],
@@ -80,7 +71,7 @@ module Searchkick
80
71
  end
81
72
 
82
73
  def self.search_timeout
83
- @search_timeout || timeout
74
+ (defined?(@search_timeout) && @search_timeout) || timeout
84
75
  end
85
76
 
86
77
  def self.server_version
@@ -91,21 +82,41 @@ module Searchkick
91
82
  Gem::Version.new(server_version.split("-")[0]) < Gem::Version.new(version.split("-")[0])
92
83
  end
93
84
 
85
+ # memoize for performance
86
+ def self.server_below7?
87
+ unless defined?(@server_below7)
88
+ @server_below7 = server_below?("7.0.0")
89
+ end
90
+ @server_below7
91
+ end
92
+
94
93
  def self.search(term = "*", model: nil, **options, &block)
95
94
  options = options.dup
96
95
  klass = model
97
96
 
98
- # make Searchkick.search(index_name: [Product]) and Product.search equivalent
97
+ # convert index_name into models if possible
98
+ # this should allow for easier upgrade
99
+ if options[:index_name] && !options[:models] && Array(options[:index_name]).all? { |v| v.respond_to?(:searchkick_index) }
100
+ options[:models] = options.delete(:index_name)
101
+ end
102
+
103
+ # make Searchkick.search(models: [Product]) and Product.search equivalent
99
104
  unless klass
100
- index_name = Array(options[:index_name])
101
- if index_name.size == 1 && index_name.first.respond_to?(:searchkick_index)
102
- klass = index_name.first
103
- options.delete(:index_name)
105
+ models = Array(options[:models])
106
+ if models.size == 1
107
+ klass = models.first
108
+ options.delete(:models)
109
+ end
110
+ end
111
+
112
+ if klass
113
+ if (options[:models] && Array(options[:models]) != [klass]) || Array(options[:index_name]).any? { |v| v.respond_to?(:searchkick_index) && v != klass }
114
+ raise ArgumentError, "Use Searchkick.search to search multiple models"
104
115
  end
105
116
  end
106
117
 
107
- query = Searchkick::Query.new(klass, term, options)
108
- block.call(query.body) if block
118
+ options = options.merge(block: block) if block
119
+ query = Searchkick::Query.new(klass, term, **options)
109
120
  if options[:execute] == false
110
121
  query
111
122
  else
@@ -135,7 +146,7 @@ module Searchkick
135
146
  end
136
147
  end
137
148
 
138
- def self.callbacks(value)
149
+ def self.callbacks(value = nil)
139
150
  if block_given?
140
151
  previous_value = callbacks_value
141
152
  begin
@@ -153,6 +164,7 @@ module Searchkick
153
164
 
154
165
  def self.aws_credentials=(creds)
155
166
  begin
167
+ # TODO remove in Searchkick 5 (just use aws_sigv4)
156
168
  require "faraday_middleware/aws_signers_v4"
157
169
  rescue LoadError
158
170
  require "faraday_middleware/aws_sigv4"
@@ -162,17 +174,16 @@ module Searchkick
162
174
  end
163
175
 
164
176
  def self.reindex_status(index_name)
165
- if redis
166
- batches_left = Searchkick::Index.new(index_name).batches_left
167
- {
168
- completed: batches_left == 0,
169
- batches_left: batches_left
170
- }
171
- else
172
- raise Searchkick::Error, "Redis not configured"
173
- end
177
+ raise Searchkick::Error, "Redis not configured" unless redis
178
+
179
+ batches_left = Searchkick::Index.new(index_name).batches_left
180
+ {
181
+ completed: batches_left == 0,
182
+ batches_left: batches_left
183
+ }
174
184
  end
175
185
 
186
+ # TODO use ConnectionPool::Wrapper when redis is set so this is no longer needed
176
187
  def self.with_redis
177
188
  if redis
178
189
  if redis.respond_to?(:with)
@@ -185,6 +196,10 @@ module Searchkick
185
196
  end
186
197
  end
187
198
 
199
+ def self.warn(message)
200
+ super("[searchkick] WARNING: #{message}")
201
+ end
202
+
188
203
  # private
189
204
  def self.load_records(records, ids)
190
205
  records =
@@ -238,10 +253,26 @@ module Searchkick
238
253
  }
239
254
  end
240
255
  end
256
+
257
+ # private
258
+ # methods are forwarded to base class
259
+ # this check to see if scope exists on that class
260
+ # it's a bit tricky, but this seems to work
261
+ def self.relation?(klass)
262
+ if klass.respond_to?(:current_scope)
263
+ !klass.current_scope.nil?
264
+ elsif defined?(Mongoid::Threaded)
265
+ !Mongoid::Threaded.current_scope(klass).nil?
266
+ end
267
+ end
241
268
  end
242
269
 
243
- # TODO find better ActiveModel hook
270
+ require "active_model/callbacks"
244
271
  ActiveModel::Callbacks.include(Searchkick::Model)
272
+ # TODO use
273
+ # ActiveSupport.on_load(:mongoid) do
274
+ # Mongoid::Document::ClassMethods.include Searchkick::Model
275
+ # end
245
276
 
246
277
  ActiveSupport.on_load(:active_record) do
247
278
  extend Searchkick::Model
@@ -61,7 +61,7 @@ module Searchkick
61
61
  if records.any?
62
62
  if async
63
63
  Searchkick::BulkReindexJob.perform_later(
64
- class_name: records.first.class.name,
64
+ class_name: records.first.class.searchkick_options[:class_name],
65
65
  record_ids: records.map(&:id),
66
66
  index_name: index.name,
67
67
  method_name: method_name ? method_name.to_s : nil
@@ -87,6 +87,8 @@ module Searchkick
87
87
  # TODO expire Redis key
88
88
  primary_key = scope.primary_key
89
89
 
90
+ scope = scope.select(primary_key).except(:includes, :preload)
91
+
90
92
  starting_id =
91
93
  begin
92
94
  scope.minimum(primary_key)
@@ -139,8 +141,8 @@ module Searchkick
139
141
 
140
142
  def bulk_reindex_job(scope, batch_id, options)
141
143
  Searchkick.with_redis { |r| r.sadd(batches_key, batch_id) }
142
- Searchkick::BulkReindexJob.perform_later({
143
- class_name: scope.model_name.name,
144
+ Searchkick::BulkReindexJob.perform_later(**{
145
+ class_name: scope.searchkick_options[:class_name],
144
146
  index_name: index.name,
145
147
  batch_id: batch_id
146
148
  }.merge(options))
@@ -2,8 +2,6 @@ require "searchkick/index_options"
2
2
 
3
3
  module Searchkick
4
4
  class Index
5
- include IndexOptions
6
-
7
5
  attr_reader :name, :options
8
6
 
9
7
  def initialize(name, options = {})
@@ -12,12 +10,16 @@ module Searchkick
12
10
  @klass_document_type = {} # cache
13
11
  end
14
12
 
13
+ def index_options
14
+ IndexOptions.new(self).index_options
15
+ end
16
+
15
17
  def create(body = {})
16
18
  client.indices.create index: name, body: body
17
19
  end
18
20
 
19
21
  def delete
20
- if !Searchkick.server_below?("6.0.0") && alias_exists?
22
+ if alias_exists?
21
23
  # can't call delete directly on aliases in ES 6
22
24
  indices = client.indices.get_alias(name: name).keys
23
25
  client.indices.delete index: indices
@@ -47,7 +49,7 @@ module Searchkick
47
49
  end
48
50
 
49
51
  def refresh_interval
50
- settings.values.first["settings"]["index"]["refresh_interval"]
52
+ index_settings["refresh_interval"]
51
53
  end
52
54
 
53
55
  def update_settings(settings)
@@ -68,7 +70,7 @@ module Searchkick
68
70
  }
69
71
  )
70
72
 
71
- response["hits"]["total"]
73
+ Searchkick::Results.new(nil, response).total_count
72
74
  end
73
75
 
74
76
  def promote(new_name, update_refresh_interval: false)
@@ -91,17 +93,22 @@ module Searchkick
91
93
  alias_method :swap, :promote
92
94
 
93
95
  def retrieve(record)
94
- client.get(
95
- index: name,
96
- type: document_type(record),
97
- id: search_id(record)
98
- )["_source"]
96
+ record_data = RecordData.new(self, record).record_data
97
+
98
+ # remove underscore
99
+ get_options = Hash[record_data.map { |k, v| [k.to_s.sub(/\A_/, "").to_sym, v] }]
100
+
101
+ client.get(get_options)["_source"]
99
102
  end
100
103
 
101
104
  def all_indices(unaliased: false)
102
105
  indices =
103
106
  begin
104
- client.indices.get_aliases
107
+ if client.indices.respond_to?(:get_alias)
108
+ client.indices.get_alias
109
+ else
110
+ client.indices.get_aliases
111
+ end
105
112
  rescue Elasticsearch::Transport::Transport::Errors::NotFound
106
113
  {}
107
114
  end
@@ -169,6 +176,17 @@ module Searchkick
169
176
  Searchkick.search(like_text, model: record.class, **options)
170
177
  end
171
178
 
179
+ def reload_synonyms
180
+ require "elasticsearch/xpack"
181
+ raise Error, "Requires Elasticsearch 7.3+" if Searchkick.server_below?("7.3.0")
182
+ raise Error, "Requires elasticsearch-xpack 7.8+" unless client.xpack.respond_to?(:indices)
183
+ begin
184
+ client.xpack.indices.reload_search_analyzers(index: name)
185
+ rescue Elasticsearch::Transport::Transport::Errors::MethodNotAllowed
186
+ raise Error, "Requires non-OSS version of Elasticsearch"
187
+ end
188
+ end
189
+
172
190
  # queue
173
191
 
174
192
  def reindex_queue
@@ -179,13 +197,20 @@ module Searchkick
179
197
 
180
198
  def reindex(relation, method_name, scoped:, full: false, scope: nil, **options)
181
199
  refresh = options.fetch(:refresh, !scoped)
200
+ options.delete(:refresh)
182
201
 
183
202
  if method_name
203
+ # TODO throw ArgumentError
204
+ Searchkick.warn("unsupported keywords: #{options.keys.map(&:inspect).join(", ")}") if options.any?
205
+
184
206
  # update
185
207
  import_scope(relation, method_name: method_name, scope: scope)
186
208
  self.refresh if refresh
187
209
  true
188
210
  elsif scoped && !full
211
+ # TODO throw ArgumentError
212
+ Searchkick.warn("unsupported keywords: #{options.keys.map(&:inspect).join(", ")}") if options.any?
213
+
189
214
  # reindex association
190
215
  import_scope(relation, scope: scope)
191
216
  self.refresh if refresh
@@ -244,6 +269,11 @@ module Searchkick
244
269
  end
245
270
  end
246
271
 
272
+ # private
273
+ def uuid
274
+ index_settings["uuid"]
275
+ end
276
+
247
277
  protected
248
278
 
249
279
  def client
@@ -254,6 +284,14 @@ module Searchkick
254
284
  @bulk_indexer ||= BulkIndexer.new(self)
255
285
  end
256
286
 
287
+ def index_settings
288
+ settings.values.first["settings"]["index"]
289
+ end
290
+
291
+ def import_before_promotion(index, relation, **import_options)
292
+ index.import_scope(relation, **import_options)
293
+ end
294
+
257
295
  # https://gist.github.com/jarosan/3124884
258
296
  # http://www.elasticsearch.org/blog/changing-mapping-with-zero-downtime/
259
297
  def reindex_scope(relation, import: true, resume: false, retain: false, async: false, refresh_interval: nil, scope: nil)
@@ -276,14 +314,16 @@ module Searchkick
276
314
  scope: scope
277
315
  }
278
316
 
317
+ uuid = index.uuid
318
+
279
319
  # check if alias exists
280
320
  alias_exists = alias_exists?
281
321
  if alias_exists
282
- # import before promotion
283
- index.import_scope(relation, **import_options) if import
322
+ import_before_promotion(index, relation, **import_options) if import
284
323
 
285
324
  # get existing indices to remove
286
325
  unless async
326
+ check_uuid(uuid, index.uuid)
287
327
  promote(index.name, update_refresh_interval: !refresh_interval.nil?)
288
328
  clean_indices unless retain
289
329
  end
@@ -308,6 +348,7 @@ module Searchkick
308
348
  # already promoted if alias didn't exist
309
349
  if alias_exists
310
350
  puts "Jobs complete. Promoting..."
351
+ check_uuid(uuid, index.uuid)
311
352
  promote(index.name, update_refresh_interval: !refresh_interval.nil?)
312
353
  end
313
354
  clean_indices unless retain
@@ -326,5 +367,15 @@ module Searchkick
326
367
 
327
368
  raise e
328
369
  end
370
+
371
+ # safety check
372
+ # still a chance for race condition since its called before promotion
373
+ # ideal is for user to disable automatic index creation
374
+ # https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-index_.html#index-creation
375
+ def check_uuid(old_uuid, new_uuid)
376
+ if old_uuid != new_uuid
377
+ raise Searchkick::Error, "Safety check failed - only run one Model.reindex per model at a time"
378
+ end
379
+ end
329
380
  end
330
381
  end
@@ -1,426 +1,558 @@
1
1
  module Searchkick
2
- module IndexOptions
2
+ class IndexOptions
3
+ attr_reader :options
4
+
5
+ def initialize(index)
6
+ @options = index.options
7
+ end
8
+
3
9
  def index_options
4
- options = @options
5
- language = options[:language]
6
- language = language.call if language.respond_to?(:call)
7
- index_type = options[:_type]
8
- index_type = index_type.call if index_type.respond_to?(:call)
10
+ custom_mapping = options[:mappings] || {}
11
+ if below70? && custom_mapping.keys.map(&:to_sym).include?(:properties)
12
+ # add type
13
+ custom_mapping = {index_type => custom_mapping}
14
+ end
9
15
 
10
16
  if options[:mappings] && !options[:merge_mappings]
11
17
  settings = options[:settings] || {}
12
- mappings = options[:mappings]
18
+ mappings = custom_mapping
13
19
  else
14
- below60 = Searchkick.server_below?("6.0.0")
15
- below62 = Searchkick.server_below?("6.2.0")
16
-
17
- default_type = "text"
18
- default_analyzer = :searchkick_index
19
- keyword_mapping = {type: "keyword"}
20
-
21
- all = options.key?(:_all) ? options[:_all] : false
22
- index_true_value = true
23
- index_false_value = false
24
-
25
- keyword_mapping[:ignore_above] = options[:ignore_above] || 30000
26
-
27
- settings = {
28
- analysis: {
29
- analyzer: {
30
- searchkick_keyword: {
31
- type: "custom",
32
- tokenizer: "keyword",
33
- filter: ["lowercase"] + (options[:stem_conversions] ? ["searchkick_stemmer"] : [])
34
- },
35
- default_analyzer => {
36
- type: "custom",
37
- # character filters -> tokenizer -> token filters
38
- # https://www.elastic.co/guide/en/elasticsearch/guide/current/analysis-intro.html
39
- char_filter: ["ampersand"],
40
- tokenizer: "standard",
41
- # synonym should come last, after stemming and shingle
42
- # shingle must come before searchkick_stemmer
43
- filter: ["standard", "lowercase", "asciifolding", "searchkick_index_shingle", "searchkick_stemmer"]
44
- },
45
- searchkick_search: {
46
- type: "custom",
47
- char_filter: ["ampersand"],
48
- tokenizer: "standard",
49
- filter: ["standard", "lowercase", "asciifolding", "searchkick_search_shingle", "searchkick_stemmer"]
50
- },
51
- searchkick_search2: {
52
- type: "custom",
53
- char_filter: ["ampersand"],
54
- tokenizer: "standard",
55
- filter: ["standard", "lowercase", "asciifolding", "searchkick_stemmer"]
56
- },
57
- # https://github.com/leschenko/elasticsearch_autocomplete/blob/master/lib/elasticsearch_autocomplete/analyzers.rb
58
- searchkick_autocomplete_search: {
59
- type: "custom",
60
- tokenizer: "keyword",
61
- filter: ["lowercase", "asciifolding"]
62
- },
63
- searchkick_word_search: {
64
- type: "custom",
65
- tokenizer: "standard",
66
- filter: ["lowercase", "asciifolding"]
67
- },
68
- searchkick_suggest_index: {
69
- type: "custom",
70
- tokenizer: "standard",
71
- filter: ["lowercase", "asciifolding", "searchkick_suggest_shingle"]
72
- },
73
- searchkick_text_start_index: {
74
- type: "custom",
75
- tokenizer: "keyword",
76
- filter: ["lowercase", "asciifolding", "searchkick_edge_ngram"]
77
- },
78
- searchkick_text_middle_index: {
79
- type: "custom",
80
- tokenizer: "keyword",
81
- filter: ["lowercase", "asciifolding", "searchkick_ngram"]
82
- },
83
- searchkick_text_end_index: {
84
- type: "custom",
85
- tokenizer: "keyword",
86
- filter: ["lowercase", "asciifolding", "reverse", "searchkick_edge_ngram", "reverse"]
87
- },
88
- searchkick_word_start_index: {
89
- type: "custom",
90
- tokenizer: "standard",
91
- filter: ["lowercase", "asciifolding", "searchkick_edge_ngram"]
92
- },
93
- searchkick_word_middle_index: {
94
- type: "custom",
95
- tokenizer: "standard",
96
- filter: ["lowercase", "asciifolding", "searchkick_ngram"]
97
- },
98
- searchkick_word_end_index: {
99
- type: "custom",
100
- tokenizer: "standard",
101
- filter: ["lowercase", "asciifolding", "reverse", "searchkick_edge_ngram", "reverse"]
102
- }
103
- },
104
- filter: {
105
- searchkick_index_shingle: {
106
- type: "shingle",
107
- token_separator: ""
108
- },
109
- # lucky find http://web.archiveorange.com/archive/v/AAfXfQ17f57FcRINsof7
110
- searchkick_search_shingle: {
111
- type: "shingle",
112
- token_separator: "",
113
- output_unigrams: false,
114
- output_unigrams_if_no_shingles: true
115
- },
116
- searchkick_suggest_shingle: {
117
- type: "shingle",
118
- max_shingle_size: 5
119
- },
120
- searchkick_edge_ngram: {
121
- type: "edgeNGram",
122
- min_gram: 1,
123
- max_gram: 50
124
- },
125
- searchkick_ngram: {
126
- type: "nGram",
127
- min_gram: 1,
128
- max_gram: 50
129
- },
130
- searchkick_stemmer: {
131
- # use stemmer if language is lowercase, snowball otherwise
132
- type: language == language.to_s.downcase ? "stemmer" : "snowball",
133
- language: language || "English"
134
- }
135
- },
136
- char_filter: {
137
- # https://www.elastic.co/guide/en/elasticsearch/guide/current/custom-analyzers.html
138
- # &_to_and
139
- ampersand: {
140
- type: "mapping",
141
- mappings: ["&=> and "]
142
- }
143
- }
144
- }
145
- }
20
+ settings = generate_settings
21
+ mappings = generate_mappings.symbolize_keys.deep_merge(custom_mapping.symbolize_keys)
22
+ end
23
+
24
+ set_deep_paging(settings) if options[:deep_paging]
25
+
26
+ {
27
+ settings: settings,
28
+ mappings: mappings
29
+ }
30
+ end
146
31
 
147
- case language
148
- when "chinese"
149
- settings[:analysis][:analyzer].merge!(
32
+ def generate_settings
33
+ language = options[:language]
34
+ language = language.call if language.respond_to?(:call)
35
+
36
+ settings = {
37
+ analysis: {
38
+ analyzer: {
39
+ searchkick_keyword: {
40
+ type: "custom",
41
+ tokenizer: "keyword",
42
+ filter: ["lowercase"] + (options[:stem_conversions] ? ["searchkick_stemmer"] : [])
43
+ },
150
44
  default_analyzer => {
151
- type: "ik_smart"
45
+ type: "custom",
46
+ # character filters -> tokenizer -> token filters
47
+ # https://www.elastic.co/guide/en/elasticsearch/guide/current/analysis-intro.html
48
+ char_filter: ["ampersand"],
49
+ tokenizer: "standard",
50
+ # synonym should come last, after stemming and shingle
51
+ # shingle must come before searchkick_stemmer
52
+ filter: ["lowercase", "asciifolding", "searchkick_index_shingle", "searchkick_stemmer"]
152
53
  },
153
54
  searchkick_search: {
154
- type: "ik_smart"
55
+ type: "custom",
56
+ char_filter: ["ampersand"],
57
+ tokenizer: "standard",
58
+ filter: ["lowercase", "asciifolding", "searchkick_search_shingle", "searchkick_stemmer"]
155
59
  },
156
60
  searchkick_search2: {
157
- type: "ik_max_word"
158
- }
159
- )
160
-
161
- settings[:analysis][:filter].delete(:searchkick_stemmer)
162
- when "japanese"
163
- settings[:analysis][:analyzer].merge!(
164
- default_analyzer => {
165
- type: "kuromoji"
61
+ type: "custom",
62
+ char_filter: ["ampersand"],
63
+ tokenizer: "standard",
64
+ filter: ["lowercase", "asciifolding", "searchkick_stemmer"]
166
65
  },
167
- searchkick_search: {
168
- type: "kuromoji"
66
+ # https://github.com/leschenko/elasticsearch_autocomplete/blob/master/lib/elasticsearch_autocomplete/analyzers.rb
67
+ searchkick_autocomplete_search: {
68
+ type: "custom",
69
+ tokenizer: "keyword",
70
+ filter: ["lowercase", "asciifolding"]
169
71
  },
170
- searchkick_search2: {
171
- type: "kuromoji"
172
- }
173
- )
174
- when "korean"
175
- settings[:analysis][:analyzer].merge!(
176
- default_analyzer => {
177
- type: "openkoreantext-analyzer"
72
+ searchkick_word_search: {
73
+ type: "custom",
74
+ tokenizer: "standard",
75
+ filter: ["lowercase", "asciifolding"]
178
76
  },
179
- searchkick_search: {
180
- type: "openkoreantext-analyzer"
77
+ searchkick_suggest_index: {
78
+ type: "custom",
79
+ tokenizer: "standard",
80
+ filter: ["lowercase", "asciifolding", "searchkick_suggest_shingle"]
181
81
  },
182
- searchkick_search2: {
183
- type: "openkoreantext-analyzer"
184
- }
185
- )
186
- when "vietnamese"
187
- settings[:analysis][:analyzer].merge!(
188
- default_analyzer => {
189
- type: "vi_analyzer"
82
+ searchkick_text_start_index: {
83
+ type: "custom",
84
+ tokenizer: "keyword",
85
+ filter: ["lowercase", "asciifolding", "searchkick_edge_ngram"]
190
86
  },
191
- searchkick_search: {
192
- type: "vi_analyzer"
87
+ searchkick_text_middle_index: {
88
+ type: "custom",
89
+ tokenizer: "keyword",
90
+ filter: ["lowercase", "asciifolding", "searchkick_ngram"]
193
91
  },
194
- searchkick_search2: {
195
- type: "vi_analyzer"
92
+ searchkick_text_end_index: {
93
+ type: "custom",
94
+ tokenizer: "keyword",
95
+ filter: ["lowercase", "asciifolding", "reverse", "searchkick_edge_ngram", "reverse"]
96
+ },
97
+ searchkick_word_start_index: {
98
+ type: "custom",
99
+ tokenizer: "standard",
100
+ filter: ["lowercase", "asciifolding", "searchkick_edge_ngram"]
101
+ },
102
+ searchkick_word_middle_index: {
103
+ type: "custom",
104
+ tokenizer: "standard",
105
+ filter: ["lowercase", "asciifolding", "searchkick_ngram"]
106
+ },
107
+ searchkick_word_end_index: {
108
+ type: "custom",
109
+ tokenizer: "standard",
110
+ filter: ["lowercase", "asciifolding", "reverse", "searchkick_edge_ngram", "reverse"]
196
111
  }
197
- )
198
- when "polish", "ukrainian", "smartcn"
199
- settings[:analysis][:analyzer].merge!(
200
- default_analyzer => {
201
- type: language
112
+ },
113
+ filter: {
114
+ searchkick_index_shingle: {
115
+ type: "shingle",
116
+ token_separator: ""
202
117
  },
203
- searchkick_search: {
204
- type: language
118
+ # lucky find https://web.archiveorange.com/archive/v/AAfXfQ17f57FcRINsof7
119
+ searchkick_search_shingle: {
120
+ type: "shingle",
121
+ token_separator: "",
122
+ output_unigrams: false,
123
+ output_unigrams_if_no_shingles: true
205
124
  },
206
- searchkick_search2: {
207
- type: language
125
+ searchkick_suggest_shingle: {
126
+ type: "shingle",
127
+ max_shingle_size: 5
128
+ },
129
+ searchkick_edge_ngram: {
130
+ type: "edge_ngram",
131
+ min_gram: 1,
132
+ max_gram: 50
133
+ },
134
+ searchkick_ngram: {
135
+ type: "ngram",
136
+ min_gram: 1,
137
+ max_gram: 50
138
+ },
139
+ searchkick_stemmer: {
140
+ # use stemmer if language is lowercase, snowball otherwise
141
+ type: language == language.to_s.downcase ? "stemmer" : "snowball",
142
+ language: language || "English"
208
143
  }
209
- )
210
- end
144
+ },
145
+ char_filter: {
146
+ # https://www.elastic.co/guide/en/elasticsearch/guide/current/custom-analyzers.html
147
+ # &_to_and
148
+ ampersand: {
149
+ type: "mapping",
150
+ mappings: ["&=> and "]
151
+ }
152
+ }
153
+ }
154
+ }
211
155
 
212
- if Searchkick.env == "test"
213
- settings[:number_of_shards] = 1
214
- settings[:number_of_replicas] = 0
215
- end
156
+ update_language(settings, language)
157
+ update_stemming(settings)
216
158
 
217
- if options[:similarity]
218
- settings[:similarity] = {default: {type: options[:similarity]}}
219
- end
159
+ if Searchkick.env == "test"
160
+ settings[:number_of_shards] = 1
161
+ settings[:number_of_replicas] = 0
162
+ end
220
163
 
221
- unless below62
222
- settings[:index] = {
223
- max_ngram_diff: 49,
224
- max_shingle_diff: 4
225
- }
226
- end
164
+ # TODO remove in Searchkick 5 (classic no longer supported)
165
+ if options[:similarity]
166
+ settings[:similarity] = {default: {type: options[:similarity]}}
167
+ end
227
168
 
228
- if options[:case_sensitive]
229
- settings[:analysis][:analyzer].each do |_, analyzer|
230
- analyzer[:filter].delete("lowercase")
231
- end
232
- end
169
+ unless below62?
170
+ settings[:index] = {
171
+ max_ngram_diff: 49,
172
+ max_shingle_diff: 4
173
+ }
174
+ end
233
175
 
234
- if options[:stem] == false
235
- settings[:analysis][:analyzer].each do |_, analyzer|
236
- analyzer[:filter].delete("searchkick_stemmer")
237
- end
176
+ if options[:case_sensitive]
177
+ settings[:analysis][:analyzer].each do |_, analyzer|
178
+ analyzer[:filter].delete("lowercase")
238
179
  end
180
+ end
239
181
 
240
- settings = settings.symbolize_keys.deep_merge((options[:settings] || {}).symbolize_keys)
241
-
242
- # synonyms
243
- synonyms = options[:synonyms] || []
182
+ # TODO do this last in Searchkick 5
183
+ settings = settings.symbolize_keys.deep_merge((options[:settings] || {}).symbolize_keys)
244
184
 
245
- synonyms = synonyms.call if synonyms.respond_to?(:call)
185
+ add_synonyms(settings)
186
+ add_search_synonyms(settings)
187
+ # TODO remove in Searchkick 5
188
+ add_wordnet(settings) if options[:wordnet]
246
189
 
247
- if synonyms.any?
248
- settings[:analysis][:filter][:searchkick_synonym] = {
249
- type: "synonym",
250
- # only remove a single space from synonyms so three-word synonyms will fail noisily instead of silently
251
- synonyms: synonyms.select { |s| s.size > 1 }.map { |s| s.is_a?(Array) ? s.map { |s2| s2.sub(/\s+/, "") }.join(",") : s }.map(&:downcase)
252
- }
253
- # choosing a place for the synonym filter when stemming is not easy
254
- # https://groups.google.com/forum/#!topic/elasticsearch/p7qcQlgHdB8
255
- # TODO use a snowball stemmer on synonyms when creating the token filter
256
-
257
- # http://elasticsearch-users.115913.n3.nabble.com/synonym-multi-words-search-td4030811.html
258
- # I find the following approach effective if you are doing multi-word synonyms (synonym phrases):
259
- # - Only apply the synonym expansion at index time
260
- # - Don't have the synonym filter applied search
261
- # - Use directional synonyms where appropriate. You want to make sure that you're not injecting terms that are too general.
262
- settings[:analysis][:analyzer][default_analyzer][:filter].insert(4, "searchkick_synonym") if below60
263
- settings[:analysis][:analyzer][default_analyzer][:filter] << "searchkick_synonym"
264
-
265
- %w(word_start word_middle word_end).each do |type|
266
- settings[:analysis][:analyzer]["searchkick_#{type}_index".to_sym][:filter].insert(2, "searchkick_synonym")
267
- end
190
+ if options[:special_characters] == false
191
+ settings[:analysis][:analyzer].each_value do |analyzer_settings|
192
+ analyzer_settings[:filter].reject! { |f| f == "asciifolding" }
268
193
  end
194
+ end
269
195
 
270
- if options[:wordnet]
271
- settings[:analysis][:filter][:searchkick_wordnet] = {
272
- type: "synonym",
273
- format: "wordnet",
274
- synonyms_path: Searchkick.wordnet_path
196
+ settings
197
+ end
198
+
199
+ def update_language(settings, language)
200
+ case language
201
+ when "chinese"
202
+ settings[:analysis][:analyzer].merge!(
203
+ default_analyzer => {
204
+ type: "ik_smart"
205
+ },
206
+ searchkick_search: {
207
+ type: "ik_smart"
208
+ },
209
+ searchkick_search2: {
210
+ type: "ik_max_word"
211
+ }
212
+ )
213
+ when "chinese2", "smartcn"
214
+ settings[:analysis][:analyzer].merge!(
215
+ default_analyzer => {
216
+ type: "smartcn"
217
+ },
218
+ searchkick_search: {
219
+ type: "smartcn"
220
+ },
221
+ searchkick_search2: {
222
+ type: "smartcn"
223
+ }
224
+ )
225
+ when "japanese"
226
+ settings[:analysis][:analyzer].merge!(
227
+ default_analyzer => {
228
+ type: "kuromoji"
229
+ },
230
+ searchkick_search: {
231
+ type: "kuromoji"
232
+ },
233
+ searchkick_search2: {
234
+ type: "kuromoji"
235
+ }
236
+ )
237
+ when "korean"
238
+ settings[:analysis][:analyzer].merge!(
239
+ default_analyzer => {
240
+ type: "openkoreantext-analyzer"
241
+ },
242
+ searchkick_search: {
243
+ type: "openkoreantext-analyzer"
244
+ },
245
+ searchkick_search2: {
246
+ type: "openkoreantext-analyzer"
275
247
  }
248
+ )
249
+ when "korean2"
250
+ settings[:analysis][:analyzer].merge!(
251
+ default_analyzer => {
252
+ type: "nori"
253
+ },
254
+ searchkick_search: {
255
+ type: "nori"
256
+ },
257
+ searchkick_search2: {
258
+ type: "nori"
259
+ }
260
+ )
261
+ when "vietnamese"
262
+ settings[:analysis][:analyzer].merge!(
263
+ default_analyzer => {
264
+ type: "vi_analyzer"
265
+ },
266
+ searchkick_search: {
267
+ type: "vi_analyzer"
268
+ },
269
+ searchkick_search2: {
270
+ type: "vi_analyzer"
271
+ }
272
+ )
273
+ when "polish", "ukrainian"
274
+ settings[:analysis][:analyzer].merge!(
275
+ default_analyzer => {
276
+ type: language
277
+ },
278
+ searchkick_search: {
279
+ type: language
280
+ },
281
+ searchkick_search2: {
282
+ type: language
283
+ }
284
+ )
285
+ end
286
+ end
276
287
 
277
- settings[:analysis][:analyzer][default_analyzer][:filter].insert(4, "searchkick_wordnet")
278
- settings[:analysis][:analyzer][default_analyzer][:filter] << "searchkick_wordnet"
288
+ def update_stemming(settings)
289
+ stem = options[:stem]
279
290
 
280
- %w(word_start word_middle word_end).each do |type|
281
- settings[:analysis][:analyzer]["searchkick_#{type}_index".to_sym][:filter].insert(2, "searchkick_wordnet")
282
- end
283
- end
291
+ # language analyzer used
292
+ stem = false if settings[:analysis][:analyzer][default_analyzer][:type] != "custom"
284
293
 
285
- if options[:special_characters] == false
286
- settings[:analysis][:analyzer].each_value do |analyzer_settings|
287
- analyzer_settings[:filter].reject! { |f| f == "asciifolding" }
288
- end
294
+ if stem == false
295
+ settings[:analysis][:filter].delete(:searchkick_stemmer)
296
+ settings[:analysis][:analyzer].each do |_, analyzer|
297
+ analyzer[:filter].delete("searchkick_stemmer") if analyzer[:filter]
289
298
  end
299
+ end
290
300
 
291
- mapping = {}
301
+ if options[:stemmer_override]
302
+ stemmer_override = {
303
+ type: "stemmer_override"
304
+ }
305
+ if options[:stemmer_override].is_a?(String)
306
+ stemmer_override[:rules_path] = options[:stemmer_override]
307
+ else
308
+ stemmer_override[:rules] = options[:stemmer_override]
309
+ end
310
+ settings[:analysis][:filter][:searchkick_stemmer_override] = stemmer_override
292
311
 
293
- # conversions
294
- Array(options[:conversions]).each do |conversions_field|
295
- mapping[conversions_field] = {
296
- type: "nested",
297
- properties: {
298
- query: {type: default_type, analyzer: "searchkick_keyword"},
299
- count: {type: "integer"}
300
- }
301
- }
312
+ settings[:analysis][:analyzer].each do |_, analyzer|
313
+ stemmer_index = analyzer[:filter].index("searchkick_stemmer") if analyzer[:filter]
314
+ analyzer[:filter].insert(stemmer_index, "searchkick_stemmer_override") if stemmer_index
302
315
  end
316
+ end
303
317
 
304
- mapping_options = Hash[
305
- [:suggest, :word, :text_start, :text_middle, :text_end, :word_start, :word_middle, :word_end, :highlight, :searchable, :filterable]
306
- .map { |type| [type, (options[type] || []).map(&:to_s)] }
307
- ]
318
+ if options[:stem_exclusion]
319
+ settings[:analysis][:filter][:searchkick_stem_exclusion] = {
320
+ type: "keyword_marker",
321
+ keywords: options[:stem_exclusion]
322
+ }
308
323
 
309
- word = options[:word] != false && (!options[:match] || options[:match] == :word)
324
+ settings[:analysis][:analyzer].each do |_, analyzer|
325
+ stemmer_index = analyzer[:filter].index("searchkick_stemmer") if analyzer[:filter]
326
+ analyzer[:filter].insert(stemmer_index, "searchkick_stem_exclusion") if stemmer_index
327
+ end
328
+ end
329
+ end
310
330
 
311
- mapping_options[:searchable].delete("_all")
331
+ def generate_mappings
332
+ mapping = {}
312
333
 
313
- analyzed_field_options = {type: default_type, index: index_true_value, analyzer: default_analyzer}
334
+ keyword_mapping = {type: "keyword"}
335
+ keyword_mapping[:ignore_above] = options[:ignore_above] || 30000
314
336
 
315
- mapping_options.values.flatten.uniq.each do |field|
316
- fields = {}
337
+ # conversions
338
+ Array(options[:conversions]).each do |conversions_field|
339
+ mapping[conversions_field] = {
340
+ type: "nested",
341
+ properties: {
342
+ query: {type: default_type, analyzer: "searchkick_keyword"},
343
+ count: {type: "integer"}
344
+ }
345
+ }
346
+ end
317
347
 
318
- if options.key?(:filterable) && !mapping_options[:filterable].include?(field)
319
- fields[field] = {type: default_type, index: index_false_value}
320
- else
321
- fields[field] = keyword_mapping
322
- end
348
+ mapping_options = Hash[
349
+ [:suggest, :word, :text_start, :text_middle, :text_end, :word_start, :word_middle, :word_end, :highlight, :searchable, :filterable]
350
+ .map { |type| [type, (options[type] || []).map(&:to_s)] }
351
+ ]
323
352
 
324
- if !options[:searchable] || mapping_options[:searchable].include?(field)
325
- if word
326
- fields[:analyzed] = analyzed_field_options
353
+ word = options[:word] != false && (!options[:match] || options[:match] == :word)
327
354
 
328
- if mapping_options[:highlight].include?(field)
329
- fields[:analyzed][:term_vector] = "with_positions_offsets"
330
- end
331
- end
355
+ mapping_options[:searchable].delete("_all")
332
356
 
333
- mapping_options.except(:highlight, :searchable, :filterable, :word).each do |type, f|
334
- if options[:match] == type || f.include?(field)
335
- fields[type] = {type: default_type, index: index_true_value, analyzer: "searchkick_#{type}_index"}
336
- end
337
- end
338
- end
357
+ analyzed_field_options = {type: default_type, index: true, analyzer: default_analyzer}
339
358
 
340
- mapping[field] = fields[field].merge(fields: fields.except(field))
341
- end
359
+ mapping_options.values.flatten.uniq.each do |field|
360
+ fields = {}
342
361
 
343
- (options[:locations] || []).map(&:to_s).each do |field|
344
- mapping[field] = {
345
- type: "geo_point"
346
- }
362
+ if options.key?(:filterable) && !mapping_options[:filterable].include?(field)
363
+ fields[field] = {type: default_type, index: false}
364
+ else
365
+ fields[field] = keyword_mapping
347
366
  end
348
367
 
349
- options[:geo_shape] = options[:geo_shape].product([{}]).to_h if options[:geo_shape].is_a?(Array)
350
- (options[:geo_shape] || {}).each do |field, shape_options|
351
- mapping[field] = shape_options.merge(type: "geo_shape")
352
- end
368
+ if !options[:searchable] || mapping_options[:searchable].include?(field)
369
+ if word
370
+ fields[:analyzed] = analyzed_field_options
353
371
 
354
- if options[:inheritance]
355
- mapping[:type] = keyword_mapping
356
- end
372
+ if mapping_options[:highlight].include?(field)
373
+ fields[:analyzed][:term_vector] = "with_positions_offsets"
374
+ end
375
+ end
357
376
 
358
- routing = {}
359
- if options[:routing]
360
- routing = {required: true}
361
- unless options[:routing] == true
362
- routing[:path] = options[:routing].to_s
377
+ mapping_options.except(:highlight, :searchable, :filterable, :word).each do |type, f|
378
+ if options[:match] == type || f.include?(field)
379
+ fields[type] = {type: default_type, index: true, analyzer: "searchkick_#{type}_index"}
380
+ end
363
381
  end
364
382
  end
365
383
 
366
- dynamic_fields = {
367
- # analyzed field must be the default field for include_in_all
368
- # http://www.elasticsearch.org/guide/reference/mapping/multi-field-type/
369
- # however, we can include the not_analyzed field in _all
370
- # and the _all index analyzer will take care of it
371
- "{name}" => keyword_mapping
384
+ mapping[field] = fields[field].merge(fields: fields.except(field))
385
+ end
386
+
387
+ (options[:locations] || []).map(&:to_s).each do |field|
388
+ mapping[field] = {
389
+ type: "geo_point"
372
390
  }
391
+ end
373
392
 
374
- if below60 && all
375
- dynamic_fields["{name}"][:include_in_all] = !options[:searchable]
376
- end
393
+ options[:geo_shape] = options[:geo_shape].product([{}]).to_h if options[:geo_shape].is_a?(Array)
394
+ (options[:geo_shape] || {}).each do |field, shape_options|
395
+ mapping[field] = shape_options.merge(type: "geo_shape")
396
+ end
397
+
398
+ if options[:inheritance]
399
+ mapping[:type] = keyword_mapping
400
+ end
377
401
 
378
- if options.key?(:filterable)
379
- dynamic_fields["{name}"] = {type: default_type, index: index_false_value}
402
+ routing = {}
403
+ if options[:routing]
404
+ routing = {required: true}
405
+ unless options[:routing] == true
406
+ routing[:path] = options[:routing].to_s
380
407
  end
408
+ end
381
409
 
382
- unless options[:searchable]
383
- if options[:match] && options[:match] != :word
384
- dynamic_fields[options[:match]] = {type: default_type, index: index_true_value, analyzer: "searchkick_#{options[:match]}_index"}
385
- end
410
+ dynamic_fields = {
411
+ # analyzed field must be the default field for include_in_all
412
+ # http://www.elasticsearch.org/guide/reference/mapping/multi-field-type/
413
+ # however, we can include the not_analyzed field in _all
414
+ # and the _all index analyzer will take care of it
415
+ "{name}" => keyword_mapping
416
+ }
386
417
 
387
- if word
388
- dynamic_fields[:analyzed] = analyzed_field_options
389
- end
418
+ if options.key?(:filterable)
419
+ dynamic_fields["{name}"] = {type: default_type, index: false}
420
+ end
421
+
422
+ unless options[:searchable]
423
+ if options[:match] && options[:match] != :word
424
+ dynamic_fields[options[:match]] = {type: default_type, index: true, analyzer: "searchkick_#{options[:match]}_index"}
390
425
  end
391
426
 
392
- # http://www.elasticsearch.org/guide/reference/mapping/multi-field-type/
393
- multi_field = dynamic_fields["{name}"].merge(fields: dynamic_fields.except("{name}"))
394
-
395
- mappings = {
396
- index_type => {
397
- properties: mapping,
398
- _routing: routing,
399
- # https://gist.github.com/kimchy/2898285
400
- dynamic_templates: [
401
- {
402
- string_template: {
403
- match: "*",
404
- match_mapping_type: "string",
405
- mapping: multi_field
406
- }
407
- }
408
- ]
427
+ if word
428
+ dynamic_fields[:analyzed] = analyzed_field_options
429
+ end
430
+ end
431
+
432
+ # http://www.elasticsearch.org/guide/reference/mapping/multi-field-type/
433
+ multi_field = dynamic_fields["{name}"].merge(fields: dynamic_fields.except("{name}"))
434
+
435
+ mappings = {
436
+ properties: mapping,
437
+ _routing: routing,
438
+ # https://gist.github.com/kimchy/2898285
439
+ dynamic_templates: [
440
+ {
441
+ string_template: {
442
+ match: "*",
443
+ match_mapping_type: "string",
444
+ mapping: multi_field
445
+ }
409
446
  }
447
+ ]
448
+ }
449
+
450
+ if below70?
451
+ mappings = {index_type => mappings}
452
+ end
453
+
454
+ mappings
455
+ end
456
+
457
+ def add_synonyms(settings)
458
+ synonyms = options[:synonyms] || []
459
+ synonyms = synonyms.call if synonyms.respond_to?(:call)
460
+ if synonyms.any?
461
+ settings[:analysis][:filter][:searchkick_synonym] = {
462
+ type: "synonym",
463
+ # only remove a single space from synonyms so three-word synonyms will fail noisily instead of silently
464
+ synonyms: synonyms.select { |s| s.size > 1 }.map { |s| s.is_a?(Array) ? s.map { |s2| s2.sub(/\s+/, "") }.join(",") : s }.map(&:downcase)
410
465
  }
466
+ # choosing a place for the synonym filter when stemming is not easy
467
+ # https://groups.google.com/forum/#!topic/elasticsearch/p7qcQlgHdB8
468
+ # TODO use a snowball stemmer on synonyms when creating the token filter
469
+
470
+ # http://elasticsearch-users.115913.n3.nabble.com/synonym-multi-words-search-td4030811.html
471
+ # I find the following approach effective if you are doing multi-word synonyms (synonym phrases):
472
+ # - Only apply the synonym expansion at index time
473
+ # - Don't have the synonym filter applied search
474
+ # - Use directional synonyms where appropriate. You want to make sure that you're not injecting terms that are too general.
475
+ settings[:analysis][:analyzer][default_analyzer][:filter].insert(2, "searchkick_synonym")
476
+
477
+ %w(word_start word_middle word_end).each do |type|
478
+ settings[:analysis][:analyzer]["searchkick_#{type}_index".to_sym][:filter].insert(2, "searchkick_synonym")
479
+ end
480
+ end
481
+ end
411
482
 
412
- if below60
413
- all_enabled = all && (!options[:searchable] || options[:searchable].to_a.map(&:to_s).include?("_all"))
414
- mappings[index_type][:_all] = all_enabled ? analyzed_field_options : {enabled: false}
483
+ def add_search_synonyms(settings)
484
+ search_synonyms = options[:search_synonyms] || []
485
+ search_synonyms = search_synonyms.call if search_synonyms.respond_to?(:call)
486
+ if search_synonyms.is_a?(String) || search_synonyms.any?
487
+ if search_synonyms.is_a?(String)
488
+ synonym_graph = {
489
+ type: "synonym_graph",
490
+ synonyms_path: search_synonyms
491
+ }
492
+ synonym_graph[:updateable] = true unless below73?
493
+ else
494
+ synonym_graph = {
495
+ type: "synonym_graph",
496
+ # TODO confirm this is correct
497
+ synonyms: search_synonyms.select { |s| s.size > 1 }.map { |s| s.is_a?(Array) ? s.join(",") : s }.map(&:downcase)
498
+ }
415
499
  end
500
+ settings[:analysis][:filter][:searchkick_synonym_graph] = synonym_graph
416
501
 
417
- mappings = mappings.symbolize_keys.deep_merge((options[:mappings] || {}).symbolize_keys)
502
+ [:searchkick_search2, :searchkick_word_search].each do |analyzer|
503
+ settings[:analysis][:analyzer][analyzer][:filter].insert(2, "searchkick_synonym_graph")
504
+ end
418
505
  end
506
+ end
419
507
 
420
- {
421
- settings: settings,
422
- mappings: mappings
508
+ def add_wordnet(settings)
509
+ settings[:analysis][:filter][:searchkick_wordnet] = {
510
+ type: "synonym",
511
+ format: "wordnet",
512
+ synonyms_path: Searchkick.wordnet_path
423
513
  }
514
+
515
+ settings[:analysis][:analyzer][default_analyzer][:filter].insert(4, "searchkick_wordnet")
516
+ settings[:analysis][:analyzer][default_analyzer][:filter] << "searchkick_wordnet"
517
+
518
+ %w(word_start word_middle word_end).each do |type|
519
+ settings[:analysis][:analyzer]["searchkick_#{type}_index".to_sym][:filter].insert(2, "searchkick_wordnet")
520
+ end
521
+ end
522
+
523
+ def set_deep_paging(settings)
524
+ if !settings.dig(:index, :max_result_window) && !settings[:"index.max_result_window"]
525
+ settings[:index] ||= {}
526
+ settings[:index][:max_result_window] = 1_000_000_000
527
+ end
528
+ end
529
+
530
+ def index_type
531
+ @index_type ||= begin
532
+ index_type = options[:_type]
533
+ index_type = index_type.call if index_type.respond_to?(:call)
534
+ index_type
535
+ end
536
+ end
537
+
538
+ def default_type
539
+ "text"
540
+ end
541
+
542
+ def default_analyzer
543
+ :searchkick_index
544
+ end
545
+
546
+ def below62?
547
+ Searchkick.server_below?("6.2.0")
548
+ end
549
+
550
+ def below70?
551
+ Searchkick.server_below?("7.0.0")
552
+ end
553
+
554
+ def below73?
555
+ Searchkick.server_below?("7.3.0")
424
556
  end
425
557
  end
426
558
  end