> ## Documentation Index
> Fetch the complete documentation index at: https://suvera.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# OpenSearch Migrations: Version Index Templates and Policies

> Apply versioned OpenSearch index templates, ISM policies, and index schemas from JSON files with the built-in Winter Boot migration runner.

Winter Boot's OpenSearch migration runner applies JSON-based index templates, ISM policies, and index schemas against configured OpenSearch connections. It tracks execution the same way [SQL migrations](/data/migrations) do, so you can version-control your search cluster schema alongside your database schema.

<Tip>
  The migration runner is part of the framework and does not require the [OpenSearch library](/modules/opensearch) to be enabled at runtime. It reads OpenSearch connection details directly from `application.yml` or from a module config file such as `opensearch-config.yml`.
</Tip>

## Quick start

<Steps>
  <Step title="Enable migrations on the connection">
    Add `migrations.enabled: true` to your OpenSearch connection. This works both for connections declared directly in `application.yml` and for connections defined in a module config file such as `opensearch-config.yml`.

    ```yaml opensearch-config.yml theme={null}
    opensearch:
        -   name: opensearch
            hosts:
                - https://localhost:9200
            username: admin
            password: secret
            ssl_verification: false
            migrations:
                enabled: true
    ```
  </Step>

  <Step title="Create the migration directory">
    ```bash theme={null}
    mkdir -p /migrations/opensearch
    ```

    Migrations live in one folder per connection name, so a connection named `opensearch` reads files from `/migrations/opensearch/`.
  </Step>

  <Step title="Add JSON files">
    Create `/migrations/opensearch/sf-entities-template.json`:

    ```json theme={null}
    {
      "settings": {
        "index": {
          "number_of_shards": 1,
          "number_of_replicas": 0
        }
      },
      "mappings": {
        "properties": {
          "entity_name": { "type": "keyword" }
        }
      }
    }
    ```
  </Step>

  <Step title="Run migrations">
    ```bash theme={null}
    # Using the PHAR built with build/sqlmigrator/build.sh
    ./winter-migrations-app.phar -c /path/to/config --sqlPath /path/to/migrations -m opensearch

    # Or using PHP CLI directly
    php bin/migrate.php -c /path/to/config --sqlPath /path/to/migrations -m opensearch
    ```

    Omit `-m` (or pass `-m sql`) to run SQL migrations instead. The `--sqlPath` argument is the shared migrations root for both types.
  </Step>
</Steps>

## Migration file types

An explicit envelope (`{method, path, body?, name?}`) is always sent as a raw request. Otherwise the **filename decides first**:

| Filename          | Action                               |
| ----------------- | ------------------------------------ |
| `*-template.json` | `PUT /_index_template/{name}`        |
| `*-policy.json`   | `PUT /_plugins/_ism/policies/{name}` |
| anything else     | derived from content (see below)     |

For `*-template.json`, a full composable template body (with `index_patterns`) is sent as-is. A raw `settings` / `mappings` / `aliases` definition is wrapped into a valid template, defaulting `index_patterns` to `["{name}*"]` (which covers the bare index plus dated variants like `sf-events-2026.09.04`). Put an explicit `index_patterns` list in the file to override that default.

For other filenames the content decides:

| Content                                           | Action                               |
| ------------------------------------------------- | ------------------------------------ |
| document with `index_patterns`                    | `PUT /_index_template/{name}`        |
| document with top-level `policy` object           | `PUT /_plugins/_ism/policies/{name}` |
| document with `settings` / `mappings` / `aliases` | `PUT /{name}` (create index)         |

The resource `{name}` defaults to the file name without `.json` and without a trailing `-template`, `-policy`, or `-index` suffix, so `sf-entities-template.json` manages the `sf-entities` index (or template, or policy, depending on content). Index names are lowercased because OpenSearch requires lowercase index names.

Files are executed in alphabetical order (including sub-folders such as `release-1.0/`), and the relative path is the migration identity.

## Idempotency

Executed migrations are recorded as documents in the `winter_migrations` index (one document per connection plus relative path, the OpenSearch equivalent of the `winter_migrations` SQL table), together with a SHA-256 hash of the file content. Re-runs skip files whose hash is unchanged and re-apply files whose content changed, so editing a template or policy file and re-running updates it in place.

Every operation is also safe to replay:

* Index template and ISM policy PUTs are natural upserts.
* Index creation first checks whether the index already exists, so re-running against a cluster whose `winter_migrations` index was lost succeeds instead of failing with `resource_already_exists_exception`.

<Warning>
  Re-applying an index-schema file does not alter a live index: OpenSearch applies settings and mappings at index-creation time. To change a live index, add a new migration file with the appropriate API call (for example an envelope `{method: PUT, path: /<index>/_mapping, ...}` to add a field).
</Warning>

## Connection configuration keys

* `hosts` (required)
* `username` / `password` (basic auth)
* `ssl_verification` (default `true`)
* `timeout` and `connect_timeout` in seconds
* `proxy` (explicit proxy URL, or `false` to disable proxying)

## Notes

* The HTTP client is self-contained (no `opensearch-php` dependency). It uses the Swoole coroutine HTTP client inside a Swoole coroutine (the same approach as the winter-opensearch `SwooleHttpHandler`), cURL when available, and the PHP stream wrapper as a fallback. Hosts are tried in order on transport failure.
* First failure stops all migrations. There is no automatic rollback, matching the SQL migration behaviour.
