← Back to DevBytes

Migrating from Elasticsearch to OpenSearch: Step-by-Step Guide

Understanding Elasticsearch and OpenSearch

Elasticsearch is a distributed, RESTful search and analytics engine built on top of Apache Lucene. For years, it was the go-to solution for full-text search, log analytics, and real-time data exploration. OpenSearch is an open-source fork of Elasticsearch 7.10.2, created by AWS in 2021 after Elastic changed its licensing model. OpenSearch maintains Apache 2.0 licensing while offering a compatible API, making it a compelling alternative for organizations concerned about licensing restrictions and vendor lock-in.

Key Differences at a Glance

Why Migration Matters

Organizations choose to migrate for several reasons: avoiding proprietary licensing risks, reducing costs (free security features), embracing a community-driven open-source project, or complying with organizational open-source policies. The migration itself is straightforward when planned properly, especially if you are currently on Elasticsearch 7.x. The compatibility layer means your existing client code, queries, and ingestion pipelines will largely continue working with minimal changes.

Pre-Migration Assessment

πŸš€ Deploy your AI agent in 10 minutes

Managed Hermes hosting. Zero DevOps. 100M tokens/mo included.

Try it free →

Compatibility Matrix

Before starting your migration, verify your Elasticsearch version against the OpenSearch compatibility table. This is the single most important stepβ€”it determines which migration path you must take.

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Elasticsearch Version        β”‚ OpenSearch Migration Path    β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ 7.10.2 or earlier (7.x)      β”‚ Direct upgrade to OS 1.x     β”‚
β”‚ 7.11 - 7.17.x                β”‚ Reindex or snapshot restore  β”‚
β”‚                              β”‚ with compatibility mode      β”‚
β”‚ 6.x                          β”‚ Upgrade to ES 7.10.2 first,  β”‚
β”‚                              β”‚ then migrate to OS 1.x       β”‚
β”‚ 8.x                          β”‚ Reindex from scratch         β”‚
β”‚                              β”‚ (no direct compatibility)    β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Check Your Current Setup

Run the following command to determine your Elasticsearch version and index settings:

# Check Elasticsearch version
curl -X GET "http://localhost:9200/_nodes/plugins?pretty"

# List all indices with their settings
curl -X GET "http://localhost:9200/_cat/indices?v&s=index"

# Check cluster settings
curl -X GET "http://localhost:9200/_cluster/settings?pretty"

Identify Breaking Changes

OpenSearch 1.x is compatible with Elasticsearch 7.10.2, but there are subtle differences you must audit:

Migration Path 1: Snapshot and Restore (Recommended for 7.x)

This is the cleanest migration method when moving from Elasticsearch 7.10.2 or earlier. It preserves all index data, mappings, and settings.

Step 1: Register a Snapshot Repository on Elasticsearch

Create a shared file system repository accessible to both clusters, or use S3 if you are on AWS:

# File-system based repository (local/shared mount)
curl -X PUT "http://localhost:9200/_snapshot/migration_repo" -H 'Content-Type: application/json' -d '{
  "type": "fs",
  "settings": {
    "location": "/mnt/shared_snapshots",
    "compress": true,
    "max_snapshot_bytes_per_sec": "100mb",
    "max_restore_bytes_per_sec": "100mb"
  }
}'
# S3-based repository (AWS)
curl -X PUT "http://localhost:9200/_snapshot/s3_migration_repo" -H 'Content-Type: application/json' -d '{
  "type": "s3",
  "settings": {
    "bucket": "my-snapshot-bucket",
    "region": "us-east-1",
    "compress": true
  }
}'

Step 2: Create a Snapshot of All Indices

# Create snapshot of all indices (excluding system indices if desired)
curl -X PUT "http://localhost:9200/_snapshot/migration_repo/snapshot_2024_migration?wait_for_completion=true" -H 'Content-Type: application/json' -d '{
  "indices": "*,-.kibana*,-.elastic*",
  "ignore_unavailable": true,
  "include_global_state": false
}'

Important: Set include_global_state to false to avoid importing Elasticsearch-specific cluster metadata into OpenSearch. The wait_for_completion=true parameter makes this a synchronous callβ€”remove it for large snapshots and poll the status instead.

Step 3: Verify the Snapshot

# Check snapshot status
curl -X GET "http://localhost:9200/_snapshot/migration_repo/snapshot_2024_migration?pretty"

