search-engine-for-typesense 30.1.6.3 → 30.1.6.4

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: 228f70410334d2a324508d0d44dfd31aadbcb4a957f28425909108490a3129a2
4
- data.tar.gz: 89fd3243af9aec91757d9819136ce9b0fd783845ff1ff6e6fe72467106d20ecb
3
+ metadata.gz: 46b318e4be1f0f8d12e5f3ead35ad4945c472773f8f7117427d1ffd2a0f44732
4
+ data.tar.gz: 4db7bc9cf0e081ff21b87c8b089627fe02e651020ad8a9df7fc0a3edb0cc7dea
5
5
  SHA512:
6
- metadata.gz: 42e556e2b25d40fd5ba85cb1ae1961223420d6affb6f532936642a48159dc013a8297398cba3724ef798f2cab05a0819140c9038d415b0035c7f9a95b2408e07
7
- data.tar.gz: ea3aba9866079ade74da2712d73f6e2a7169cc0a013dff5814ccbc7034789e9f2d6ae38979e49761210e46e4a61f329fde3ae9bb356f02b643b14899a0d24d9d
6
+ metadata.gz: cccc05756309485db9c5c49a880e45aa09a3f8f7e6afade62fd2d064eb3ccca286c96791aed830da01090ee10c60b6cf5c602269b55859b3466f76d3d22bd704
7
+ data.tar.gz: 112cbcd585dfeec20a848c9b153024fcb48526b4f56127a75ef45300693baf6ca666899f607f436e32889d50d2e7f1ad1b29d1daf6af6c5f59b1611db8d9b683
@@ -45,13 +45,13 @@ module SearchEngine
45
45
  def cleanup(into: nil, partition: nil, clear_cache: false)
46
46
  logical = respond_to?(:collection) ? collection.to_s : name.to_s
47
47
  puts
48
- puts(%(>>>>>> Cleanup Collection "#{logical}"))
48
+ puts(SearchEngine::Logging::Color.header(%(>>>>>> Cleanup Collection "#{logical}")))
49
49
 
50
50
  filters = SearchEngine::StaleRules.compile_filters(self, partition: partition)
51
51
  filters.compact!
52
52
  filters.reject! { |f| f.to_s.strip.empty? }
53
53
  if filters.empty?
54
- puts('Cleanup — skip (no stale configuration)')
54
+ puts(SearchEngine::Logging::Color.dim('Cleanup — skip (no stale configuration)'))
55
55
  return 0
56
56
  end
57
57
 
@@ -65,25 +65,23 @@ module SearchEngine
65
65
  partition: partition
66
66
  )
67
67
 
