Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Opensearch Migrations Laravel Package

directorytree/opensearch-migrations

OpenSearch index migrations for Laravel. Create, configure, and run versioned index migrations with a familiar artisan workflow, including mappings, settings, index and alias prefixes, and publishing config files.

View on GitHub
Deep Wiki
Context7

Getting Started

Two publishes, in this order, are required — the migrations provider assumes directorytree/opensearch-client is already configured:

composer require directorytree/opensearch-migrations

php artisan vendor:publish --provider="DirectoryTree\OpenSearchClient\OpenSearchClientServiceProvider"
php artisan vendor:publish --provider="DirectoryTree\OpenSearchMigrations\OpenSearchMigrationsServiceProvider"

Read config/opensearch-migrations.php first. It is five keys and it defines everything the rest of this assessment depends on:

'table' => env('OPENSEARCH_MIGRATIONS_TABLE', 'opensearch_migrations'),
'connection' => env('OPENSEARCH_MIGRATIONS_CONNECTION'),
'storage_directory' => env('OPENSEARCH_MIGRATIONS_DIRECTORY', base_path('opensearch/migrations')),
'index_name_prefix' => env('OPENSEARCH_MIGRATIONS_INDEX_NAME_PREFIX', env('SCOUT_PREFIX', '')),
'alias_name_prefix' => env('OPENSEARCH_MIGRATIONS_ALIAS_NAME_PREFIX', env('SCOUT_PREFIX', '')),

Internalise the split immediately: the ledger is a database table, the things being migrated are OpenSearch indices. Two stateful systems, one command. Nearly every gotcha in the last section falls out of that.

Your first real use case is "the mapping my code assumes is not the mapping in production." Scaffold, write, run, verify:

php artisan opensearch:make:migration create_posts_index
use DirectoryTree\OpenSearchAdapter\Indices\Mapping;
use DirectoryTree\OpenSearchAdapter\Indices\Settings;
use DirectoryTree\OpenSearchMigrations\Facades\Index;
use DirectoryTree\OpenSearchMigrations\MigrationInterface;

class CreatePostsIndex implements MigrationInterface
{
    public function up(): void
    {
        Index::create('posts', function (Mapping $mapping, Settings $settings) {
            $mapping->text('title');
            $mapping->text('body');
        });
    }

    public function down(): void
    {
        Index::dropIfExists('posts');
    }
}
php artisan opensearch:migrate
php artisan opensearch:migrate:status

Verification is two-part and both halves matter: the row exists in opensearch_migrations and GET <prefix>posts returns the mapping you wrote. One true without the other is the state you will spend the most time in.

Implementation Patterns

Make the migration file the source of truth for index definitions, and nothing else. This package does not touch querying, document indexing, or reindexing. Your Scout models, aggregations, and query builders stay exactly where they are. The value delivered is that posts's mapping is a reviewed file in a PR instead of a console command someone ran months ago.

Keep up() and down() boring and inverse. Creation migrations drop; additive migrations remove the field they added. Resist conditional logic in down() — if you cannot express the inverse cleanly, that is the signal the change should be a new index plus reindex rather than an in-place edit.

Separate creation from evolution. Ship create_posts_index with the full day-one mapping. Later changes get their own timestamped file (add_tags_to_posts_index). Because migrate:rollback operates on the last batch, batching a create and an evolution together makes rollback coarser than you want — you would have to drop the index to undo a field addition.

Baseline existing indices before the first production run. This is the single most important adoption step and it is entirely manual. If posts is already serving traffic:

  1. Write a migration whose up() reproduces the current live mapping verbatim.
  2. Insert its filename into opensearch_migrations by hand with an appropriate batch number.
  3. Only then begin adding new migrations.

Skip this and the first deploy errors on an existing index, or — worse — gets "fixed" by deleting it.

Use the prefix config as the environment lever, never branching code. Both prefixes default to SCOUT_PREFIX, so if you already namespace Scout indices this is consistent for free. In migrations always write the logical name (posts); the config supplies staging_posts. Hardcoding a prefix in a migration defeats the whole mechanism and breaks the moment you promote to prod.

Run it in the deploy pipeline immediately after php artisan migrate. Same ordering rationale, same log capture, same failure semantics:

php artisan migrate --force
php artisan opensearch:migrate --no-interaction