# List all snapshots in the repository
curl -X GET "http://localhost:9200/_snapshot/migration_repo/_all?pretty"

Step 4: Install and Start OpenSearch

Download OpenSearch and OpenSearch Dashboards. For a production setup, use the tarball or Docker images:

# Docker-based OpenSearch
docker run -d \
  --name opensearch \
  -p 9200:9200 \
  -p 9600:9600 \
  -e "discovery.type=single-node" \
  -e "OPENSEARCH_INITIAL_ADMIN_PASSWORD=YourStrongPassword123!" \
  -v /mnt/shared_snapshots:/mnt/shared_snapshots \
  opensearchproject/opensearch:latest
# Docker-based OpenSearch Dashboards
docker run -d \
  --name opensearch-dashboards \
  -p 5601:5601 \
  -e "OPENSEARCH_HOSTS=http://opensearch:9200" \
  opensearchproject/opensearch-dashboards:latest

Step 5: Register the Same Repository in OpenSearch

curl -X PUT "http://localhost:9200/_snapshot/migration_repo" -H 'Content-Type: application/json' -d '{
  "type": "fs",
  "settings": {
    "location": "/mnt/shared_snapshots",
    "compress": true
  }
}'

Step 6: Restore the Snapshot into OpenSearch

# Restore all indices from the snapshot
curl -X POST "http://localhost:9200/_snapshot/migration_repo/snapshot_2024_migration/_restore?wait_for_completion=true" -H 'Content-Type: application/json' -d '{
  "indices": "*",
  "ignore_unavailable": true,
  "include_global_state": false,
  "rename_pattern": "(.+)",
  "rename_replacement": "$1"
}'

Step 7: Verify the Restoration

# Check cluster health
curl -X GET "http://localhost:9200/_cluster/health?pretty"

# Verify document counts
curl -X GET "http://localhost:9200/_cat/indices?v&s=index"

# Run a sample search to confirm data integrity
curl -X GET "http://localhost:9200/your_index_name/_search?pretty" -H 'Content-Type: application/json' -d '{
  "query": {
    "match_all": {}
  },
  "size": 5
}'

Migration Path 2: Reindex API (For Cross-Version Migration)

When you cannot use snapshot/restore (e.g., migrating from Elasticsearch 8.x to OpenSearch 2.x), use the Reindex API. This method streams documents from the source cluster to the destination cluster.

Step 1: Configure OpenSearch to Allow Remote Reindex

Add the source Elasticsearch cluster as a reindex whitelist entry in opensearch.yml:

# opensearch.yml
reindex.remote.whitelist: "old-elasticsearch-host:9200"

Restart OpenSearch after this change.

Step 2: Execute Remote Reindex

# Reindex a single index from remote Elasticsearch
curl -X POST "http://localhost:9200/_reindex?pretty&refresh=true&wait_for_completion=false" -H 'Content-Type: application/json' -d '{
  "source": {
    "remote": {
      "host": "http://old-elasticsearch-host:9200",
      "socket_timeout": "60s",
      "max_retry_timeout": "60s"
    },
    "index": "source_index_name",
    "size": 1000
  },
  "dest": {
    "index": "destination_index_name",
    "op_type": "create"
  },
  "conflicts": "proceed"
}'

Step 3: Monitor the Reindex Task

# Get the task ID from the reindex response, then check status
curl -X GET "http://localhost:9200/_tasks/your_task_id?pretty"

# List all running tasks
curl -X GET "http://localhost:9200/_tasks?actions=*reindex&pretty"

Step 4: Reindex Multiple Indices with a Script

#!/bin/bash
# reindex_all.sh β€” Reindex all indices from remote Elasticsearch to OpenSearch

SOURCE_HOST="http://old-elasticsearch-host:9200"
DEST_HOST="http://localhost:9200"

# Fetch all indices from source (excluding system indices)
INDICES=$(curl -s "${SOURCE_HOST}/_cat/indices?h=index" | grep -v -E "^\.(kibana|elastic|opensearch)")

for INDEX in $INDICES; do
  echo "Reindexing: $INDEX"
  curl -X POST "${DEST_HOST}/_reindex?wait_for_completion=false" \
    -H 'Content-Type: application/json' \
    -d "{
      \"source\": {
        \"remote\": {
          \"host\": \"${SOURCE_HOST}\"
        },
        \"index\": \"${INDEX}\"
      },
      \"dest\": {
        \"index\": \"${INDEX}\"
      },
      \"conflicts\": \"proceed\"
    }"
  echo ""
