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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6f539715e56d25adec8340f5ef92aa4897e8494e5dd1d17a19e9e974a440d8b9
4
- data.tar.gz: 8e15d07dfd0ac820a934ef8d53b7a36c3cd86c24b1c248fd2ef1922e8f8b9622
3
+ metadata.gz: 4596b142a6080ea8f9941f94611f32e9c461b3e9be51bb8e024e821c68ccd498
4
+ data.tar.gz: 0ea309bf811b27491de030021053c888d3c4fae0047292432a7f89a46f4ad722
5
5
  SHA512:
6
- metadata.gz: a9b03abc402eec4c55a781ad6f9dae88f7c1c3ffc6fd64ca5b6fba1be34202c394aceb62dc1ed9734b31bc3f9f2bdf55294a97c3c9c9db6a9440f5ba31fad28b
7
- data.tar.gz: cd8c090303502de20f4d6f0852f0c769540c2cedd0ee3b6423eb6afa44bdbd5bcf2c472a610935d7114153a8cada8e80763efab0813c86e90833470034c2c7f3
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.each do |p|
223
- summary = SearchEngine::Indexer.rebuild_partition!(self, partition: p, into: nil)
224
- summaries << summary
225
- puts(SearchEngine::Logging::PartitionProgress.line(p, summary))
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
- physical = if alias_target && !alias_target.to_s.strip.empty?
50
- alias_target.to_s
51
- else
52
- live = client.retrieve_collection_schema(logical, timeout_ms: 10_000)
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 physical.nil?
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
- step.update("dropping (logical=#{logical} physical=#{physical})")
65
- client.delete_collection(physical, timeout_ms: 60_000)
66
- step.finish('done')
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
- physical = if alias_target && !alias_target.to_s.strip.empty?
79
- alias_target.to_s
80
- else
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 physical
87
- step.update("dropping existing (logical=#{logical} physical=#{physical})")
88
- client.delete_collection(physical)
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
- summary = SearchEngine::Indexer.rebuild_partition!(self, partition: nil, into: into)
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 [void]
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]
@@ -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]
@@ -83,7 +83,7 @@ module SearchEngine
83
83
  enum: docs_enum,
84
84
  batch_size: nil,
85
85
  action: :upsert,
86
- log_batches: partition.nil?,
86
+ log_batches: partition.nil? && on_batch.nil?,
87
87
  max_parallel: max_parallel,
88
88
  on_batch: on_batch
89
89
  )
@@ -56,6 +56,10 @@ module SearchEngine
56
56
  nil
57
57
  end
58
58
 
59
+ def delete_alias(alias_name)
60
+ {}
61
+ end
62
+
59
63
  def upsert_alias(alias_name, physical_name)
60
64
  {}
61
65
  end
@@ -3,5 +3,5 @@
3
3
  module SearchEngine
4
4
  # Current gem version.
5
5
  # @return [String]
6
- VERSION = '30.1.6.11'
6
+ VERSION = '30.1.6.12'
7
7
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: search-engine-for-typesense
3
3
  version: !ruby/object:Gem::Version
4
- version: 30.1.6.11
4
+ version: 30.1.6.12
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nikita Shkoda