search-engine-for-typesense 30.1.6.12 → 30.1.6.13

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: 4596b142a6080ea8f9941f94611f32e9c461b3e9be51bb8e024e821c68ccd498
4
- data.tar.gz: 0ea309bf811b27491de030021053c888d3c4fae0047292432a7f89a46f4ad722
3
+ metadata.gz: 687864490764b552a2e0a30095711766c1ce073dcb0de9538ecc710ae142aa31
4
+ data.tar.gz: 48aa2e8cdcc750685264e432e8bf3d74b6b61fb11a0e0220d42bd393d90a4e97
5
5
  SHA512:
6
- metadata.gz: 3deec81bb58a05b8582c7081aa345cb4b1cc16b270d576ba22a0d03316590f1fdecbd885a7e5ed35842f909a3e0c278c77dce03232b81ec5091f33771fb5ec09
7
- data.tar.gz: 5577de682690a81acb90e23563b5e7cdd26bfc4b90efe7c28b7d8bbe6224e5c804820829d2a7b9d5020e8941e6b022f5587c76675e5c871f6a660a4183cd23bc
6
+ metadata.gz: 67a000ce3cfa475b57d6059c640ba42682747e32ed3f2ca680bd64f5b4f08f0def7aa205e5d1ba44df9004fc76899734602f0f395542af211962290706dfeb3e
7
+ data.tar.gz: 1aa9dab69f9daad382d32e9d291d0d3226b057363a0da981e77b6b595a92f516257f8776518813d01a6616c0ac823c6d5efe7f0d148473ef52d3043c37afb31d
@@ -129,6 +129,13 @@ module SearchEngine
129
129
  step&.close
130
130
  end
131
131
 
132
+ # Drop orphaned physical collections for this model's logical collection.
133
+ # @return [Hash] { dropped: Array<String>, kept: Array<String>, total_scanned: Integer }
134
+ def prune_orphans!
135
+ logical = respond_to?(:collection) ? collection.to_s : name.to_s
136
+ SearchEngine::Schema.prune_orphans!(logical: logical)
137
+ end
138
+
132
139
  def __se_retention_cleanup!(_logical:, _client:)
133
140
  SearchEngine::Schema.prune_history!(self)
134
141
  end
@@ -60,6 +60,15 @@ module SearchEngine
60
60
  run!(mode: :reindex, targets: names, client: client)
61
61
  end
62
62
 
63
+ # Drop orphaned physical collections across all logical collections.
64
+ # Delegates to {SearchEngine::Schema.prune_orphans!}.
65
+ # @param client [SearchEngine::Client, nil]
66
+ # @param logical [String, nil] scope to a single logical collection
67
+ # @return [Hash] { dropped: Array<String>, kept: Array<String>, total_scanned: Integer }
68
+ def prune_orphans!(client: nil, logical: nil)
69
+ SearchEngine::Schema.prune_orphans!(client: client, logical: logical)
70
+ end
71
+
63
72
  private
64
73
 
65
74
  # @param mode [Symbol] :index | :reindex
@@ -57,6 +57,21 @@ module SearchEngine
57
57
  instrument(:get, path, (start ? (current_monotonic_ms - start) : 0.0), {}, request_token: start)
58
58
  end
59
59
 
60
+ # @return [Array<Hash>] list of aliases, each with :name and :collection_name
61
+ def list_aliases
62
+ start = current_monotonic_ms
63
+ path = '/aliases'
64
+
65
+ result = with_exception_mapping(:get, path, {}, start) do
66
+ typesense.aliases.retrieve
67
+ end
68
+
69
+ raw = symbolize_keys_deep(result)
70
+ Array(raw[:aliases])
71
+ ensure
72
+ instrument(:get, path, (start ? (current_monotonic_ms - start) : 0.0), {}, request_token: start)
73
+ end
74
+
60
75
  # @param alias_name [String]
61
76
  # @return [Hash] Typesense delete response, or { status: 404 } when alias not found
62
77
  def delete_alias(alias_name)
@@ -61,6 +61,13 @@ module SearchEngine
61
61
  services.fetch(:collections).retrieve_schema(collection_name, timeout_ms: timeout_ms)
62
62
  end
63
63
 
64
+ # List all aliases defined on the Typesense server.
65
+ # @return [Array<Hash>] list of aliases, each with :name and :collection_name
66
+ # @see `https://typesense.org/docs/latest/api/aliases.html#list-all-aliases`
67
+ def list_aliases
68
+ services.fetch(:collections).list_aliases
69
+ end
70
+
64
71
  # Delete an alias by name. Returns { status: 404 } when alias not found.
65
72
  # @param alias_name [String]
66
73
  # @return [Hash]