done

echo "Reindex tasks submitted. Monitor with: curl ${DEST_HOST}/_tasks?actions=*reindex"

Client Library Migration

Client applications must be updated to use OpenSearch client libraries. While the REST API is compatible, official client libraries differ.

Java High-Level REST Client Migration

// Old Elasticsearch dependency (Maven)
// Remove this:
// <dependency>
//   <groupId>org.elasticsearch.client</groupId>
//   <artifactId>elasticsearch-rest-high-level-client</artifactId>
//   <version>7.17.x</version>
// </dependency>

// New OpenSearch dependency (Maven)
// Add this:
<dependency>
  <groupId>org.opensearch.client</groupId>
  <artifactId>opensearch-rest-high-level-client</artifactId>
  <version>2.11.1</version>
</dependency>
// Java code changes
// Old Elasticsearch imports:
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.action.search.SearchRequest;

// New OpenSearch imports:
import org.opensearch.client.RestHighLevelClient;
import org.opensearch.client.RequestOptions;
import org.opensearch.action.search.SearchRequest;

// Client initialization remains nearly identical:
RestHighLevelClient client = new RestHighLevelClient(
    RestClient.builder(
        new HttpHost("localhost", 9200, "http")
    )
);

// Most query builders work unchanged:
SearchRequest searchRequest = new SearchRequest("your_index");
searchRequest.source(new SearchSourceBuilder()
    .query(QueryBuilders.matchQuery("field_name", "search_term"))
    .size(10));

SearchResponse response = client.search(searchRequest, RequestOptions.DEFAULT);
client.close();

Python Client Migration

# Uninstall old Elasticsearch client
# pip uninstall elasticsearch

# Install OpenSearch client
# pip install opensearch-py

# Python code changes
# Old:
# from elasticsearch import Elasticsearch
# es = Elasticsearch(['http://localhost:9200'])

# New:
from opensearchpy import OpenSearch

client = OpenSearch(
    hosts=['http://localhost:9200'],
    http_auth=('admin', 'YourStrongPassword123!'),
    use_ssl=False,
    verify_certs=False
)

# Search example β€” API is nearly identical
response = client.search(
    index='your_index',
    body={
        'query': {
            'match': {'field_name': 'search_term'}
        }
    }
)

for hit in response['hits']['hits']:
    print(hit['_source'])

Node.js Client Migration

// Uninstall old client:
// npm uninstall @elastic/elasticsearch

// Install OpenSearch client:
// npm install @opensearch-project/opensearch

// Code changes:
// Old:
// const { Client } = require('@elastic/elasticsearch');
// const client = new Client({ node: 'http://localhost:9200' });

// New:
const { Client } = require('@opensearch-project/opensearch');

const client = new Client({
  node: 'http://localhost:9200',
  auth: {
    username: 'admin',
    password: 'YourStrongPassword123!'
  }
});

// Search example
async function search() {
  const response = await client.search({
    index: 'your_index',
    body: {
      query: {
        match: { field_name: 'search_term' }
      }
    }
  });
  console.log(response.body.hits.hits);
}

search().catch(console.error);

Configuration and Security Migration

Enabling Security in OpenSearch

Unlike Elasticsearch where security requires a paid license, OpenSearch includes a full security framework. Configure it in opensearch.yml:

# opensearch.yml security configuration
plugins.security.disabled: false
plugins.security.ssl.transport.enabled: true
plugins.security.ssl.http.enabled: true
plugins.security.authcz.admin_dn:
  - "CN=admin,O=Example Com Inc.,C=US"

# Internal user database
plugins.security.allow_default_init_securityindex: true
plugins.security.authcz.rest_roles_roles_enabled: true
plugins.security.nodes_dn:
  - "CN=node1,O=Example Com Inc.,C=US"
  - "CN=node2,O=Example Com Inc.,C=US"

Migrating User Accounts and Roles

Export Elasticsearch users (if you had a commercial license) and recreate them in OpenSearch:

# Create an OpenSearch internal user
curl -X PUT "http://localhost:9200/_plugins/_security/api/internalusers/app_user" \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Basic YWRtaW46WW91clN0cm9uZ1Bhc3N3b3JkMTIzIQ==' \
  -d '{
    "password": "SecureAppPassword456!",
    "opendistro_security_roles": ["readall", "writeall"],
    "attributes": {
      "department": "engineering"
    }
  }'

