Skip to content

OpenSearch Migrations: Version Index Templates and Policies

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 do, so you can version-control your search cluster schema alongside your database schema.

  1. 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.

    opensearch-config.yml
    opensearch:
    - name: opensearch
    hosts:
    - https://localhost:9200
    username: admin
    password: secret
    ssl_verification: false
    migrations:
    enabled: true
  2. Create the migration directory

    Terminal window
    mkdir -p /migrations/opensearch

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

  3. Add JSON files

    Create /migrations/opensearch/sf-entities-template.json:

    {
    "settings": {
    "index": {
    "number_of_shards": 1,
    "number_of_replicas": 0
    }
    },
    "mappings": {
    "properties": {
    "entity_name": { "type": "keyword" }
    }
    }
    }
  4. Run migrations

    Use the same runner script as for SQL migrations, adding -m opensearch:

    Terminal window
    php bin/migrate.php --configDir /path/to/config --sqlPath /path/to/migrations -m opensearch

    Omit -m (or pass -m sql) to run SQL migrations instead. --sqlPath is the shared migrations root for both 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 natural order of their relative path (including sub-folders such as release-1.0/; 1.9.0/ runs before 1.10.0/), and the relative path is the migration identity.

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.
  • 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)

Values support the same property expressions as everywhere else in the application: $env.NAME, $ini.key (from the propertySources declared in application.yml) and fallback chains such as $env.NAME || default. This applies to connections in application.yml and to those read from module config files such as opensearch-config.yml, so the migrator connects with exactly the settings the running application uses. Keep credentials out of version control this way:

opensearch-config.yml
opensearch:
- name: opensearch
hosts:
- "$env.OPENSEARCH_HOST || https://localhost:9200"
username: "$ini.opensearchUsername"
password: "$ini.opensearchPassword"
ssl_verification: "$env.OPENSEARCH_SSL_VERIFY || false"
migrations:
enabled: true

A reference that cannot be resolved (for example a key missing from the ini file) stops the migration with an error instead of skipping the connection.

  • The HTTP client is self-contained (no opensearch-php dependency). It uses the Swoole coroutine HTTP client inside a Swoole coroutine 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 behavior.