Class: Parse::Schema::SearchIndexMigrator

Inherits:
Object
  • Object
show all
Defined in:
lib/parse/schema/search_index_migrator.rb

Overview

Reconciliation engine for Core::SearchIndexing declarations vs. the actual Atlas Search index state. Reads existing indexes via ‘$listSearchIndexes` (so `plan` works without writer config); applies via the writer connection through AtlasSearch::IndexManager.create_index / AtlasSearch::IndexManager.update_index / AtlasSearch::IndexManager.drop_index.

**Drift semantics are detect-and-refuse, not auto-update.** When a declared definition differs from what Atlas reports as the index’s ‘latestDefinition`, the migrator classifies the declaration as `drifted:` and leaves the index alone. The operator opts into the update with `apply!(update: true)`. This matches the spirit of the regular IndexMigrator’s ‘conflicts:` slot but with an explicit opt-in escape hatch, because Atlas Search rebuilds run asynchronously and an over-eager auto-update would silently rebuild production indexes on every deploy.

**Builds are async.** ‘apply!(wait: false)` (the default) submits commands and returns immediately. `apply!(wait: true)` blocks on AtlasSearch::IndexManager.wait_for_ready after each create / update to confirm the index transitions to `READY`. CI / deployment pipelines that need post-apply queryability should opt-in; default fire-and-forget keeps the common rake task fast.

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(model_class) ⇒ SearchIndexMigrator

Returns a new instance of SearchIndexMigrator.



33
34
35
36
37
38
39
# File 'lib/parse/schema/search_index_migrator.rb', line 33

def initialize(model_class)
  unless model_class.is_a?(Class) && model_class < Parse::Object
    raise ArgumentError,
          "SearchIndexMigrator expects a Parse::Object subclass; got #{model_class.inspect}"
  end
  @model_class = model_class
end

Instance Attribute Details

#model_classObject (readonly)

Returns the value of attribute model_class.



31
32
33
# File 'lib/parse/schema/search_index_migrator.rb', line 31

def model_class
  @model_class
end

Instance Method Details

#apply!(update: false, drop: false, wait: false, timeout: 600) ⇒ Hash

Apply the plan. Additive by default — only ‘:to_create` is mutated. Pass `update: true` to also rebuild drifted indexes, `drop: true` to also drop orphans, `wait: true` to block on build completion after each mutation.

Parameters:

  • update (Boolean) (defaults to: false)
  • drop (Boolean) (defaults to: false)
  • wait (Boolean) (defaults to: false)
  • timeout (Integer) (defaults to: 600)

    wait-per-mutation seconds (when wait: true)

Returns:

  • (Hash)

    keys:

    • ‘:created` — Array<Hash> of declarations submitted via create

    • ‘:skipped_exists` — declarations the writer-side check found present at apply time (rare race: plan said to_create, but someone created the index in the window between plan and apply)

    • ‘:in_sync` — declarations the plan classified as in_sync (returned verbatim, no command issued)

    • ‘:updated` — names of indexes rebuilt via update (`update: true` only)

    • ‘:drifted_skipped` — names of drifted declarations that were reported but not updated (default `update: false`)

    • ‘:dropped` — names of orphans dropped (`drop: true` only)

    • ‘:orphans_skipped` — names of orphans reported but not dropped (default `drop: false`)

    • ‘:wait_results` — Hash=> :ready|:failed|:timeout when `wait: true`; empty otherwise.



133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
# File 'lib/parse/schema/search_index_migrator.rb', line 133

def apply!(update: false, drop: false, wait: false, timeout: 600)
  p = plan
  coll = p[:collection]
  wait_results = {}

  # Drops run BEFORE creates so any per-cluster cap (Atlas has a
  # cluster-wide search-index quota) doesn't reject a create that
  # would have fit after the orphan was removed.
  dropped = []
  orphans_skipped = []
  if drop
    p[:orphans].each do |name|
      confirm = "drop_search:#{coll}:#{name}"
      res = Parse::AtlasSearch::IndexManager.drop_index(coll, name, confirm: confirm)
      dropped << name if res == :dropped
    end
  else
    orphans_skipped = p[:orphans].dup
  end

  created = []
  skipped_exists = []
  p[:to_create].each do |decl|
    res = Parse::AtlasSearch::IndexManager.create_index(coll, decl[:name], decl[:definition])
    if res == :exists
      skipped_exists << decl
    else
      created << decl
      wait_results[decl[:name]] = wait_for(coll, decl[:name], timeout) if wait
    end
  end

  updated = []
  drifted_skipped = []
  if update
    p[:drifted].each do |entry|
      decl = entry[:declared]
      Parse::AtlasSearch::IndexManager.update_index(coll, decl[:name], decl[:definition])
      updated << decl[:name]
      wait_results[decl[:name]] = wait_for(coll, decl[:name], timeout) if wait
    end
  else
    drifted_skipped = p[:drifted].map { |e| e[:declared][:name] }
  end

  {
    created:         created,
    skipped_exists:  skipped_exists,
    in_sync:         p[:in_sync],
    updated:         updated,
    drifted_skipped: drifted_skipped,
    dropped:         dropped,
    orphans_skipped: orphans_skipped,
    wait_results:    wait_results,
  }
end

#collection_nameString

Returns the model’s collection name (parse_class).

Returns:

  • (String)

    the model’s collection name (parse_class).



42
43
44
# File 'lib/parse/schema/search_index_migrator.rb', line 42

def collection_name
  @model_class.parse_class
end

#planHash

Compute the plan: what would change if ‘apply!` ran now.

Returns:

  • (Hash)

    keys:

    • ‘:collection` — target collection name

    • ‘:declared` — Array of declaration Hashes

    • ‘:existing` — raw `$listSearchIndexes` result for the collection (or `[]` when Atlas is not available)

    • ‘:to_create` — declarations whose name is absent from `:existing`. These will be submitted on `apply!`.

    • ‘:in_sync` — declarations whose name exists AND whose normalized definition matches the existing `latestDefinition`.

    • ‘:drifted` — declarations whose name exists but whose definition differs from `latestDefinition`. Reported only; never auto-updated. Each entry is `{ declared:, existing: }`.

    • ‘:orphans` — names of search indexes present on the collection but not declared. Reported only by default; dropped under `apply!(drop: true)`.

    • ‘:atlas_available` — false when `$listSearchIndexes` failed (e.g. running against vanilla Mongo without Atlas Search). In that case `:existing` is `[]` and every declaration appears in `:to_create`.



67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
# File 'lib/parse/schema/search_index_migrator.rb', line 67

def plan
  coll = collection_name
  existing, available = fetch_existing_indexes(coll)
  declared = @model_class.mongo_search_index_declarations

  existing_by_name = existing.each_with_object({}) do |idx, h|
    name = (idx["name"] || idx[:name]).to_s
    h[name] = idx unless name.empty?
  end

  to_create = []
  in_sync   = []
  drifted   = []

  declared.each do |decl|
    target = existing_by_name[decl[:name]]
    if target.nil?
      to_create << decl
    elsif definition_matches?(target, decl[:definition])
      in_sync << decl
    else
      drifted << { declared: decl, existing: serialize_existing(target) }
    end
  end

  declared_names = declared.map { |d| d[:name] }.to_set
  orphans = existing_by_name.keys.reject { |name| declared_names.include?(name) }

  {
    collection:      coll,
    declared:        declared,
    existing:        existing,
    atlas_available: available,
    to_create:       to_create,
    in_sync:         in_sync,
    drifted:         drifted,
    orphans:         orphans,
  }
end