# Create a custom role
curl -X PUT "http://localhost:9200/_plugins/_security/api/roles/custom_read_role" \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Basic YWRtaW46WW91clN0cm9uZ1Bhc3N3b3JkMTIzIQ==' \
  -d '{
    "cluster_permissions": ["cluster_composite_ops_ro"],
    "index_permissions": [{
      "index_patterns": ["app-logs-*"],
      "allowed_actions": ["read", "search"]
    }],
    "tenant_permissions": []
  }'

# Verify user was created
curl -X GET "http://localhost:9200/_plugins/_security/api/internalusers" \
  -H 'Authorization: Basic YWRtaW46WW91clN0cm9uZ1Bhc3N3b3JkMTIzIQ=='

Migrating Kibana Saved Objects to OpenSearch Dashboards

Export saved objects from Kibana and import them into OpenSearch Dashboards:

# Export from Kibana (Elasticsearch 7.x)
# Use Kibana's saved objects UI: Management β†’ Saved Objects β†’ Export
# Or use the export API:
curl -X POST "http://localhost:5601/api/saved_objects/_export" \
  -H 'kbn-xsrf: true' \
  -H 'Content-Type: application/json' \
  -d '{
    "type": ["dashboard", "visualization", "search", "index-pattern"],
    "excludeExportDetails": true
  }' > kibana_export.ndjson

# Import into OpenSearch Dashboards
curl -X POST "http://localhost:5601/api/saved_objects/_import?overwrite=true" \
  -H 'osd-xsrf: true' \
  -H 'Content-Type: application/ndjson' \
  --data-binary @kibana_export.ndjson

Note: The header changed from kbn-xsrf to osd-xsrf. Index patterns referencing .kibana may need adjustment to .opensearch equivalents.

Ingestion Pipeline Migration

Logstash Migration

Logstash continues to work with OpenSearch using the same output plugin. Update the host and authentication:

# logstash.conf β€” Update the OpenSearch output plugin
output {
  opensearch {
    hosts => ["http://opensearch-host:9200"]
    user => "admin"
    password => "YourStrongPassword123!"
    index => "logs-%{+YYYY.MM.dd}"
    ssl => false
    ilm_enabled => false
    manage_template => true
    template_name => "logstash-template"
  }
}

Fluentd / Fluent Bit Migration

# fluent-bit.conf β€” Update output configuration
[OUTPUT]
    Name          opensearch
    Match         *
    Host          opensearch-host
    Port          9200
    HTTP_User     admin
    HTTP_Passwd   YourStrongPassword123!
    Index         app-logs
    Logstash_Format On
    Logstash_Prefix app-logs

Custom Ingest Pipelines

Elasticsearch ingest pipelines work in OpenSearch with minor modifications. Remove Elasticsearch-specific processors:

# Define an ingest pipeline in OpenSearch
curl -X PUT "http://localhost:9200/_ingest/pipeline/log_pipeline" -H 'Content-Type: application/json' -d '{
  "description": "Parse application logs",
  "processors": [
    {
      "grok": {
        "field": "message",
        "patterns": ["%{TIMESTAMP_ISO8601:timestamp} %{LOGLEVEL:level} %{GREEDYDATA:log_message}"]
      }
    },
    {
      "date": {
        "field": "timestamp",
        "formats": ["ISO8601"]
      }
    },
    {
      "set": {
        "field": "ingested_by",
        "value": "opensearch"
      }
    }
  ]
}'

# Apply pipeline during indexing
curl -X POST "http://localhost:9200/app-logs/_doc?pipeline=log_pipeline" -H 'Content-Type: application/json' -d '{
  "message": "2024-03-15T10:30:00Z ERROR Database connection failed"
}'

Testing Your Migration

Functional Test Checklist

Run these validation queries against your OpenSearch cluster to ensure everything works:

# 1. Cluster health
curl -X GET "http://localhost:9200/_cluster/health?pretty"

# 2. Verify all expected indices exist
curl -X GET "http://localhost:9200/_cat/indices?v&s=index"

# 3. Compare document count for critical indices
curl -X GET "http://localhost:9200/critical_index/_count?pretty"