68
- puts("Cleanup — deleted=#{deleted}")
68
+ puts("Cleanup — #{SearchEngine::Logging::Color.apply("deleted=#{deleted}", :green)}")
69
69
  deleted
70
70
  rescue StandardError => error
71
- warn(
72
- "Cleanup — error=#{error.class}: #{error.message.to_s[0, 200]}"
73
- )
71
+ err_msg = "Cleanup — error=#{error.class}: #{error.message.to_s[0, 200]}"
72
+ warn(SearchEngine::Logging::Color.apply(err_msg, :red))
74
73
  0
75
74
  ensure
76
75
  if clear_cache
77
76
  begin
78
- puts('Cleanup — cache clear')
77
+ puts("Cleanup — #{SearchEngine::Logging::Color.bold('cache clear')}")
79
78
  SearchEngine::Cache.clear
80
79
  rescue StandardError => error
81
- warn(
82
- "Cleanup — cache clear error=#{error.class}: #{error.message.to_s[0, 200]}"
83
- )
80
+ err_msg = "Cleanup — cache clear error=#{error.class}: #{error.message.to_s[0, 200]}"
81
+ warn(SearchEngine::Logging::Color.apply(err_msg, :red))
84
82
  end
85
83
  end
86
- puts(%(>>>>>> Cleanup Done))
84
+ puts(SearchEngine::Logging::Color.header(%(>>>>>> Cleanup Done)))
87
85
  end
88
86
 
89
87
  private
@@ -17,7 +17,7 @@ module SearchEngine
17
17
  def index_collection(partition: nil, client: nil, pre: nil, force_rebuild: false)
18
18
  logical = respond_to?(:collection) ? collection.to_s : name.to_s
19
19
  puts
20
- puts(%(>>>>>> Indexing Collection "#{logical}"))
20
+ puts(SearchEngine::Logging::Color.header(%(>>>>>> Indexing Collection "#{logical}")))
21
21
  client_obj = client || SearchEngine.client
22
22
 
23
23
  result = if partition.nil?
@@ -56,7 +56,12 @@ module SearchEngine
56
56
 
57
57
  diff = SearchEngine::Schema.diff(self, client: client)[:diff] || {}
58
58
  missing = __se_schema_missing?(diff)
59
- puts("Step 1: Presence — processing → #{missing ? 'missing' : 'present'}")
59
+ presence = if missing
60
+ SearchEngine::Logging::Color.apply('missing', :yellow)
61
+ else
62
+ SearchEngine::Logging::Color.apply('present', :green)
63
+ end
64
+ puts("Step 1: Presence — #{SearchEngine::Logging::Color.bold('processing')} → #{presence}")
60
65
 
61
66
  applied, indexed_inside_apply = __se_full_apply_if_missing(client, missing)
62
67
  drift = __se_full_check_drift(diff, missing, force_rebuild)
@@ -76,43 +81,48 @@ module SearchEngine
76
81
  applied = false
77
82
  indexed_inside_apply = false
78
83
  if missing
79
- puts('Step 2: Create+Apply Schema — processing')
84
+ puts("Step 2: Create+Apply Schema — #{SearchEngine::Logging::Color.bold('processing')}")
80
85
  SearchEngine::Schema.apply!(self, client: client) do |new_physical|
81
86
  indexed_inside_apply = __se_index_partitions!(into: new_physical)
82
87
  end
83
88
  applied = true
84
- puts('Step 2: Create+Apply Schema — done')
89
+ puts("Step 2: Create+Apply Schema — #{SearchEngine::Logging::Color.apply('done', :green)}")
85
90
  else
86
- puts('Step 2: Create+Apply Schema — skip (collection present)')
91
+ puts(SearchEngine::Logging::Color.dim('Step 2: Create+Apply Schema — skip (collection present)'))
87
92
  end
88
93
  [applied, indexed_inside_apply]
89
94
  end
90
95
 
91
96
  def __se_full_check_drift(diff, missing, force_rebuild)
92
97
  unless missing
93
- puts('Step 3: Check Schema Status — processing')
98
+ puts("Step 3: Check Schema Status — #{SearchEngine::Logging::Color.bold('processing')}")
94
99
  drift = __se_schema_drift?(diff)
95
100
  if force_rebuild && !drift
96
- puts('Step 3: Check Schema Status — force_rebuild')
101
+ puts("Step 3: Check Schema Status — #{SearchEngine::Logging::Color.apply('force_rebuild', :yellow)}")
97
102
  return true
98
103
  end
99
- puts("Step 3: Check Schema Status — #{drift ? 'drift' : 'in_sync'}")
104
+ schema_status = if drift
105
+ SearchEngine::Logging::Color.apply('drift', :yellow)
106
+ else
107
+ SearchEngine::Logging::Color.apply('in_sync', :green)
108
+ end
109
+ puts("Step 3: Check Schema Status — #{schema_status}")
100
110
  return drift
101
111
  end
102
- puts('Step 3: Check Schema Status — skip (just created)')
112
+ puts(SearchEngine::Logging::Color.dim('Step 3: Check Schema Status — skip (just created)'))
103
113
  false
104
114
  end
105
115
 
106
116
  def __se_full_apply_if_drift(client, drift, applied, indexed_inside_apply, force_rebuild)
107
117
  if drift
108
- puts('Step 4: Apply New Schema — processing')
118
+ puts("Step 4: Apply New Schema — #{SearchEngine::Logging::Color.bold('processing')}")
109
119
  SearchEngine::Schema.apply!(self, client: client, force_rebuild: force_rebuild) do |new_physical|
110
120
  indexed_inside_apply = __se_index_partitions!(into: new_physical)
111
121
  end
112
122
  applied = true
113
- puts('Step 4: Apply New Schema — done')
123
+ puts("Step 4: Apply New Schema — #{SearchEngine::Logging::Color.apply('done', :green)}")
114
124
  else
115
- puts('Step 4: Apply New Schema — skip')
125
+ puts(SearchEngine::Logging::Color.dim('Step 4: Apply New Schema — skip'))
116
126
  end
117
127
  [applied, indexed_inside_apply]
118
128
  end
@@ -120,12 +130,12 @@ module SearchEngine
120
130
  def __se_full_indexation(applied, indexed_inside_apply)
121
131
  result = nil
122
132
  if applied && indexed_inside_apply
123
- puts('Step 5: Indexing — skip (performed during schema apply)')
133
+ puts(SearchEngine::Logging::Color.dim('Step 5: Indexing — skip (performed during schema apply)'))
124
134
  result = indexed_inside_apply if indexed_inside_apply.is_a?(Hash)
125
135
  else
126
- puts('Step 5: Indexing — processing')
136
+ puts("Step 5: Indexing — #{SearchEngine::Logging::Color.bold('processing')}")
127
137
  result = __se_index_partitions!(into: nil)
128
- puts('Step 5: Indexing — done')
138
+ puts("Step 5: Indexing — #{SearchEngine::Logging::Color.apply('done', :green)}")
129
139
  end
130
140
 
131
141
  cascade_ok = result.is_a?(Hash) ? result[:status] == :ok : false
@@ -135,11 +145,12 @@ module SearchEngine
135
145
 
136
146
  def __se_full_retention(applied, logical, client)
137
147
  if applied
138
- puts('Step 6: Retention Cleanup — skip (handled by schema apply)')
148
+ puts(SearchEngine::Logging::Color.dim('Step 6: Retention Cleanup — skip (handled by schema apply)'))
139
149
  else
140
- puts('Step 6: Retention Cleanup — processing')
150
+ puts("Step 6: Retention Cleanup — #{SearchEngine::Logging::Color.bold('processing')}")
141
151
  dropped = __se_retention_cleanup!(logical: logical, client: client)
142
- puts("Step 6: Retention Cleanup — dropped=#{dropped.inspect}")
152
+ dropped_str = SearchEngine::Logging::Color.apply("dropped=#{dropped.inspect}", :green)
153
+ puts("Step 6: Retention Cleanup — #{dropped_str}")
143
154
  end
144
155
  end
145
156
 
@@ -149,32 +160,43 @@ module SearchEngine
149
160
  diff = diff_res[:diff] || {}
150
161
 
151
162
  missing = __se_schema_missing?(diff)
152
- puts("Step 1: Presence — processing → #{missing ? 'missing' : 'present'}")
163
+ presence = if missing
164
+ SearchEngine::Logging::Color.apply('missing', :yellow)
165
+ else
166
+ SearchEngine::Logging::Color.apply('present', :green)
167
+ end
168
+ puts("Step 1: Presence — #{SearchEngine::Logging::Color.bold('processing')} → #{presence}")
153
169
  if missing
154
- puts('Partial: collection is not present. Quit early.')
170
+ msg = SearchEngine::Logging::Color.apply(
171
+ 'Step 1: Partial — collection is not present. Quit early.', :yellow
172
+ )
173
+ puts(msg)
155
174
  return { status: :failed, docs_total: 0, success_total: 0, failed_total: 0,
156
175
  sample_error: 'Collection not present' }
157
176
  end
158
177
 
159
- puts('Step 2: Check Schema Status — processing')
178
+ puts("Step 2: Check Schema Status — #{SearchEngine::Logging::Color.bold('processing')}")
160
179
  drift = __se_schema_drift?(diff)
161
180
  if drift
162
- puts('Partial: schema is not up-to-date. Exit early (run full indexing).')
181
+ msg = SearchEngine::Logging::Color.apply(
182
+ 'Step 2: Partial — schema is not up-to-date. Exit early (run full indexing).', :yellow
183
+ )
184
+ puts(msg)
163
185
  return { status: :failed, docs_total: 0, success_total: 0, failed_total: 0,
164
186
  sample_error: 'Schema drift detected' }
165
187
  end
166
- puts('Step 2: Check Schema Status — in_sync')
188
+ puts("Step 2: Check Schema Status — #{SearchEngine::Logging::Color.apply('in_sync', :green)}")
167
189
 
168
190
  __se_preflight_dependencies!(mode: pre, client: client) if pre
169
191
 
170
- puts('Step 3: Partial Indexing — processing')
192
+ puts("Step 3: Partial Indexing — #{SearchEngine::Logging::Color.bold('processing')}")
171
193
  summaries = []
172
194
  partitions.each do |p|
173
195
  summary = SearchEngine::Indexer.rebuild_partition!(self, partition: p, into: nil)
174
196
  summaries << summary
175
197
  puts(SearchEngine::Logging::PartitionProgress.line(p, summary))
176
198
  end
177
- puts('Step 3: Partial Indexing — done')
199
+ puts("Step 3: Partial Indexing — #{SearchEngine::Logging::Color.apply('done', :green)}")
178
200
 
179
201
  result = __se_build_index_result(summaries)
180
202
  __se_cascade_after_indexation!(context: :full) if result[:status] == :ok
@@ -185,34 +207,34 @@ module SearchEngine
185
207
  def __se_cascade_after_indexation!(context: :full)
186
208
  if SearchEngine::Instrumentation.context&.[](:bulk_suppress_cascade)
187
209
  puts
188
- puts('>>>>>> Cascade Referencers — suppressed (bulk)')
210
+ puts(SearchEngine::Logging::Color.dim('>>>>>> Cascade Referencers — suppressed (bulk)'))
189
211
  return
190
212
  end
191
213
  puts
192
- puts(%(>>>>>> Cascade Referencers))
214
+ puts(SearchEngine::Logging::Color.header(%(>>>>>> Cascade Referencers)))
193
215
  results = SearchEngine::Cascade.cascade_reindex!(source: self, ids: nil, context: context)
194
216
  outcomes = Array(results[:outcomes])
195
217
  if outcomes.empty?
196
- puts(' none')
218
+ puts(SearchEngine::Logging::Color.dim(' none'))
197
219
  else
198
220
  outcomes.each do |o|
199
221
  coll = o[:collection] || o['collection']
200
222
  mode = (o[:mode] || o['mode']).to_s
201
223
  case mode
202
224
  when 'partial'
203
- puts(%( Referencer "#{coll}" → partial reindex))
225
+ puts(%( Referencer "#{coll}" → #{SearchEngine::Logging::Color.apply('partial reindex', :green)}))
204
226
  when 'full'
205
- puts(%( Referencer "#{coll}" → full reindex))
227
+ puts(%( Referencer "#{coll}" → #{SearchEngine::Logging::Color.apply('full reindex', :green)}))
206
228
  when 'skipped_unregistered'
207
- puts(%( Referencer "#{coll}" → skipped (unregistered)))
229
+ puts(SearchEngine::Logging::Color.dim(%( Referencer "#{coll}" → skipped (unregistered))))
208
230
  when 'skipped_cycle'
209
- puts(%( Referencer "#{coll}" → skipped (cycle)))
231
+ puts(SearchEngine::Logging::Color.dim(%( Referencer "#{coll}" → skipped (cycle))))
210
232
  else
211
233
  puts(%( Referencer "#{coll}" → #{mode}))
212
234
  end
213
235
  end
214
236
  end
215
- puts('>>>>>> Cascade Done')
237
+ puts(SearchEngine::Logging::Color.header('>>>>>> Cascade Done'))
216
238
  rescue StandardError => error
217
239
  base = "Cascade — error=#{error.class}: #{error.message.to_s[0, 200]}"
218
240
  if error.respond_to?(:status) || error.respond_to?(:body)
@@ -233,9 +255,12 @@ module SearchEngine
233
255
  rescue StandardError
234
256
  nil
235
257
  end
236
- warn([base, ("status=#{status}" if status), ("body=#{body_preview}" if body_preview)].compact.join(' '))
258
+ err_parts = [base]
259
+ err_parts << "status=#{status}" if status
260
+ err_parts << "body=#{body_preview}" if body_preview
261
+ warn(SearchEngine::Logging::Color.apply(err_parts.compact.join(' '), :red))
237
262
  else
238
- warn(base)
263
+ warn(SearchEngine::Logging::Color.apply(base, :red))
239
264
  end
240
265
  end
241
266
  # rubocop:enable Metrics/PerceivedComplexity, Metrics/AbcSize
@@ -28,13 +28,18 @@ module SearchEngine
28
28
  def update_collection!
29
29
  client = SearchEngine.client
30
30
 
31
- puts 'Update Collection — analyzing diff for in-place update...'
31
+ status_word = SearchEngine::Logging::Color.bold('analyzing diff for in-place update...')
32
+ puts "Update Collection — #{status_word}"
32
33
  updated = SearchEngine::Schema.update!(self, client: client)
33
34
 
34
35
  if updated
35
- puts 'Update Collection — schema updated in-place (PATCH)'
36
+ status_word = SearchEngine::Logging::Color.apply('schema updated in-place (PATCH)', :green)
37
+ puts "Update Collection — #{status_word}"
36
38
  else
37
- puts 'Update Collection — in-place update not possible (no changes or incompatible)'
39
+ msg = SearchEngine::Logging::Color.dim(
40
+ 'Update Collection — in-place update not possible (no changes or incompatible)'
41
+ )
42
+ puts(msg)
38
43
  end
39
44
  updated
40
45
  end
@@ -53,17 +58,19 @@ module SearchEngine
53
58
  end
54
59
 
55
60
  if physical.nil?
56
- puts('Drop Collection — skip (not present)')
61
+ puts(SearchEngine::Logging::Color.dim('Drop Collection — skip (not present)'))
57
62
  return
58
63
  end
59
64
 
60
65
  puts
61
- puts(%(>>>>>> Dropping Collection "#{logical}"))
62
- puts("Drop Collection — processing (logical=#{logical} physical=#{physical})")
66
+ header = SearchEngine::Logging::Color.header(%(>>>>>> Dropping Collection "#{logical}"))
67
+ puts(header)
68
+ status_word = SearchEngine::Logging::Color.bold('processing')
69
+ puts("Drop Collection — #{status_word} (logical=#{logical} physical=#{physical})")
63
70
  # Use an extended timeout to accommodate large collection drops
64
71
  client.delete_collection(physical, timeout_ms: 60_000)
65
- puts('Drop Collection — done')
66
- puts(%(>>>>>> Dropped Collection "#{logical}"))
72
+ puts("Drop Collection — #{SearchEngine::Logging::Color.apply('done', :green)}")
73
+ puts(SearchEngine::Logging::Color.header(%(>>>>>> Dropped Collection "#{logical}")))
67
74
  nil
68
75
  end
69
76
 
@@ -80,16 +87,19 @@ module SearchEngine
80
87
  end
81
88
 
82
89
  if physical
83
- puts("Recreate Collection dropping existing (logical=#{logical} physical=#{physical})")
90
+ status_word = SearchEngine::Logging::Color.apply('dropping existing', :yellow)
91
+ puts("Recreate Collection — #{status_word} (logical=#{logical} physical=#{physical})")
84
92
  client.delete_collection(physical)
85
93
  else
86
- puts('Recreate Collection — no existing collection (skip drop)')
94
+ msg = SearchEngine::Logging::Color.dim('Recreate Collection — no existing collection (skip drop)')
95
+ puts(msg)
87
96
  end
88
97
 
89
98
  schema = SearchEngine::Schema.compile(self)
90
- puts("Recreate Collection creating collection with schema (logical=#{logical})")
99
+ status_word = SearchEngine::Logging::Color.bold('creating collection with schema')
100
+ puts("Recreate Collection — #{status_word} (logical=#{logical})")
91
101
  client.create_collection(schema)
92
- puts('Recreate Collection — done')
102
+ puts("Recreate Collection — #{SearchEngine::Logging::Color.apply('done', :green)}")
93
103
  nil
94
104
  end
95
105
 
@@ -5,6 +5,7 @@ require 'search_engine/base/index_maintenance/cleanup'
5
5
  require 'search_engine/base/index_maintenance/lifecycle'
6
6
  require 'search_engine/base/index_maintenance/schema'
7
7
  require 'search_engine/logging/color'
8
+ require 'search_engine/logging/batch_line'
8
9
 
9
10
  module SearchEngine
10
11
  class Base
@@ -41,7 +42,10 @@ module SearchEngine
41
42
 
42
43
  indent = ' ' * depth
43
44
  puts if depth.zero?
44
- puts(%(#{indent}>>>>>> Preflight Dependencies (mode: #{mode}, collection: "#{current}")))
45
+ header = SearchEngine::Logging::Color.header(
46
+ %(#{indent}>>>>>> Preflight Dependencies (mode: #{mode}, collection: "#{current}"))
47
+ )
48
+ puts(header)
45
49
 
46
50
  deps.each do |cfg|
47
51
  dep_coll = (cfg[:collection] || cfg['collection']).to_s
@@ -50,7 +54,7 @@ module SearchEngine
50
54
  dep_klass = __se_resolve_dep_class(dep_coll)
51
55
 
52
56
  if dep_klass.nil?
53
- puts(%(#{indent} "#{dep_coll}" → skipped (unregistered)))
57
+ puts(SearchEngine::Logging::Color.dim(%(#{indent} "#{dep_coll}" → skipped (unregistered))))
54
58
  visited.add(dep_coll)
55
59
  next
56
60
  end
@@ -72,7 +76,7 @@ module SearchEngine
72
76
  visited.add(dep_coll)
73
77
  end
74
78
 
75
- puts(%(#{indent}>>>>>> Preflight Done (collection: "#{current}")))
79
+ puts(SearchEngine::Logging::Color.header(%(#{indent}>>>>>> Preflight Done (collection: "#{current}"))))
76
80
  end
77
81
 
78
82
  # @return [String] current collection logical name; empty string when unavailable
@@ -165,27 +169,29 @@ module SearchEngine
165
169
  case mode.to_s
166
170
  when 'ensure'
167
171
  if missing
168
- puts(%(#{indent}"#{dep_coll}" ensure (missing) → index_collection))
172
+ status_word = SearchEngine::Logging::Color.apply('ensure (missing)', :yellow)
173
+ puts(%(#{indent}"#{dep_coll}" → #{status_word} → index_collection))
169
174
  # Avoid nested preflight to prevent redundant recursion cycles
170
175
  SearchEngine::Instrumentation.with_context(bulk_suppress_cascade: true) do
171
176
  dep_klass.index_collection(client: client)
172
177
  end
173
178
  else
174
- puts(%(#{indent}"#{dep_coll}" → present (skip)))
179
+ puts(SearchEngine::Logging::Color.dim(%(#{indent}"#{dep_coll}" → present (skip))))
175
180
  end
176
181
  when 'index'
177
182
  if missing || drift
178
183
  reason = missing ? 'missing' : 'drift'
179
- puts(%(#{indent}"#{dep_coll}" → index (#{reason}) → index_collection))
184
+ status_word = SearchEngine::Logging::Color.apply("index (#{reason})", :yellow)
185
+ puts(%(#{indent}"#{dep_coll}" → #{status_word} → index_collection))
180
186
  # Avoid nested preflight to prevent redundant recursion cycles
181
187
  SearchEngine::Instrumentation.with_context(bulk_suppress_cascade: true) do
182
188
  dep_klass.index_collection(client: client)
183
189
  end
184
190
  else
185
- puts(%(#{indent}"#{dep_coll}" → in_sync (skip)))
191
+ puts(SearchEngine::Logging::Color.dim(%(#{indent}"#{dep_coll}" → in_sync (skip))))
186
192
  end
187
193
  else
188
- puts(%(#{indent}"#{dep_coll}" → skipped (unknown mode: #{mode})))
194
+ puts(SearchEngine::Logging::Color.dim(%(#{indent}"#{dep_coll}" → skipped (unknown mode: #{mode}))))
189
195
  end
190
196
  end
191
197
 
@@ -193,61 +199,10 @@ module SearchEngine
193
199
  return unless batches.is_a?(Array)
194
200
 
195
201
  batches.each_with_index do |batch_stats, idx|
196
- batch_number = idx + 1
197
- batch_status = __se_batch_status_from_stats(batch_stats)
198
- status_color = SearchEngine::Logging::Color.for_status(batch_status)
199
-
200
- prefix = batch_number == 1 ? ' single → ' : ' '
201
- line = +prefix
202
- line << SearchEngine::Logging::Color.apply("status=#{batch_status}", status_color) << ' '
203
- docs_count = batch_stats[:docs_count] || batch_stats['docs_count'] || 0
204
- line << "docs=#{docs_count}" << ' '
205
- success_count = (batch_stats[:success_count] || batch_stats['success_count'] || 0).to_i
206
- success_str = "success=#{success_count}"
207
- line << (
208
- success_count.positive? ? SearchEngine::Logging::Color.bold(success_str) : success_str
209
- ) << ' '
210
- failed_count = (batch_stats[:failure_count] || batch_stats['failure_count'] || 0).to_i
211
- failed_str = "failed=#{failed_count}"
212
- line << (
213
- failed_count.positive? ? SearchEngine::Logging::Color.apply(failed_str, :red) : failed_str
214
- ) << ' '
215
- line << "batch=#{batch_number} "
216
- duration_ms = batch_stats[:duration_ms] || batch_stats['duration_ms'] || 0.0
217
- line << "duration_ms=#{duration_ms}"
218
-
219
- # Extract sample error from batch stats
220
- sample_err = __se_extract_batch_sample_error(batch_stats)
221
- line << " sample_error=#{sample_err.inspect}" if sample_err
222
-
223
- puts(line)
202
+ puts(SearchEngine::Logging::BatchLine.format(batch_stats, idx + 1, indifferent: true))
224
203
  end
225
204
  end
226
205
 
227
- def __se_batch_status_from_stats(stats)
228
- success_count = (stats[:success_count] || stats['success_count'] || 0).to_i
229
- failure_count = (stats[:failure_count] || stats['failure_count'] || 0).to_i
230
-
231
- if failure_count.positive? && success_count.positive?
232
- :partial
233
- elsif failure_count.positive?
234
- :failed
235
- else
236
- :ok
237
- end
238
- end
239
-
240
- def __se_extract_batch_sample_error(stats)
241
- samples = stats[:errors_sample] || stats['errors_sample']
242
- return nil unless samples.is_a?(Array) && samples.any?
243
-
244
- samples.each do |msg|
245
- s = msg.to_s
246
- return s unless s.strip.empty?
247
- end
248
- nil
249
- end
250
-
251
206
  private :__se_current_collection_name,
252
207
  :__se_fetch_joins_config,
253
208
  :__se_belongs_to_dependencies,
@@ -257,9 +212,7 @@ module SearchEngine
257
212
  :__se_diff_for,
258
213
  :__se_dependency_status,
259
214
  :__se_handle_preflight_action,
260
- :__se_log_batches_from_summary,
261
- :__se_batch_status_from_stats,
262
- :__se_extract_batch_sample_error
215
+ :__se_log_batches_from_summary
263
216
  end
264
217
 
265
218
  class_methods do
@@ -390,7 +343,8 @@ module SearchEngine
390
343
  end
391
344
  rescue StandardError => error
392
345
  mtx.synchronize do
393
- warn(" partition=#{part.inspect} → error=#{error.class}: #{error.message.to_s[0, 200]}")
346
+ err_msg = " partition=#{part.inspect} → error=#{error.class}: #{error.message.to_s[0, 200]}"
347
+ warn(SearchEngine::Logging::Color.apply(err_msg, :red))
394
348
  partition_errors << "#{error.class}: #{error.message.to_s[0, 200]}"
395
349
  end
396
350
  end
@@ -145,7 +145,7 @@ into: nil
145
145
  # when no partitions are configured.
146
146
  # @param ref_klass [Class]
147
147
  # @return [void]
148
- # rubocop:disable Metrics/PerceivedComplexity
148
+ # rubocop:disable Metrics/PerceivedComplexity, Metrics/AbcSize
149
149
  def __se_full_reindex_for_referrer(ref_klass, client:, alias_cache:)
150
150
  logical = ref_klass.respond_to?(:collection) ? ref_klass.collection.to_s : ref_klass.name.to_s
151
151
  physical = resolve_physical_collection_name(logical, client: client, cache: alias_cache)
@@ -184,12 +184,13 @@ into: nil
184
184
 
185
185
  if parts.empty?
186
186
  coll_display = physical && physical != logical ? "#{logical} (physical: #{physical})" : logical
187
- puts(%( Referencer "#{coll_display}" — partitions=0 → skip))
187
+ puts(SearchEngine::Logging::Color.dim(%( Referencer "#{coll_display}" — partitions=0 → skip)))
188
188
  return false
189
189
  end
190
190
 
191
191
  coll_display = physical && physical != logical ? "#{logical} (physical: #{physical})" : logical
192
- puts(%( Referencer "#{coll_display}" — partitions=#{parts.size} parallel=#{compiled.max_parallel}))
192
+ parts_str = SearchEngine::Logging::Color.bold("partitions=#{parts.size}")
193
+ puts(%( Referencer "#{coll_display}" — #{parts_str} parallel=#{compiled.max_parallel}))
193
194
  mp = compiled.max_parallel.to_i
194
195
  if mp > 1 && parts.size > 1
195
196
  require 'concurrent-ruby'
@@ -211,13 +212,13 @@ into: nil
211
212
 
212
213
  else
213
214
  coll_display = physical && physical != logical ? "#{logical} (physical: #{physical})" : logical
214
- puts(%( Referencer "#{coll_display}" — single))
215
+ puts(%( Referencer "#{coll_display}" — #{SearchEngine::Logging::Color.bold('single')}))
215
216
  SearchEngine::Indexer.rebuild_partition!(ref_klass, partition: nil, into: nil)
216
217
  executed = true
217
218
  end
218
219
  executed
219
220
  end
220
- # rubocop:enable Metrics/PerceivedComplexity
221
+ # rubocop:enable Metrics/PerceivedComplexity, Metrics/AbcSize
221
222
 
222
223
  # Resolve logical alias to physical name with optional per-run memoization.
223
224
  # @param logical [String]
@@ -333,27 +334,31 @@ into: nil
333
334
  def reindex_referencer_with_fresh_schema!(ref_klass, logical, physical, client:, force_rebuild: false)
334
335
  coll_display = physical && physical != logical ? "#{logical} (physical: #{physical})" : logical
335
336
  action = force_rebuild ? 'force_rebuild index_collection' : 'index_collection'
336
- puts(%( Referencer "#{coll_display}" — schema rebuild required, running #{action}))
337
+ status_word = SearchEngine::Logging::Color.apply("schema rebuild required, running #{action}", :yellow)
338
+ puts(%( Referencer "#{coll_display}" — #{status_word}))
337
339
 
338
340
  SearchEngine::Instrumentation.with_context(bulk_suppress_cascade: true) do
339
341
  ref_klass.index_collection(client: client, pre: :ensure, force_rebuild: force_rebuild)
340
342
  end
341
343
  true
342
344
  rescue StandardError => error
343
- puts(%( Referencer "#{logical}" — schema rebuild failed: #{error.message}))
345
+ err_line = %( Referencer "#{logical}" — schema rebuild failed: #{error.message})
346
+ puts(SearchEngine::Logging::Color.apply(err_line, :red))
344
347
  false
345
348
  end
346
349
 
347
350
  def reindex_referencer_with_drop!(ref_klass, logical, physical)
348
351
  coll_display = physical && physical != logical ? "#{logical} (physical: #{physical})" : logical
349
- puts(%( Referencer "#{coll_display}" force reindex (drop+index)))
352
+ status_word = SearchEngine::Logging::Color.apply('force reindex (drop+index)', :yellow)
353
+ puts(%( Referencer "#{coll_display}" — #{status_word}))
350
354
 
351
355
  SearchEngine::Instrumentation.with_context(bulk_suppress_cascade: true) do
352
356
  ref_klass.reindex_collection!
353
357
  end
354
358
  true
355
359
  rescue StandardError => error
356
- puts(%( Referencer "#{logical}" — force reindex failed: #{error.message}))
360
+ err_line = %( Referencer "#{logical}" — force reindex failed: #{error.message})
361
+ puts(SearchEngine::Logging::Color.apply(err_line, :red))
357
362
  false
358
363
  end
359
364
 
@@ -553,10 +553,8 @@ module SearchEngine
553
553
  started = monotonic_ms
554
554
  callable.call
555
555
  rescue StandardError => error
556
- puts "error: #{error.backtrace.join("\n")}"
557
- puts '--------------------------------'
558
- puts '--------------------------------'
559
- puts '--------------------------------'
556
+ puts SearchEngine::Cli::Support.fmt_err("#{error.class}: #{error.message}")
557
+ puts SearchEngine::Cli::Support.fmt_err(error.backtrace.first(5).join("\n"))
560
558
  failure(callable.name, started, error, hint: 'Unexpected error', doc: nil)
561
559
  end
562
560
  end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'search_engine/logging/color'
4
+ require 'search_engine/logging/batch_line'
4
5
 
5
6
  module SearchEngine
6
7
  class Indexer
@@ -141,7 +142,7 @@ module SearchEngine
141
142
  shared_state = initialize_shared_state
142
143
  producer_error = nil
143
144
 
144
- puts(' Starting parallel batch processing...') if log_batches
145
+ puts(SearchEngine::Logging::Color.dim(' Starting parallel batch processing...')) if log_batches
145
146
  started_at = monotonic_ms
146
147
 
147
148
  # Producer thread: fetch batches lazily and push to queue
@@ -154,15 +155,17 @@ module SearchEngine
154
155
  next unless log_batches && (batch_count % 10).zero?
155
156
 
156
157
  elapsed = (monotonic_ms - started_at).round(1)
157
- if total_batches_estimate
158
- puts(" Processed #{batch_count}/#{total_batches_estimate} batches... (#{elapsed}ms)")
159
- else
160
- puts(" Processed #{batch_count} batches... (#{elapsed}ms)")
161
- end
158
+ progress = if total_batches_estimate
159
+ " Processed #{batch_count}/#{total_batches_estimate} batches... (#{elapsed}ms)"
160
+ else
161
+ " Processed #{batch_count} batches... (#{elapsed}ms)"
162
+ end
163
+ puts(SearchEngine::Logging::Color.dim(progress))
162
164
  end
163
165
  rescue StandardError => error
164
166
  producer_error = error
165
- warn(" Producer failed at batch #{batch_count}: #{error.class}: #{error.message.to_s[0, 200]}")
167
+ err_msg = " Producer failed at batch #{batch_count}: #{error.class}: #{error.message.to_s[0, 200]}"
168
+ warn(SearchEngine::Logging::Color.apply(err_msg, :red))
166
169
  ensure
167
170
  # Signal completion to all workers
168
171
  max_parallel.times { batch_queue.push(sentinel) }
@@ -299,7 +302,8 @@ module SearchEngine
299
302
  failure_stat = failure_stats(thread_idx, docs_count, 0, error)
300
303
 
301
304
  shared_state[:mtx].synchronize do
302
- warn(" batch_index=#{thread_idx} → error=#{error.class}: #{error.message.to_s[0, 200]}")
305
+ err_msg = " batch_index=#{thread_idx} → error=#{error.class}: #{error.message.to_s[0, 200]}"
306
+ warn(SearchEngine::Logging::Color.apply(err_msg, :red))
303
307
  aggregate_stats([failure_stat], shared_state, batch_size, log_batches)
304
308
  end
305
309
  end
@@ -572,53 +576,7 @@ module SearchEngine
572
576
  end
573
577
 
574
578
  def log_batch(stats, batch_number)
575
- batch_status = batch_status_from_stats(stats)
576
- status_color = SearchEngine::Logging::Color.for_status(batch_status)
577
-
578
- prefix = batch_number == 1 ? ' single → ' : ' '
579
- line = +prefix
580
- line << SearchEngine::Logging::Color.apply("status=#{batch_status}", status_color) << ' '
581
- line << "docs=#{stats[:docs_count]}" << ' '
582
- success_count = stats[:success_count].to_i
583
- success_str = "success=#{success_count}"
584
- line << (
585
- success_count.positive? ? SearchEngine::Logging::Color.bold(success_str) : success_str
586
- ) << ' '
587
- failed_count = stats[:failure_count].to_i
588
- failed_str = "failed=#{failed_count}"
589
- line << (failed_count.positive? ? SearchEngine::Logging::Color.apply(failed_str, :red) : failed_str) << ' '
590
- line << "batch=#{batch_number} "
591
- line << "duration_ms=#{stats[:duration_ms]}"
592
-
593
- # Extract sample error from batch stats
594
- sample_err = extract_batch_sample_error(stats)
595
- line << " sample_error=#{sample_err.inspect}" if sample_err
596
-
597
- puts(line)
598
- end
599
-
600
- def extract_batch_sample_error(stats)
601
- samples = stats[:errors_sample] || stats['errors_sample']
602
- return nil unless samples.is_a?(Array) && samples.any?
603
-
604
- samples.each do |msg|
605
- s = msg.to_s
606
- return s unless s.strip.empty?
607
- end
608
- nil
609
- end
610
-
611
- def batch_status_from_stats(stats)
612
- success_count = stats[:success_count].to_i
613
- failure_count = stats[:failure_count].to_i
614
-
615
- if failure_count.positive? && success_count.positive?
616
- :partial
617
- elsif failure_count.positive?
618
- :failed
619
- else
620
- :ok
621
- end
579
+ puts(SearchEngine::Logging::BatchLine.format(stats, batch_number))
622
580
  end
623
581
  end
624
582
  end
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SearchEngine
4
+ module Logging
5
+ # Shared formatter for per-batch log lines during indexation.
6
+ #
7
+ # Produces the aligned `single →` / continuation lines used by both
8
+ # {SearchEngine::Base::IndexMaintenance} and {SearchEngine::Indexer::BulkImport}.
9
+ #
10
+ # @since M8
11
+ module BatchLine
12
+ FIRST_PREFIX = ' single → '
13
+ CONTINUATION_PREFIX = ' '
14
+
15
+ module_function
16
+
17
+ # Format a single batch log line.
18
+ #
19
+ # @param stats [Hash] batch statistics with :docs_count, :success_count, :failure_count, :duration_ms, :errors_sample
20
+ # @param batch_number [Integer] 1-based batch index
21
+ # @param indifferent [Boolean] when true, also check string keys (for summary hashes)
22
+ # @return [String]
23
+ def format(stats, batch_number, indifferent: false)
24
+ require 'search_engine/logging/color'
25
+
26
+ batch_status = status_from_counts(stats, indifferent: indifferent)
27
+ status_color = SearchEngine::Logging::Color.for_status(batch_status)
28
+
29
+ prefix = batch_number == 1 ? FIRST_PREFIX : CONTINUATION_PREFIX
30
+ line = +prefix
31
+ line << SearchEngine::Logging::Color.apply("status=#{batch_status}", status_color) << ' '
32
+ line << "docs=#{fetch_stat(stats, :docs_count, indifferent: indifferent, default: 0)}" << ' '
33
+
34
+ success_count = fetch_stat(stats, :success_count, indifferent: indifferent, default: 0).to_i
35
+ success_str = "success=#{success_count}"
36
+ line << (success_count.positive? ? SearchEngine::Logging::Color.bold(success_str) : success_str) << ' '
37
+
38
+ failed_count = fetch_stat(stats, :failure_count, indifferent: indifferent, default: 0).to_i
39
+ failed_str = "failed=#{failed_count}"
40
+ line << (failed_count.positive? ? SearchEngine::Logging::Color.apply(failed_str, :red) : failed_str) << ' '
41
+
42
+ line << "batch=#{batch_number} "
43
+ line << "duration_ms=#{fetch_stat(stats, :duration_ms, indifferent: indifferent, default: 0.0)}"
44
+
45
+ sample_err = extract_sample_error(stats, indifferent: indifferent)
46
+ line << " sample_error=#{sample_err.inspect}" if sample_err
47
+
48
+ line
49
+ end
50
+
51
+ # Derive batch status from success/failure counts.
52
+ # @return [Symbol] :ok, :partial, or :failed
53
+ def status_from_counts(stats, indifferent: false)
54
+ success = fetch_stat(stats, :success_count, indifferent: indifferent, default: 0).to_i
55
+ failure = fetch_stat(stats, :failure_count, indifferent: indifferent, default: 0).to_i
56
+
57
+ if failure.positive? && success.positive?
58
+ :partial
59
+ elsif failure.positive?
60
+ :failed
61
+ else
62
+ :ok
63
+ end
64
+ end
65
+
66
+ # Extract first non-blank sample error message.
67
+ # @return [String, nil]
68
+ def extract_sample_error(stats, indifferent: false)
69
+ samples = fetch_stat(stats, :errors_sample, indifferent: indifferent, default: nil)
70
+ return nil unless samples.is_a?(Array) && samples.any?
71
+
72
+ samples.each do |msg|
73
+ s = msg.to_s
74
+ return s unless s.strip.empty?
75
+ end
76
+ nil
77
+ end
78
+
79
+ # @api private
80
+ def fetch_stat(stats, key, indifferent: false, default: nil)
81
+ val = stats[key]
82
+ val = stats[key.to_s] if val.nil? && indifferent
83
+ val.nil? ? default : val
84
+ end
85
+
86
+ private_class_method :fetch_stat
87
+ end
88
+ end
89
+ end
@@ -12,7 +12,7 @@ module SearchEngine
12
12
  module_function
13
13
 
14
14
  # @param str [String]
15
- # @param color [Symbol] one of :green, :yellow, :red
15
+ # @param color [Symbol] one of :green, :yellow, :red, :cyan
16
16
  # @return [String]
17
17
  def apply(str, color)
18
18
  return str unless enabled?
@@ -21,6 +21,7 @@ module SearchEngine
21
21
  when :green then 32
22
22
  when :yellow then 33
23
23
  when :red then 31
24
+ when :cyan then 36
24
25
  else 0
25
26
  end
26
27
  return str if code.zero?
@@ -37,6 +38,22 @@ module SearchEngine
37
38
  "\e[1m#{str}\e[0m"
38
39
  end
39
40
 
41
+ # Apply dim styling to de-emphasize text (skip/no-op states).
42
+ # @param str [String]
43
+ # @return [String]
44
+ def dim(str)
45
+ return str unless enabled?
46
+
47
+ "\e[2m#{str}\e[0m"
48
+ end
49
+
50
+ # Bold text for section headers (>>>>>> lines).
51
+ # @param str [String]
52
+ # @return [String]
53
+ def header(str)
54
+ bold(str)
55
+ end
56
+
40
57
  # Map indexation status to a color.
41
58
  # @param status [#to_s]
42
59
  # @return [Symbol] color name
@@ -3,5 +3,5 @@
3
3
  module SearchEngine
4
4
  # Current gem version.
5
5
  # @return [String]
6
- VERSION = '30.1.6.3'
6
+ VERSION = '30.1.6.4'
7
7
  end
metadata CHANGED
@@ -1,14 +1,14 @@
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.3
4
+ version: 30.1.6.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nikita Shkoda
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-03-23 00:00:00.000000000 Z
11
+ date: 2026-03-24 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: concurrent-ruby
@@ -153,6 +153,7 @@ files:
153
153
  - lib/search_engine/instrumentation.rb
154
154
  - lib/search_engine/joins/guard.rb
155
155
  - lib/search_engine/joins/resolver.rb
156
+ - lib/search_engine/logging/batch_line.rb
156
157
  - lib/search_engine/logging/color.rb
157
158
  - lib/search_engine/logging/format_helpers.rb
158
159
  - lib/search_engine/logging/partition_progress.rb