search-engine-for-typesense 30.1.6.11 → 30.1.6.12
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 +4 -4
- data/lib/search_engine/base/index_maintenance/lifecycle.rb +23 -4
- data/lib/search_engine/base/index_maintenance/schema.rb +49 -19
- data/lib/search_engine/base/index_maintenance.rb +33 -2
- data/lib/search_engine/cascade.rb +13 -27
- data/lib/search_engine/client/services/collections.rb +20 -0
- data/lib/search_engine/client.rb +8 -0
- data/lib/search_engine/indexer.rb +1 -1
- data/lib/search_engine/test/offline_client.rb +4 -0
- data/lib/search_engine/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4596b142a6080ea8f9941f94611f32e9c461b3e9be51bb8e024e821c68ccd498
|
|
4
|
+
data.tar.gz: 0ea309bf811b27491de030021053c888d3c4fae0047292432a7f89a46f4ad722
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3deec81bb58a05b8582c7081aa345cb4b1cc16b270d576ba22a0d03316590f1fdecbd885a7e5ed35842f909a3e0c278c77dce03232b81ec5091f33771fb5ec09
|
|
7
|
+
data.tar.gz: 5577de682690a81acb90e23563b5e7cdd26bfc4b90efe7c28b7d8bbe6224e5c804820829d2a7b9d5020e8941e6b022f5587c76675e5c871f6a660a4183cd23bc
|
|
@@ -218,11 +218,29 @@ module SearchEngine
|
|
|
218
218
|
step = SearchEngine::Logging::StepLine.new('Partial Indexing')
|
|
219
219
|
step.update('indexing')
|
|
220
220
|
step.yield_line!
|
|
221
|
+
|
|
222
|
+
renderer = SearchEngine::Logging::LiveRenderer.new(
|
|
223
|
+
labels: partitions.map(&:inspect), partitions: partitions
|
|
224
|
+
)
|
|
225
|
+
renderer.start
|
|
221
226
|
summaries = []
|
|
222
|
-
partitions.
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
227
|
+
partitions.each_with_index do |p, idx|
|
|
228
|
+
slot = renderer[idx]
|
|
229
|
+
slot.start
|
|
230
|
+
begin
|
|
231
|
+
on_batch = ->(info) { slot.progress(**info) }
|
|
232
|
+
summary = SearchEngine::Indexer.rebuild_partition!(self, partition: p, into: nil, on_batch: on_batch)
|
|
233
|
+
slot.finish(summary)
|
|
234
|
+
summaries << summary
|
|
235
|
+
rescue StandardError => error
|
|
236
|
+
slot.finish_error(error)
|
|
237
|
+
raise
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
begin
|
|
241
|
+
renderer.stop
|
|
242
|
+
rescue StandardError
|
|
243
|
+
nil
|
|
226
244
|
end
|
|
227
245
|
step.finish('done')
|
|
228
246
|
|
|
@@ -230,6 +248,7 @@ module SearchEngine
|
|
|
230
248
|
__se_cascade_after_indexation!(context: :full) if result[:status] == :ok
|
|
231
249
|
result
|
|
232
250
|
ensure
|
|
251
|
+
renderer&.stop
|
|
233
252
|
step&.close
|
|
234
253
|
end
|
|
235
254
|
|
|
@@ -46,46 +46,76 @@ module SearchEngine
|
|
|
46
46
|
logical = respond_to?(:collection) ? collection.to_s : name.to_s
|
|
47
47
|
|
|
48
48
|
alias_target = client.resolve_alias(logical, timeout_ms: 10_000)
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
live ? logical : nil
|
|
54
|
-
end
|
|
49
|
+
has_alias = alias_target && !alias_target.to_s.strip.empty?
|
|
50
|
+
|
|
51
|
+
physicals = __se_list_all_physicals(logical, client)
|
|
52
|
+
bare_schema = client.retrieve_collection_schema(logical, timeout_ms: 10_000)
|
|
55
53
|
|
|
56
54
|
step = SearchEngine::Logging::StepLine.new('Drop Collection')
|
|
57
|
-
if
|
|
55
|
+
if !has_alias && physicals.empty? && bare_schema.nil?
|
|
58
56
|
step.skip('not present')
|
|
59
57
|
return
|
|
60
58
|
end
|
|
61
59
|
|
|
62
60
|
puts
|
|
63
61
|
puts(SearchEngine::Logging::Color.header(%(>>>>>> Dropping Collection "#{logical}")))
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
62
|
+
|
|
63
|
+
physicals.each do |name|
|
|
64
|
+
step.update("dropping physical #{name}")
|
|
65
|
+
client.delete_collection(name, timeout_ms: 60_000)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
if bare_schema && !physicals.include?(logical)
|
|
69
|
+
step.update("dropping bare collection #{logical}")
|
|
70
|
+
client.delete_collection(logical, timeout_ms: 60_000)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
if has_alias
|
|
74
|
+
step.update("deleting alias #{logical}")
|
|
75
|
+
client.delete_alias(logical)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
step.finish("done (physicals=#{physicals.size})")
|
|
67
79
|
puts(SearchEngine::Logging::Color.header(%(>>>>>> Dropped Collection "#{logical}")))
|
|
68
80
|
nil
|
|
69
81
|
ensure
|
|
70
82
|
step&.close
|
|
71
83
|
end
|
|
72
84
|
|
|
85
|
+
# @return [Array<String>] physical collection names matching the logical pattern
|
|
86
|
+
def __se_list_all_physicals(logical, client)
|
|
87
|
+
meta_timeout = begin
|
|
88
|
+
t = SearchEngine.config.timeout_ms.to_i
|
|
89
|
+
[t, 10_000].max
|
|
90
|
+
rescue StandardError
|
|
91
|
+
10_000
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
all_collections = Array(client.list_collections(timeout_ms: meta_timeout))
|
|
95
|
+
names = all_collections.map { |c| (c[:name] || c['name']).to_s }
|
|
96
|
+
re = /\A#{Regexp.escape(logical)}_\d{8}_\d{6}_\d{3}\z/
|
|
97
|
+
names.select { |n| re.match?(n) }
|
|
98
|
+
rescue StandardError
|
|
99
|
+
[]
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
private :__se_list_all_physicals
|
|
103
|
+
|
|
73
104
|
def recreate_collection!
|
|
74
105
|
client = SearchEngine.client
|
|
75
106
|
logical = respond_to?(:collection) ? collection.to_s : name.to_s
|
|
76
107
|
|
|
77
108
|
alias_target = client.resolve_alias(logical)
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
live = client.retrieve_collection_schema(logical)
|
|
82
|
-
live ? logical : nil
|
|
83
|
-
end
|
|
109
|
+
has_alias = alias_target && !alias_target.to_s.strip.empty?
|
|
110
|
+
physicals = __se_list_all_physicals(logical, client)
|
|
111
|
+
bare_schema = client.retrieve_collection_schema(logical)
|
|
84
112
|
|
|
85
113
|
step = SearchEngine::Logging::StepLine.new('Recreate Collection')
|
|
86
|
-
if
|
|
87
|
-
step.update("dropping existing (logical=#{logical}
|
|
88
|
-
client.delete_collection(
|
|
114
|
+
if has_alias || physicals.any? || bare_schema
|
|
115
|
+
step.update("dropping existing (logical=#{logical})")
|
|
116
|
+
physicals.each { |name| client.delete_collection(name) }
|
|
117
|
+
client.delete_collection(logical) if bare_schema && !physicals.include?(logical)
|
|
118
|
+
client.delete_alias(logical) if has_alias
|
|
89
119
|
else
|
|
90
120
|
step.update("creating (logical=#{logical})")
|
|
91
121
|
end
|
|
@@ -274,14 +274,45 @@ module SearchEngine
|
|
|
274
274
|
|
|
275
275
|
__se_index_partitions_parallel!(parts, into, max_p, compiled)
|
|
276
276
|
else
|
|
277
|
-
|
|
278
|
-
__se_build_index_result([summary])
|
|
277
|
+
__se_index_single_with_renderer!(into)
|
|
279
278
|
end
|
|
280
279
|
rescue StandardError => error
|
|
281
280
|
{ status: :failed, docs_total: 0, success_total: 0, failed_total: 0,
|
|
282
281
|
sample_error: "#{error.class}: #{error.message.to_s[0, 200]}" }
|
|
283
282
|
end
|
|
284
283
|
|
|
284
|
+
def __se_index_single_with_renderer!(into)
|
|
285
|
+
docs_estimate = __se_heuristic_docs_estimate(1)
|
|
286
|
+
renderer = SearchEngine::Logging::LiveRenderer.new(
|
|
287
|
+
labels: ['single'], partitions: [nil],
|
|
288
|
+
per_partition_docs_estimates: [docs_estimate]
|
|
289
|
+
)
|
|
290
|
+
renderer.start
|
|
291
|
+
|
|
292
|
+
summary = nil
|
|
293
|
+
slot = renderer[0]
|
|
294
|
+
slot.start
|
|
295
|
+
begin
|
|
296
|
+
on_batch = ->(info) { slot.progress(**info) }
|
|
297
|
+
summary = SearchEngine::Indexer.rebuild_partition!(self, partition: nil, into: into, on_batch: on_batch)
|
|
298
|
+
slot.finish(summary)
|
|
299
|
+
rescue StandardError => error
|
|
300
|
+
slot.finish_error(error)
|
|
301
|
+
raise
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
begin
|
|
305
|
+
renderer.stop
|
|
306
|
+
rescue StandardError
|
|
307
|
+
nil
|
|
308
|
+
end
|
|
309
|
+
__se_build_index_result([summary])
|
|
310
|
+
ensure
|
|
311
|
+
renderer&.stop
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
private :__se_index_single_with_renderer!
|
|
315
|
+
|
|
285
316
|
# Aggregate an array of Indexer::Summary structs into a single result hash.
|
|
286
317
|
# @param summaries [Array<SearchEngine::Indexer::Summary>]
|
|
287
318
|
# @return [Hash] { status:, docs_total:, success_total:, failed_total:, sample_error: }
|
|
@@ -143,15 +143,24 @@ into: nil
|
|
|
143
143
|
# Perform a full reindex for a referencer collection, honoring partitioning
|
|
144
144
|
# directives when present. Falls back to a single non-partitioned rebuild
|
|
145
145
|
# when no partitions are configured.
|
|
146
|
+
#
|
|
147
|
+
# Strategy:
|
|
148
|
+
# 1. Try safe blue/green rebuild via index_collection(force_rebuild: true).
|
|
149
|
+
# 2. If that fails, fall through to partition-based import into the existing
|
|
150
|
+
# collection. This is non-destructive: the collection stays available
|
|
151
|
+
# with its current schema while documents are refreshed.
|
|
152
|
+
#
|
|
153
|
+
# The previous destructive fallback (drop + recreate) was removed because
|
|
154
|
+
# it caused availability gaps — the collection disappears entirely while
|
|
155
|
+
# being rebuilt, and if the rebuild fails, it stays gone.
|
|
156
|
+
#
|
|
146
157
|
# @param ref_klass [Class]
|
|
147
|
-
# @return [
|
|
148
|
-
# rubocop:disable Metrics/AbcSize
|
|
158
|
+
# @return [Boolean]
|
|
149
159
|
def __se_full_reindex_for_referrer(ref_klass, client:, alias_cache:)
|
|
150
160
|
logical = ref_klass.respond_to?(:collection) ? ref_klass.collection.to_s : ref_klass.name.to_s
|
|
151
161
|
physical = resolve_physical_collection_name(logical, client: client, cache: alias_cache)
|
|
162
|
+
coll_display = physical && physical != logical ? "#{logical} (physical: #{physical})" : logical
|
|
152
163
|
|
|
153
|
-
# For cascade full reindex, force a schema rebuild (blue/green) to
|
|
154
|
-
# refresh reference targets before importing documents.
|
|
155
164
|
forced = reindex_referencer_with_fresh_schema!(
|
|
156
165
|
ref_klass,
|
|
157
166
|
logical,
|
|
@@ -161,10 +170,6 @@ into: nil
|
|
|
161
170
|
)
|
|
162
171
|
return true if forced
|
|
163
172
|
|
|
164
|
-
# Fallback: force full destructive reindex when forced rebuild fails.
|
|
165
|
-
dropped = reindex_referencer_with_drop!(ref_klass, logical, physical)
|
|
166
|
-
return true if dropped
|
|
167
|
-
|
|
168
173
|
begin
|
|
169
174
|
compiled = SearchEngine::Partitioner.for(ref_klass)
|
|
170
175
|
rescue StandardError
|
|
@@ -183,12 +188,10 @@ into: nil
|
|
|
183
188
|
parts = parts.reject { |p| p.nil? || p.to_s.strip.empty? }
|
|
184
189
|
|
|
185
190
|
if parts.empty?
|
|
186
|
-
coll_display = physical && physical != logical ? "#{logical} (physical: #{physical})" : logical
|
|
187
191
|
puts(SearchEngine::Logging::Color.dim(%( Referencer "#{coll_display}" — partitions=0 → skip)))
|
|
188
192
|
return false
|
|
189
193
|
end
|
|
190
194
|
|
|
191
|
-
coll_display = physical && physical != logical ? "#{logical} (physical: #{physical})" : logical
|
|
192
195
|
parts_str = SearchEngine::Logging::Color.bold("partitions=#{parts.size}")
|
|
193
196
|
puts(%( Referencer "#{coll_display}" — #{parts_str} parallel=#{compiled.max_parallel}))
|
|
194
197
|
mp = compiled.max_parallel.to_i
|
|
@@ -207,14 +210,12 @@ into: nil
|
|
|
207
210
|
end
|
|
208
211
|
|
|
209
212
|
else
|
|
210
|
-
coll_display = physical && physical != logical ? "#{logical} (physical: #{physical})" : logical
|
|
211
213
|
puts(%( Referencer "#{coll_display}" — #{SearchEngine::Logging::Color.bold('single')}))
|
|
212
214
|
SearchEngine::Indexer.rebuild_partition!(ref_klass, partition: nil, into: nil)
|
|
213
215
|
executed = true
|
|
214
216
|
end
|
|
215
217
|
executed
|
|
216
218
|
end
|
|
217
|
-
# rubocop:enable Metrics/AbcSize
|
|
218
219
|
|
|
219
220
|
# Resolve logical alias to physical name with optional per-run memoization.
|
|
220
221
|
# @param logical [String]
|
|
@@ -343,21 +344,6 @@ into: nil
|
|
|
343
344
|
false
|
|
344
345
|
end
|
|
345
346
|
|
|
346
|
-
def reindex_referencer_with_drop!(ref_klass, logical, physical)
|
|
347
|
-
coll_display = physical && physical != logical ? "#{logical} (physical: #{physical})" : logical
|
|
348
|
-
status_word = SearchEngine::Logging::Color.apply('force reindex (drop+index)', :yellow)
|
|
349
|
-
puts(%( Referencer "#{coll_display}" — #{status_word}))
|
|
350
|
-
|
|
351
|
-
SearchEngine::Instrumentation.with_context(bulk_suppress_cascade: true) do
|
|
352
|
-
ref_klass.reindex_collection!
|
|
353
|
-
end
|
|
354
|
-
true
|
|
355
|
-
rescue StandardError => error
|
|
356
|
-
err_line = %( Referencer "#{logical}" — force reindex failed: #{error.message})
|
|
357
|
-
puts(SearchEngine::Logging::Color.apply(err_line, :red))
|
|
358
|
-
false
|
|
359
|
-
end
|
|
360
|
-
|
|
361
347
|
def post_partitions_to_pool!(pool, ctx, parts, ref_klass, mtx)
|
|
362
348
|
parts.each do |p|
|
|
363
349
|
pool.post do
|
|
@@ -57,6 +57,26 @@ module SearchEngine
|
|
|
57
57
|
instrument(:get, path, (start ? (current_monotonic_ms - start) : 0.0), {}, request_token: start)
|
|
58
58
|
end
|
|
59
59
|
|
|
60
|
+
# @param alias_name [String]
|
|
61
|
+
# @return [Hash] Typesense delete response, or { status: 404 } when alias not found
|
|
62
|
+
def delete_alias(alias_name)
|
|
63
|
+
a = alias_name.to_s
|
|
64
|
+
start = current_monotonic_ms
|
|
65
|
+
path = [Client::RequestBuilder::ALIASES_PREFIX, a].join
|
|
66
|
+
|
|
67
|
+
result = with_exception_mapping(:delete, path, {}, start) do
|
|
68
|
+
typesense.aliases[a].delete
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
symbolize_keys_deep(result)
|
|
72
|
+
rescue Errors::Api => error
|
|
73
|
+
return { status: 404 } if error.status.to_i == 404
|
|
74
|
+
|
|
75
|
+
raise
|
|
76
|
+
ensure
|
|
77
|
+
instrument(:delete, path, (start ? (current_monotonic_ms - start) : 0.0), {}, request_token: start)
|
|
78
|
+
end
|
|
79
|
+
|
|
60
80
|
# @param alias_name [String]
|
|
61
81
|
# @param physical_name [String]
|
|
62
82
|
# @return [Hash]
|
data/lib/search_engine/client.rb
CHANGED
|
@@ -61,6 +61,14 @@ module SearchEngine
|
|
|
61
61
|
services.fetch(:collections).retrieve_schema(collection_name, timeout_ms: timeout_ms)
|
|
62
62
|
end
|
|
63
63
|
|
|
64
|
+
# Delete an alias by name. Returns { status: 404 } when alias not found.
|
|
65
|
+
# @param alias_name [String]
|
|
66
|
+
# @return [Hash]
|
|
67
|
+
# @see `https://typesense.org/docs/latest/api/aliases.html#delete-an-alias`
|
|
68
|
+
def delete_alias(alias_name)
|
|
69
|
+
services.fetch(:collections).delete_alias(alias_name)
|
|
70
|
+
end
|
|
71
|
+
|
|
64
72
|
# Upsert an alias to point to the provided physical collection (atomic server-side swap).
|
|
65
73
|
# @param alias_name [String]
|
|
66
74
|
# @param physical_name [String]
|