@@ -345,6 +345,49 @@ module SearchEngine
345
345
  true
346
346
  end
347
347
 
348
+ # Drop orphaned physical collections that no alias points to.
349
+ #
350
+ # A physical collection is orphaned when it matches the timestamped naming
351
+ # pattern (`logical_YYYYMMDD_HHMMSS_###`) but is not the target of any
352
+ # alias. These typically accumulate from failed blue/green deployments
353
+ # where the alias swap never occurred.
354
+ #
355
+ # @param client [SearchEngine::Client, nil]
356
+ # @param logical [String, nil] scope to a single logical collection name; when nil, scans all
357
+ # @return [Hash] { dropped: Array<String>, kept: Array<String>, total_scanned: Integer }
358
+ def prune_orphans!(client: nil, logical: nil)
359
+ require 'set'
360
+ client ||= SearchEngine.client
361
+
362
+ meta_timeout = begin
363
+ t = SearchEngine.config.timeout_ms.to_i
364
+ [t, 10_000].max
365
+ rescue StandardError
366
+ 10_000
367
+ end
368
+
369
+ all_collections = Array(client.list_collections(timeout_ms: meta_timeout))
370
+ all_names = all_collections.map { |c| (c[:name] || c['name']).to_s }
371
+
372
+ alias_targets = client.list_aliases.each_with_object(Set.new) do |a, set|
373
+ target = (a[:collection_name] || a['collection_name']).to_s
374
+ set.add(target) unless target.empty?
375
+ end
376
+
377
+ physical_re = if logical
378
+ /\A#{Regexp.escape(logical)}_\d{8}_\d{6}_\d{3}\z/
379
+ else
380
+ /\A.+_\d{8}_\d{6}_\d{3}\z/
381
+ end
382
+
383
+ physicals = all_names.select { |n| physical_re.match?(n) }
384
+ kept, orphaned = physicals.partition { |n| alias_targets.include?(n) }
385
+
386
+ orphaned.each { |name| client.delete_collection(name, timeout_ms: 60_000) }
387
+
388
+ { dropped: orphaned, kept: kept, total_scanned: all_names.size }
389
+ end
390
+
348
391
  private
349
392
 
350
393
  # Generate a new physical name using UTC timestamp + 3-digit sequence.
@@ -56,6 +56,10 @@ module SearchEngine
56
56
  nil
57
57
  end
58
58
 
59
+ def list_aliases
60
+ []
61
+ end
62
+
59
63
  def delete_alias(alias_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.12'
6
+ VERSION = '30.1.6.13'
7
7
  end
@@ -132,6 +132,45 @@ namespace :search_engine do
132
132
  warn("schema:rollback failed: #{error.message}")
133
133
  Kernel.exit(1)
134
134
  end
135
+
136
+ desc "Prune orphaned physicals. Usage: rails 'search_engine:schema:prune_orphans[coll]'"
137
+ task :prune_orphans, [:collection] => :environment do |_t, args|
138
+ logical = nil
139
+ if args[:collection] && !args[:collection].to_s.strip.empty?
140
+ begin
141
+ klass = SearchEngine::Cli.resolve_collection!(args[:collection])
142
+ logical = klass.respond_to?(:collection) ? klass.collection.to_s : klass.name.to_s
143
+ rescue ArgumentError => error
144
+ warn("Error: #{error.message}")
145
+ print_schema_usage
146
+ Kernel.exit(1)
147
+ end
148
+ end
149
+
150
+ payload = { task: 'schema:prune_orphans', collection: logical || '(all)' }
151
+ result = nil
152
+ SearchEngine::Cli.with_task_instrumentation('schema:prune_orphans', payload) do
153
+ result = SearchEngine::Schema.prune_orphans!(logical: logical)
154
+ end
155
+
156
+ if SearchEngine::Cli.json_output?
157
+ puts(JSON.generate({ status: 'ok' }.merge(result)))
158
+ else
159
+ puts("Scanned: #{result[:total_scanned]} collections")
160
+ puts("Kept (alias targets): #{result[:kept].size}")
161
+ if result[:dropped].empty?
162
+ puts('No orphaned physicals found.')
163
+ else
164
+ puts("Dropped #{result[:dropped].size} orphaned physical(s):")
165
+ result[:dropped].each { |name| puts(" - #{name}") }
166
+ end
167
+ end
168
+
169
+ Kernel.exit(0)
170
+ rescue StandardError => error
171
+ warn("schema:prune_orphans failed: #{error.message}")
172
+ Kernel.exit(1)
173
+ end
135
174
  end
136
175
 
137
176
  # ------------------------- Index tasks -------------------------
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.12
4
+ version: 30.1.6.13
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nikita Shkoda