# 4. Run a complex search query
curl -X POST "http://localhost:9200/your_index/_search?pretty" -H 'Content-Type: application/json' -d '{
  "query": {
    "bool": {
      "must": [
        {"match": {"field1": "value"}}
      ],
      "filter": [
        {"range": {"@timestamp": {"gte": "now-7d"}}}
      ]
    }
  },
  "aggregations": {
    "top_terms": {
      "terms": {"field": "category", "size": 10}
    }
  }
}'

# 5. Test bulk indexing
curl -X POST "http://localhost:9200/_bulk" -H 'Content-Type: application/x-ndjson' -d '
{"index": {"_index": "test_index", "_id": "1"}}
{"field1": "value1", "field2": "value2"}
{"index": {"_index": "test_index", "_id": "2"}}
{"field1": "value3", "field2": "value4"}
'

# 6. Verify OpenSearch Dashboards is accessible
curl -X GET "http://localhost:5601/api/status" -H 'osd-xsrf: true'

Performance Comparison

# Benchmark search performance
# Run this against both Elasticsearch and OpenSearch for comparison
curl -X POST "http://localhost:9200/benchmark_index/_search" -H 'Content-Type: application/json' -d '{
  "query": {
    "multi_match": {
      "query": "performance test query",
      "fields": ["title^3", "body", "tags"]
    }
  },
  "track_total_hits": true,
  "profile": true
}'

Best Practices for Migration

Common Pitfalls and Troubleshooting

Snapshot Restore Failures

# Error: "snapshot is incompatible with this version"
# Cause: Trying to restore an ES 8.x snapshot into OS 2.x
# Fix: Use reindex API instead, or downgrade source to 7.10.2

# Error: "repository missing"
# Cause: Repository path not accessible from OpenSearch container/node
# Fix: Verify shared filesystem mount, check permissions (chmod 755)

Client Compatibility Issues

# Common Java error: NoClassDefFoundError
# Cause: Mixing Elasticsearch and OpenSearch JARs on classpath
# Fix: Exclude all Elasticsearch dependencies, use only OpenSearch artifacts

# Common Python error: AuthenticationException
# Cause: OpenSearch security enabled but no credentials provided
# Fix: Add http_auth parameter to OpenSearch client constructor

Index Mapping Conflicts

# Check for mapping incompatibilities
curl -X GET "http://localhost:9200/problem_index/_mapping?pretty"

# Compare with source Elasticsearch mapping
curl -X GET "http://old-es-host:9200/problem_index/_mapping?pretty"

# If mappings differ, create the index manually in OpenSearch first,
# then reindex data without mapping inference:
curl -X POST "http://localhost:9200/_reindex" -H 'Content-Type: application/json' -d '{
  "source": {
    "remote": {
      "host": "http://old-es-host:9200"
    },
    "index": "problem_index"
  },
  "dest": {
    "index": "problem_index",
    "op_type": "create"
  }
}'

Rollback Plan

Always prepare a rollback strategy before starting migration. If issues arise after switching to OpenSearch:

  1. Keep Elasticsearch cluster untouched β€” do not delete it after migration
  2. Maintain dual client configuration in your application for at least 2 weeks
  3. Use DNS or load balancer to redirect traffic back to Elasticsearch quickly
  4. Keep snapshot repositories on both sides for emergency data recovery
# Quick rollback via load balancer redirect (example HAProxy)
# Switch backend from OpenSearch pool to Elasticsearch pool:
# backend opensearch_backend β†’ backend elasticsearch_backend

# Verify Elasticsearch is still healthy
curl -X GET "http://old-elasticsearch-host:9200/_cluster/health?pretty"

# Application rollback: redeploy with Elasticsearch client configuration
# Revert environment variable: SEARCH_HOST=old-elasticsearch-host:9200

Conclusion

Migrating from Elasticsearch to OpenSearch is a well-defined process that, when executed methodically, causes minimal disruption to your applications. The snapshot/restore method provides the cleanest path for Elasticsearch 7.x users, while the reindex API handles cross-version scenarios. Client library changes are mechanicalβ€”primarily import path updates with nearly identical APIs. The real value comes after migration: you gain a fully open-source search engine with bundled security, alerting, and anomaly detection, all under the Apache 2.0 license. By following the steps outlined in this guide, testing thoroughly in staging, and maintaining a solid rollback plan, your team can complete this migration with confidence and minimal downtime.

πŸš€ Need a reliable AI agent for your project?

Deploy Hermes Agent in 10 minutes. Managed hosting, zero DevOps.

Get Started β€” $23.99/mo
← Back to all articles