tiny-quick-gem 0.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 +7 -0
- data/searchkick-6.1.2/CHANGELOG.md +919 -0
- data/searchkick-6.1.2/LICENSE.txt +22 -0
- data/searchkick-6.1.2/README.md +2348 -0
- data/searchkick-6.1.2/lib/searchkick/bulk_reindex_job.rb +19 -0
- data/searchkick-6.1.2/lib/searchkick/controller_runtime.rb +40 -0
- data/searchkick-6.1.2/lib/searchkick/hash_wrapper.rb +41 -0
- data/searchkick-6.1.2/lib/searchkick/index.rb +480 -0
- data/searchkick-6.1.2/lib/searchkick/index_cache.rb +30 -0
- data/searchkick-6.1.2/lib/searchkick/index_options.rb +652 -0
- data/searchkick-6.1.2/lib/searchkick/indexer.rb +43 -0
- data/searchkick-6.1.2/lib/searchkick/log_subscriber.rb +57 -0
- data/searchkick-6.1.2/lib/searchkick/middleware.rb +19 -0
- data/searchkick-6.1.2/lib/searchkick/model.rb +120 -0
- data/searchkick-6.1.2/lib/searchkick/multi_search.rb +46 -0
- data/searchkick-6.1.2/lib/searchkick/process_batch_job.rb +20 -0
- data/searchkick-6.1.2/lib/searchkick/process_queue_job.rb +33 -0
- data/searchkick-6.1.2/lib/searchkick/query.rb +1372 -0
- data/searchkick-6.1.2/lib/searchkick/railtie.rb +7 -0
- data/searchkick-6.1.2/lib/searchkick/record_data.rb +147 -0
- data/searchkick-6.1.2/lib/searchkick/record_indexer.rb +174 -0
- data/searchkick-6.1.2/lib/searchkick/reindex_queue.rb +57 -0
- data/searchkick-6.1.2/lib/searchkick/reindex_v2_job.rb +17 -0
- data/searchkick-6.1.2/lib/searchkick/relation.rb +728 -0
- data/searchkick-6.1.2/lib/searchkick/relation_indexer.rb +184 -0
- data/searchkick-6.1.2/lib/searchkick/reranking.rb +28 -0
- data/searchkick-6.1.2/lib/searchkick/results.rb +359 -0
- data/searchkick-6.1.2/lib/searchkick/script.rb +11 -0
- data/searchkick-6.1.2/lib/searchkick/version.rb +3 -0
- data/searchkick-6.1.2/lib/searchkick/where.rb +11 -0
- data/searchkick-6.1.2/lib/searchkick.rb +388 -0
- data/searchkick-6.1.2/lib/tasks/searchkick.rake +37 -0
- data/tiny-quick-gem.gemspec +12 -0
- metadata +73 -0
|
@@ -0,0 +1,652 @@
|
|
|
1
|
+
module Searchkick
|
|
2
|
+
class IndexOptions
|
|
3
|
+
attr_reader :options
|
|
4
|
+
|
|
5
|
+
def initialize(index)
|
|
6
|
+
@options = index.options
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def index_options
|
|
10
|
+
# mortal symbols are garbage collected in Ruby 2.2+
|
|
11
|
+
custom_settings = (options[:settings] || {}).deep_symbolize_keys
|
|
12
|
+
custom_mappings = (options[:mappings] || {}).deep_symbolize_keys
|
|
13
|
+
|
|
14
|
+
if options[:mappings] && !options[:merge_mappings]
|
|
15
|
+
settings = custom_settings
|
|
16
|
+
mappings = custom_mappings
|
|
17
|
+
else
|
|
18
|
+
settings = generate_settings.deep_symbolize_keys.deep_merge(custom_settings)
|
|
19
|
+
mappings = generate_mappings.deep_symbolize_keys.deep_merge(custom_mappings)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
set_deep_paging(settings) if options[:deep_paging] || options[:max_result_window]
|
|
23
|
+
|
|
24
|
+
{
|
|
25
|
+
settings: settings,
|
|
26
|
+
mappings: mappings
|
|
27
|
+
}
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def generate_settings
|
|
31
|
+
language = options[:language]
|
|
32
|
+
language = language.call if language.respond_to?(:call)
|
|
33
|
+
|
|
34
|
+
settings = {
|
|
35
|
+
analysis: {
|
|
36
|
+
analyzer: {
|
|
37
|
+
searchkick_keyword: {
|
|
38
|
+
type: "custom",
|
|
39
|
+
tokenizer: "keyword",
|
|
40
|
+
filter: ["lowercase"] + (options[:stem_conversions] ? ["searchkick_stemmer"] : [])
|
|
41
|
+
},
|
|
42
|
+
default_analyzer => {
|
|
43
|
+
type: "custom",
|
|
44
|
+
# character filters -> tokenizer -> token filters
|
|
45
|
+
# https://www.elastic.co/guide/en/elasticsearch/guide/current/analysis-intro.html
|
|
46
|
+
char_filter: ["ampersand"],
|
|
47
|
+
tokenizer: "standard",
|
|
48
|
+
# synonym should come last, after stemming and shingle
|
|
49
|
+
# shingle must come before searchkick_stemmer
|
|
50
|
+
filter: ["lowercase", "asciifolding", "searchkick_index_shingle", "searchkick_stemmer"]
|
|
51
|
+
},
|
|
52
|
+
searchkick_search: {
|
|
53
|
+
type: "custom",
|
|
54
|
+
char_filter: ["ampersand"],
|
|
55
|
+
tokenizer: "standard",
|
|
56
|
+
filter: ["lowercase", "asciifolding", "searchkick_search_shingle", "searchkick_stemmer"]
|
|
57
|
+
},
|
|
58
|
+
searchkick_search2: {
|
|
59
|
+
type: "custom",
|
|
60
|
+
char_filter: ["ampersand"],
|
|
61
|
+
tokenizer: "standard",
|
|
62
|
+
filter: ["lowercase", "asciifolding", "searchkick_stemmer"]
|
|
63
|
+
},
|
|
64
|
+
# https://github.com/leschenko/elasticsearch_autocomplete/blob/master/lib/elasticsearch_autocomplete/analyzers.rb
|
|
65
|
+
searchkick_autocomplete_search: {
|
|
66
|
+
type: "custom",
|
|
67
|
+
tokenizer: "keyword",
|
|
68
|
+
filter: ["lowercase", "asciifolding"]
|
|
69
|
+
},
|
|
70
|
+
searchkick_word_search: {
|
|
71
|
+
type: "custom",
|
|
72
|
+
tokenizer: "standard",
|
|
73
|
+
filter: ["lowercase", "asciifolding"]
|
|
74
|
+
},
|
|
75
|
+
searchkick_suggest_index: {
|
|
76
|
+
type: "custom",
|
|
77
|
+
tokenizer: "standard",
|
|
78
|
+
filter: ["lowercase", "asciifolding", "searchkick_suggest_shingle"]
|
|
79
|
+
},
|
|
80
|
+
searchkick_text_start_index: {
|
|
81
|
+
type: "custom",
|
|
82
|
+
tokenizer: "keyword",
|
|
83
|
+
filter: ["lowercase", "asciifolding", "searchkick_edge_ngram"]
|
|
84
|
+
},
|
|
85
|
+
searchkick_text_middle_index: {
|
|
86
|
+
type: "custom",
|
|
87
|
+
tokenizer: "keyword",
|
|
88
|
+
filter: ["lowercase", "asciifolding", "searchkick_ngram"]
|
|
89
|
+
},
|
|
90
|
+
searchkick_text_end_index: {
|
|
91
|
+
type: "custom",
|
|
92
|
+
tokenizer: "keyword",
|
|
93
|
+
filter: ["lowercase", "asciifolding", "reverse", "searchkick_edge_ngram", "reverse"]
|
|
94
|
+
},
|
|
95
|
+
searchkick_word_start_index: {
|
|
96
|
+
type: "custom",
|
|
97
|
+
tokenizer: "standard",
|
|
98
|
+
filter: ["lowercase", "asciifolding", "searchkick_edge_ngram"]
|
|
99
|
+
},
|
|
100
|
+
searchkick_word_middle_index: {
|
|
101
|
+
type: "custom",
|
|
102
|
+
tokenizer: "standard",
|
|
103
|
+
filter: ["lowercase", "asciifolding", "searchkick_ngram"]
|
|
104
|
+
},
|
|
105
|
+
searchkick_word_end_index: {
|
|
106
|
+
type: "custom",
|
|
107
|
+
tokenizer: "standard",
|
|
108
|
+
filter: ["lowercase", "asciifolding", "reverse", "searchkick_edge_ngram", "reverse"]
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
filter: {
|
|
112
|
+
searchkick_index_shingle: {
|
|
113
|
+
type: "shingle",
|
|
114
|
+
token_separator: ""
|
|
115
|
+
},
|
|
116
|
+
# lucky find https://web.archiveorange.com/archive/v/AAfXfQ17f57FcRINsof7
|
|
117
|
+
searchkick_search_shingle: {
|
|
118
|
+
type: "shingle",
|
|
119
|
+
token_separator: "",
|
|
120
|
+
output_unigrams: false,
|
|
121
|
+
output_unigrams_if_no_shingles: true
|
|
122
|
+
},
|
|
123
|
+
searchkick_suggest_shingle: {
|
|
124
|
+
type: "shingle",
|
|
125
|
+
max_shingle_size: 5
|
|
126
|
+
},
|
|
127
|
+
searchkick_edge_ngram: {
|
|
128
|
+
type: "edge_ngram",
|
|
129
|
+
min_gram: 1,
|
|
130
|
+
max_gram: 50
|
|
131
|
+
},
|
|
132
|
+
searchkick_ngram: {
|
|
133
|
+
type: "ngram",
|
|
134
|
+
min_gram: 1,
|
|
135
|
+
max_gram: 50
|
|
136
|
+
},
|
|
137
|
+
searchkick_stemmer: {
|
|
138
|
+
# use stemmer if language is lowercase, snowball otherwise
|
|
139
|
+
type: language == language.to_s.downcase ? "stemmer" : "snowball",
|
|
140
|
+
language: language || "English"
|
|
141
|
+
}
|
|
142
|
+
},
|
|
143
|
+
char_filter: {
|
|
144
|
+
# https://www.elastic.co/guide/en/elasticsearch/guide/current/custom-analyzers.html
|
|
145
|
+
# &_to_and
|
|
146
|
+
ampersand: {
|
|
147
|
+
type: "mapping",
|
|
148
|
+
mappings: ["&=> and "]
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
raise ArgumentError, "Can't pass both language and stemmer" if options[:stemmer] && language
|
|
155
|
+
update_language(settings, language)
|
|
156
|
+
update_stemming(settings)
|
|
157
|
+
|
|
158
|
+
if Searchkick.env == "test"
|
|
159
|
+
settings[:number_of_shards] = 1
|
|
160
|
+
settings[:number_of_replicas] = 0
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
if options[:similarity]
|
|
164
|
+
settings[:similarity] = {default: {type: options[:similarity]}}
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
settings[:index] = {
|
|
168
|
+
max_ngram_diff: 49,
|
|
169
|
+
max_shingle_diff: 4
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if options[:knn]
|
|
173
|
+
unless Searchkick.knn_support?
|
|
174
|
+
if Searchkick.opensearch?
|
|
175
|
+
raise Error, "knn requires OpenSearch 2.4+"
|
|
176
|
+
else
|
|
177
|
+
raise Error, "knn requires Elasticsearch 8.6+"
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
if Searchkick.opensearch? && options[:knn].any? { |_, v| !v[:distance].nil? }
|
|
182
|
+
# only enable if doing approximate search
|
|
183
|
+
settings[:index][:knn] = true
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
add_synonyms(settings)
|
|
188
|
+
add_search_synonyms(settings)
|
|
189
|
+
|
|
190
|
+
if options[:special_characters] == false
|
|
191
|
+
settings[:analysis][:analyzer].each_value do |analyzer_settings|
|
|
192
|
+
analyzer_settings[:filter].reject! { |f| f == "asciifolding" }
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
if options[:case_sensitive]
|
|
197
|
+
settings[:analysis][:analyzer].each do |_, analyzer|
|
|
198
|
+
analyzer[:filter].delete("lowercase")
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
settings
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def update_language(settings, language)
|
|
206
|
+
case language
|
|
207
|
+
when "chinese"
|
|
208
|
+
settings[:analysis][:analyzer].merge!(
|
|
209
|
+
default_analyzer => {
|
|
210
|
+
type: "ik_smart"
|
|
211
|
+
},
|
|
212
|
+
searchkick_search: {
|
|
213
|
+
type: "ik_smart"
|
|
214
|
+
},
|
|
215
|
+
searchkick_search2: {
|
|
216
|
+
type: "ik_max_word"
|
|
217
|
+
}
|
|
218
|
+
)
|
|
219
|
+
when "chinese2", "smartcn"
|
|
220
|
+
settings[:analysis][:analyzer].merge!(
|
|
221
|
+
default_analyzer => {
|
|
222
|
+
type: "smartcn"
|
|
223
|
+
},
|
|
224
|
+
searchkick_search: {
|
|
225
|
+
type: "smartcn"
|
|
226
|
+
},
|
|
227
|
+
searchkick_search2: {
|
|
228
|
+
type: "smartcn"
|
|
229
|
+
}
|
|
230
|
+
)
|
|
231
|
+
when "japanese", "japanese2"
|
|
232
|
+
analyzer = {
|
|
233
|
+
type: "custom",
|
|
234
|
+
tokenizer: "kuromoji_tokenizer",
|
|
235
|
+
filter: [
|
|
236
|
+
"kuromoji_baseform",
|
|
237
|
+
"kuromoji_part_of_speech",
|
|
238
|
+
"cjk_width",
|
|
239
|
+
"ja_stop",
|
|
240
|
+
"searchkick_stemmer",
|
|
241
|
+
"lowercase"
|
|
242
|
+
]
|
|
243
|
+
}
|
|
244
|
+
settings[:analysis][:analyzer].merge!(
|
|
245
|
+
default_analyzer => analyzer.deep_dup,
|
|
246
|
+
searchkick_search: analyzer.deep_dup,
|
|
247
|
+
searchkick_search2: analyzer.deep_dup
|
|
248
|
+
)
|
|
249
|
+
settings[:analysis][:filter][:searchkick_stemmer] = {
|
|
250
|
+
type: "kuromoji_stemmer"
|
|
251
|
+
}
|
|
252
|
+
when "korean"
|
|
253
|
+
settings[:analysis][:analyzer].merge!(
|
|
254
|
+
default_analyzer => {
|
|
255
|
+
type: "openkoreantext-analyzer"
|
|
256
|
+
},
|
|
257
|
+
searchkick_search: {
|
|
258
|
+
type: "openkoreantext-analyzer"
|
|
259
|
+
},
|
|
260
|
+
searchkick_search2: {
|
|
261
|
+
type: "openkoreantext-analyzer"
|
|
262
|
+
}
|
|
263
|
+
)
|
|
264
|
+
when "korean2"
|
|
265
|
+
settings[:analysis][:analyzer].merge!(
|
|
266
|
+
default_analyzer => {
|
|
267
|
+
type: "nori"
|
|
268
|
+
},
|
|
269
|
+
searchkick_search: {
|
|
270
|
+
type: "nori"
|
|
271
|
+
},
|
|
272
|
+
searchkick_search2: {
|
|
273
|
+
type: "nori"
|
|
274
|
+
}
|
|
275
|
+
)
|
|
276
|
+
when "vietnamese"
|
|
277
|
+
settings[:analysis][:analyzer].merge!(
|
|
278
|
+
default_analyzer => {
|
|
279
|
+
type: "vi_analyzer"
|
|
280
|
+
},
|
|
281
|
+
searchkick_search: {
|
|
282
|
+
type: "vi_analyzer"
|
|
283
|
+
},
|
|
284
|
+
searchkick_search2: {
|
|
285
|
+
type: "vi_analyzer"
|
|
286
|
+
}
|
|
287
|
+
)
|
|
288
|
+
when "polish", "ukrainian"
|
|
289
|
+
settings[:analysis][:analyzer].merge!(
|
|
290
|
+
default_analyzer => {
|
|
291
|
+
type: language
|
|
292
|
+
},
|
|
293
|
+
searchkick_search: {
|
|
294
|
+
type: language
|
|
295
|
+
},
|
|
296
|
+
searchkick_search2: {
|
|
297
|
+
type: language
|
|
298
|
+
}
|
|
299
|
+
)
|
|
300
|
+
end
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
def update_stemming(settings)
|
|
304
|
+
if options[:stemmer]
|
|
305
|
+
stemmer = options[:stemmer]
|
|
306
|
+
# could also support snowball and stemmer
|
|
307
|
+
case stemmer[:type]
|
|
308
|
+
when "hunspell"
|
|
309
|
+
# supports all token filter options
|
|
310
|
+
settings[:analysis][:filter][:searchkick_stemmer] = stemmer
|
|
311
|
+
else
|
|
312
|
+
raise ArgumentError, "Unknown stemmer: #{stemmer[:type]}"
|
|
313
|
+
end
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
stem = options[:stem]
|
|
317
|
+
|
|
318
|
+
# language analyzer used
|
|
319
|
+
stem = false if settings[:analysis][:analyzer][default_analyzer][:type] != "custom"
|
|
320
|
+
|
|
321
|
+
if stem == false
|
|
322
|
+
settings[:analysis][:filter].delete(:searchkick_stemmer)
|
|
323
|
+
settings[:analysis][:analyzer].each do |_, analyzer|
|
|
324
|
+
analyzer[:filter].delete("searchkick_stemmer") if analyzer[:filter]
|
|
325
|
+
end
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
if options[:stemmer_override]
|
|
329
|
+
stemmer_override = {
|
|
330
|
+
type: "stemmer_override"
|
|
331
|
+
}
|
|
332
|
+
if options[:stemmer_override].is_a?(String)
|
|
333
|
+
stemmer_override[:rules_path] = options[:stemmer_override]
|
|
334
|
+
else
|
|
335
|
+
stemmer_override[:rules] = options[:stemmer_override]
|
|
336
|
+
end
|
|
337
|
+
settings[:analysis][:filter][:searchkick_stemmer_override] = stemmer_override
|
|
338
|
+
|
|
339
|
+
settings[:analysis][:analyzer].each do |_, analyzer|
|
|
340
|
+
stemmer_index = analyzer[:filter].index("searchkick_stemmer") if analyzer[:filter]
|
|
341
|
+
analyzer[:filter].insert(stemmer_index, "searchkick_stemmer_override") if stemmer_index
|
|
342
|
+
end
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
if options[:stem_exclusion]
|
|
346
|
+
settings[:analysis][:filter][:searchkick_stem_exclusion] = {
|
|
347
|
+
type: "keyword_marker",
|
|
348
|
+
keywords: options[:stem_exclusion]
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
settings[:analysis][:analyzer].each do |_, analyzer|
|
|
352
|
+
stemmer_index = analyzer[:filter].index("searchkick_stemmer") if analyzer[:filter]
|
|
353
|
+
analyzer[:filter].insert(stemmer_index, "searchkick_stem_exclusion") if stemmer_index
|
|
354
|
+
end
|
|
355
|
+
end
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
def generate_mappings
|
|
359
|
+
mapping = {}
|
|
360
|
+
|
|
361
|
+
keyword_mapping = {type: "keyword"}
|
|
362
|
+
keyword_mapping[:ignore_above] = options[:ignore_above] || 30000
|
|
363
|
+
|
|
364
|
+
# conversions
|
|
365
|
+
Array(options[:conversions]).each do |conversions_field|
|
|
366
|
+
mapping[conversions_field] = {
|
|
367
|
+
type: "nested",
|
|
368
|
+
properties: {
|
|
369
|
+
query: {type: default_type, analyzer: "searchkick_keyword"},
|
|
370
|
+
count: {type: "integer"}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
end
|
|
374
|
+
|
|
375
|
+
Array(options[:conversions_v2]).each do |conversions_field|
|
|
376
|
+
mapping[conversions_field] = {
|
|
377
|
+
type: "rank_features"
|
|
378
|
+
}
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
if (Array(options[:conversions_v2]).map(&:to_s) & Array(options[:conversions]).map(&:to_s)).any?
|
|
382
|
+
raise ArgumentError, "Must have separate conversions fields"
|
|
383
|
+
end
|
|
384
|
+
|
|
385
|
+
mapping_options =
|
|
386
|
+
[:suggest, :word, :text_start, :text_middle, :text_end, :word_start, :word_middle, :word_end, :highlight, :searchable, :filterable]
|
|
387
|
+
.to_h { |type| [type, (options[type] || []).map(&:to_s)] }
|
|
388
|
+
|
|
389
|
+
word = options[:word] != false && (!options[:match] || options[:match] == :word)
|
|
390
|
+
|
|
391
|
+
mapping_options[:searchable].delete("_all")
|
|
392
|
+
|
|
393
|
+
analyzed_field_options = {type: default_type, index: true, analyzer: default_analyzer.to_s}
|
|
394
|
+
|
|
395
|
+
mapping_options.values.flatten.uniq.each do |field|
|
|
396
|
+
fields = {}
|
|
397
|
+
|
|
398
|
+
if options.key?(:filterable) && !mapping_options[:filterable].include?(field)
|
|
399
|
+
fields[field] = {type: default_type, index: false}
|
|
400
|
+
else
|
|
401
|
+
fields[field] = keyword_mapping
|
|
402
|
+
end
|
|
403
|
+
|
|
404
|
+
if !options[:searchable] || mapping_options[:searchable].include?(field)
|
|
405
|
+
if word
|
|
406
|
+
fields[:analyzed] = analyzed_field_options
|
|
407
|
+
|
|
408
|
+
if mapping_options[:highlight].include?(field)
|
|
409
|
+
fields[:analyzed][:term_vector] = "with_positions_offsets"
|
|
410
|
+
end
|
|
411
|
+
end
|
|
412
|
+
|
|
413
|
+
mapping_options.except(:highlight, :searchable, :filterable, :word).each do |type, f|
|
|
414
|
+
if options[:match] == type || f.include?(field)
|
|
415
|
+
fields[type] = {type: default_type, index: true, analyzer: "searchkick_#{type}_index"}
|
|
416
|
+
end
|
|
417
|
+
end
|
|
418
|
+
end
|
|
419
|
+
|
|
420
|
+
mapping[field] = fields[field].merge(fields: fields.except(field))
|
|
421
|
+
end
|
|
422
|
+
|
|
423
|
+
(options[:locations] || []).map(&:to_s).each do |field|
|
|
424
|
+
mapping[field] = {
|
|
425
|
+
type: "geo_point"
|
|
426
|
+
}
|
|
427
|
+
end
|
|
428
|
+
|
|
429
|
+
options[:geo_shape] = options[:geo_shape].product([{}]).to_h if options[:geo_shape].is_a?(Array)
|
|
430
|
+
(options[:geo_shape] || {}).each do |field, shape_options|
|
|
431
|
+
mapping[field] = shape_options.merge(type: "geo_shape")
|
|
432
|
+
end
|
|
433
|
+
|
|
434
|
+
(options[:knn] || []).each do |field, knn_options|
|
|
435
|
+
distance = knn_options[:distance]
|
|
436
|
+
quantization = knn_options[:quantization]
|
|
437
|
+
|
|
438
|
+
if Searchkick.opensearch?
|
|
439
|
+
if distance.nil?
|
|
440
|
+
# avoid server crash if method not specified
|
|
441
|
+
raise ArgumentError, "Must specify a distance for OpenSearch"
|
|
442
|
+
end
|
|
443
|
+
|
|
444
|
+
vector_options = {
|
|
445
|
+
type: "knn_vector",
|
|
446
|
+
dimension: knn_options[:dimensions]
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
if !distance.nil?
|
|
450
|
+
space_type =
|
|
451
|
+
case distance
|
|
452
|
+
when "cosine"
|
|
453
|
+
"cosinesimil"
|
|
454
|
+
when "euclidean"
|
|
455
|
+
"l2"
|
|
456
|
+
when "inner_product"
|
|
457
|
+
"innerproduct"
|
|
458
|
+
else
|
|
459
|
+
raise ArgumentError, "Unknown distance: #{distance}"
|
|
460
|
+
end
|
|
461
|
+
|
|
462
|
+
if !quantization.nil?
|
|
463
|
+
raise ArgumentError, "Quantization not supported yet for OpenSearch"
|
|
464
|
+
end
|
|
465
|
+
|
|
466
|
+
vector_options[:method] = {
|
|
467
|
+
name: "hnsw",
|
|
468
|
+
space_type: space_type,
|
|
469
|
+
engine: "lucene",
|
|
470
|
+
parameters: knn_options.slice(:m, :ef_construction)
|
|
471
|
+
}
|
|
472
|
+
end
|
|
473
|
+
|
|
474
|
+
mapping[field.to_s] = vector_options
|
|
475
|
+
else
|
|
476
|
+
vector_options = {
|
|
477
|
+
type: "dense_vector",
|
|
478
|
+
dims: knn_options[:dimensions],
|
|
479
|
+
index: !distance.nil?
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
if !distance.nil?
|
|
483
|
+
vector_options[:similarity] =
|
|
484
|
+
case distance
|
|
485
|
+
when "cosine"
|
|
486
|
+
"cosine"
|
|
487
|
+
when "euclidean"
|
|
488
|
+
"l2_norm"
|
|
489
|
+
when "inner_product"
|
|
490
|
+
"max_inner_product"
|
|
491
|
+
else
|
|
492
|
+
raise ArgumentError, "Unknown distance: #{distance}"
|
|
493
|
+
end
|
|
494
|
+
|
|
495
|
+
type =
|
|
496
|
+
case quantization
|
|
497
|
+
when "int8", "int4", "bbq"
|
|
498
|
+
"#{quantization}_hnsw"
|
|
499
|
+
when nil
|
|
500
|
+
"hnsw"
|
|
501
|
+
else
|
|
502
|
+
raise ArgumentError, "Unknown quantization: #{quantization}"
|
|
503
|
+
end
|
|
504
|
+
|
|
505
|
+
vector_index_options = knn_options.slice(:m, :ef_construction)
|
|
506
|
+
vector_options[:index_options] = {type: type}.merge(vector_index_options)
|
|
507
|
+
end
|
|
508
|
+
|
|
509
|
+
mapping[field.to_s] = vector_options
|
|
510
|
+
end
|
|
511
|
+
end
|
|
512
|
+
|
|
513
|
+
if options[:inheritance]
|
|
514
|
+
mapping[:type] = keyword_mapping
|
|
515
|
+
end
|
|
516
|
+
|
|
517
|
+
routing = {}
|
|
518
|
+
if options[:routing]
|
|
519
|
+
routing = {required: true}
|
|
520
|
+
unless options[:routing] == true
|
|
521
|
+
routing[:path] = options[:routing].to_s
|
|
522
|
+
end
|
|
523
|
+
end
|
|
524
|
+
|
|
525
|
+
dynamic_fields = {
|
|
526
|
+
# analyzed field must be the default field for include_in_all
|
|
527
|
+
# https://www.elastic.co/guide/reference/mapping/multi-field-type/
|
|
528
|
+
# however, we can include the not_analyzed field in _all
|
|
529
|
+
# and the _all index analyzer will take care of it
|
|
530
|
+
"{name}" => keyword_mapping
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
if options.key?(:filterable)
|
|
534
|
+
dynamic_fields["{name}"] = {type: default_type, index: false}
|
|
535
|
+
end
|
|
536
|
+
|
|
537
|
+
unless options[:searchable]
|
|
538
|
+
if options[:match] && options[:match] != :word
|
|
539
|
+
dynamic_fields[options[:match]] = {type: default_type, index: true, analyzer: "searchkick_#{options[:match]}_index"}
|
|
540
|
+
end
|
|
541
|
+
|
|
542
|
+
if word
|
|
543
|
+
dynamic_fields[:analyzed] = analyzed_field_options
|
|
544
|
+
end
|
|
545
|
+
end
|
|
546
|
+
|
|
547
|
+
# https://www.elastic.co/guide/reference/mapping/multi-field-type/
|
|
548
|
+
multi_field = dynamic_fields["{name}"].merge(fields: dynamic_fields.except("{name}"))
|
|
549
|
+
|
|
550
|
+
mappings = {
|
|
551
|
+
properties: mapping,
|
|
552
|
+
_routing: routing,
|
|
553
|
+
# https://gist.github.com/kimchy/2898285
|
|
554
|
+
dynamic_templates: [
|
|
555
|
+
{
|
|
556
|
+
string_template: {
|
|
557
|
+
match: "*",
|
|
558
|
+
match_mapping_type: "string",
|
|
559
|
+
mapping: multi_field
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
]
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
mappings
|
|
566
|
+
end
|
|
567
|
+
|
|
568
|
+
def add_synonyms(settings)
|
|
569
|
+
synonyms = options[:synonyms] || []
|
|
570
|
+
synonyms = synonyms.call if synonyms.respond_to?(:call)
|
|
571
|
+
if synonyms.any?
|
|
572
|
+
settings[:analysis][:filter][:searchkick_synonym] = {
|
|
573
|
+
type: "synonym",
|
|
574
|
+
# only remove a single space from synonyms so three-word synonyms will fail noisily instead of silently
|
|
575
|
+
synonyms: synonyms.select { |s| s.size > 1 }.map { |s| s.is_a?(Array) ? s.map { |s2| s2.sub(/\s+/, "") }.join(",") : s }.map(&:downcase)
|
|
576
|
+
}
|
|
577
|
+
# choosing a place for the synonym filter when stemming is not easy
|
|
578
|
+
# https://groups.google.com/forum/#!topic/elasticsearch/p7qcQlgHdB8
|
|
579
|
+
# TODO use a snowball stemmer on synonyms when creating the token filter
|
|
580
|
+
|
|
581
|
+
# https://discuss.elastic.co/t/synonym-multi-words-search/10964
|
|
582
|
+
# I find the following approach effective if you are doing multi-word synonyms (synonym phrases):
|
|
583
|
+
# - Only apply the synonym expansion at index time
|
|
584
|
+
# - Don't have the synonym filter applied search
|
|
585
|
+
# - Use directional synonyms where appropriate. You want to make sure that you're not injecting terms that are too general.
|
|
586
|
+
settings[:analysis][:analyzer][default_analyzer][:filter].insert(2, "searchkick_synonym")
|
|
587
|
+
|
|
588
|
+
%w(word_start word_middle word_end).each do |type|
|
|
589
|
+
settings[:analysis][:analyzer]["searchkick_#{type}_index".to_sym][:filter].insert(2, "searchkick_synonym")
|
|
590
|
+
end
|
|
591
|
+
end
|
|
592
|
+
end
|
|
593
|
+
|
|
594
|
+
def add_search_synonyms(settings)
|
|
595
|
+
search_synonyms = options[:search_synonyms] || []
|
|
596
|
+
search_synonyms = search_synonyms.call if search_synonyms.respond_to?(:call)
|
|
597
|
+
if search_synonyms.is_a?(String) || search_synonyms.any?
|
|
598
|
+
if search_synonyms.is_a?(String)
|
|
599
|
+
synonym_graph = {
|
|
600
|
+
type: "synonym_graph",
|
|
601
|
+
synonyms_path: search_synonyms,
|
|
602
|
+
updateable: true
|
|
603
|
+
}
|
|
604
|
+
else
|
|
605
|
+
synonym_graph = {
|
|
606
|
+
type: "synonym_graph",
|
|
607
|
+
# TODO confirm this is correct
|
|
608
|
+
synonyms: search_synonyms.select { |s| s.size > 1 }.map { |s| s.is_a?(Array) ? s.join(",") : s }.map(&:downcase)
|
|
609
|
+
}
|
|
610
|
+
end
|
|
611
|
+
settings[:analysis][:filter][:searchkick_synonym_graph] = synonym_graph
|
|
612
|
+
|
|
613
|
+
if ["japanese", "japanese2"].include?(options[:language])
|
|
614
|
+
[:searchkick_search, :searchkick_search2].each do |analyzer|
|
|
615
|
+
settings[:analysis][:analyzer][analyzer][:filter].insert(4, "searchkick_synonym_graph")
|
|
616
|
+
end
|
|
617
|
+
else
|
|
618
|
+
[:searchkick_search2, :searchkick_word_search].each do |analyzer|
|
|
619
|
+
unless settings[:analysis][:analyzer][analyzer].key?(:filter)
|
|
620
|
+
raise Error, "Search synonyms are not supported yet for language"
|
|
621
|
+
end
|
|
622
|
+
|
|
623
|
+
settings[:analysis][:analyzer][analyzer][:filter].insert(2, "searchkick_synonym_graph")
|
|
624
|
+
end
|
|
625
|
+
end
|
|
626
|
+
end
|
|
627
|
+
end
|
|
628
|
+
|
|
629
|
+
def set_deep_paging(settings)
|
|
630
|
+
if !settings.dig(:index, :max_result_window) && !settings[:"index.max_result_window"]
|
|
631
|
+
settings[:index] ||= {}
|
|
632
|
+
settings[:index][:max_result_window] = options[:max_result_window] || 1_000_000_000
|
|
633
|
+
end
|
|
634
|
+
end
|
|
635
|
+
|
|
636
|
+
def index_type
|
|
637
|
+
@index_type ||= begin
|
|
638
|
+
index_type = options[:_type]
|
|
639
|
+
index_type = index_type.call if index_type.respond_to?(:call)
|
|
640
|
+
index_type
|
|
641
|
+
end
|
|
642
|
+
end
|
|
643
|
+
|
|
644
|
+
def default_type
|
|
645
|
+
"text"
|
|
646
|
+
end
|
|
647
|
+
|
|
648
|
+
def default_analyzer
|
|
649
|
+
:searchkick_index
|
|
650
|
+
end
|
|
651
|
+
end
|
|
652
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# thread-local (technically fiber-local) indexer
|
|
2
|
+
# used to aggregate bulk callbacks across models
|
|
3
|
+
module Searchkick
|
|
4
|
+
class Indexer
|
|
5
|
+
attr_reader :queued_items
|
|
6
|
+
|
|
7
|
+
def initialize
|
|
8
|
+
@queued_items = []
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def queue(items)
|
|
12
|
+
@queued_items.concat(items)
|
|
13
|
+
perform unless Searchkick.callbacks_value == :bulk
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def perform
|
|
17
|
+
items = @queued_items
|
|
18
|
+
@queued_items = []
|
|
19
|
+
|
|
20
|
+
return if items.empty?
|
|
21
|
+
|
|
22
|
+
response = Searchkick.client.bulk(body: items)
|
|
23
|
+
if response["errors"]
|
|
24
|
+
# note: delete does not set error when item not found
|
|
25
|
+
first_with_error = response["items"].map do |item|
|
|
26
|
+
(item["index"] || item["delete"] || item["update"])
|
|
27
|
+
end.find.with_index { |item, i| item["error"] && !ignore_missing?(items[i], item["error"]) }
|
|
28
|
+
if first_with_error
|
|
29
|
+
raise ImportError, "#{first_with_error["error"]} on item with id '#{first_with_error["_id"]}'"
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# maybe return response in future
|
|
34
|
+
nil
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def ignore_missing?(item, error)
|
|
40
|
+
error["type"] == "document_missing_exception" && item.instance_variable_defined?(:@ignore_missing)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|