For a targeted hotfix you can pass a filename — php artisan opensearch:migrate 2026_01_01_000000_add_tags_to_posts_index — but note this runs that one file; it does not skip other pending migrations for you.

Map the command set onto what you already know:

OpenSearch Database equivalent
opensearch:migrate migrate
opensearch:migrate:rollback migrate:rollback (last batch)
opensearch:migrate:reset migrate:reset (all files)
opensearch:migrate:refresh migrate:refresh (reset + rerun)
opensearch:migrate:fresh migrate:fresh (drop all, rerun)
opensearch:migrate:status migrate:status

opensearch:migrate:fresh drops every index. It is a local and CI command; it should never be reachable in a shared environment.

Put a thin seam between your app and the Index facade. A small internal service wrapping the migration operations, plus a lint rule or review convention that application code never references Index directly. This costs an hour and is your exit ramp given the package's adoption profile (single vendor, zero public dependents) and the three-deep dependency chain underneath it.

Test with the fakes, but keep one real job. v1.1.0 explicitly changed "fake index assertion callback semantics," so the callback signature differs between 1.0 and 1.1 — read the source for the version you pinned rather than trusting memory or a tutorial. Regardless, keep at least one CI job running against a containerised OpenSearch: mocks will not catch a mapping the cluster actually rejects.

Gotchas and Tips

The ledger and the cluster drift, and nothing reconciles them. If Index::create() succeeds but the insert into opensearch_migrations fails — DB blip, timeout, killed pod — the next run retries a migration that already partially applied. OpenSearch typically rejects the duplicate index, so migrate:status reports pending for a migration that is visibly half-done. Resolution is always manual: inspect the cluster, then either drop the index or insert the ledger row. Write this into your runbook now, not at 2am.

Concurrent deploys are the main path into that drift. Rolling out multiple pods that each run opensearch:migrate means two processes race the same ledger row across two systems. Unless you have verified locking in the source, serialise it in the pipeline — a single deploy job, an orchestrator lock, or flock around the command. This matters more than for DB migrations precisely because there is no single transaction spanning both stores.

Rollback deletes live data. Index::dropIfExists('posts') means opensearch:migrate:rollback in production removes a serving index. That is the correct schema-migration semantic and the wrong thing to run reflexively mid-incident. Read each down() before running rollback anywhere real; consider making destructive reversals a separate, explicitly named migration so they cannot ride along with an innocuous one.

Aliases are configured but not orchestrated. alias_name_prefix exists, and the author clearly anticipates alias-based access, but alias-swap-plus-reindex choreography is not documented and should not be assumed. If you need zero-downtime mapping changes, treat this package as the index definition component only and build the choreography yourself: create new index, backfill, atomically repoint the alias, drop the old.

You cannot change a field's type in place. text → keyword on a populated field requires a new index and a reindex. A migration that attempts it will fail or leave stale documents silently. Model these as multi-step sequences, never a single edit.

The storage directory must ship with your artifact. opensearch/migrations lives at the project root, not under app/. An over-broad .gitignore or a partial-copy deploy means the files are absent and opensearch:migrate cheerfully reports nothing pending — a silent no-op that looks like success. Add a post-deploy assertion that the directory is non-empty.

Filenames are the identity. The class must match the generated filename minus timestamp and .php, and the runner tracks the filename string. Rename an already-applied migration and its ledger row is orphaned; it will show as pending forever while the cluster is already correct.

Resolve the whole dependency chain before committing. opensearch-migrations → opensearch-client → the official opensearch/php-client. All three must satisfy your PHP and Laravel constraints, and the middle package is the one most likely to lag a framework major. Pin versions and run composer why before you upgrade Laravel.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
adachsoft/changelog-linter
adachsoft/collection
payerurl/telegrambot-crypto-payment-instant-payout
dskripchenko/laravel-hybrid-search
riorizkyrainey/lara-ced
mehedimi/static-vel
zharp/zharp
nonsapiens/bigquery-model-sync
lgu-bayambang-ict-office/lgu-laravel-development-standards
andydefer/laravel-support
se7enxweb/flysystem
lampwick/laravel
jeslage/statamic-emojis
micahdshackelford/rmap-laravel
draftsmaninc/draftsman
square-bit/laravel-volition
himito/laravel-mailman
htinlynn/genius-helper
maco-studios/hephaestus
adachsoft/php-code-style