Treat clients, query languages, plugins, and error semantics as code that must be ported and tested.

Client/SDK and Query-Language Differences, Deprecated APIs, Feature Replacements, and Testing Strategy

Turn Elasticsearch↔OpenSearch migration into an evidence-based compatibility program covering APIs, mappings, queries, clients, plugins, snapshots, security, managed-service boundaries, data sync, relevance and rollback.

Intermediate → Advanced145–195 minutesClient/API drift contract tests · Chapter 29 · Lesson 03Elasticsearch/Kibana 9.5.3 · elasticsearch-py 9.5.1 · OpenSearch/Dashboards 3.8.0 · opensearch-py 3.2.0Last reviewed: September 2026

Learning outcomes

01

Separate transport compatibility from client API compatibility and feature parity.

02

Identify Query DSL commonality, query-language divergence, and removed/deprecated API assumptions through tests.

03

Replace cross-product client reuse with product-specific adapters and contract tests.

04

Test retries, timeouts, serialization, authentication, errors, and bulk partial failures rather than only happy-path search.

05

Build a deprecation/replacement ledger that can be rerun when either server or client version changes.

Execution and safety note

Run mutating, destructive, security, lifecycle, snapshot, failure-injection, and load-test commands only in the disposable AtlasMart lab or an equivalently isolated environment. Verify the target cluster, index, tenant, credentials, and rollback path before execution; treat shown output as an expected invariant unless the lesson explicitly labels it as captured evidence.

Pinned migration baseline. Examples are reviewed against Elasticsearch/Kibana 9.5.3 (released 2026-09-03) and OpenSearch/OpenSearch Dashboards 3.8.0 (released 2026-08-04), using their bundled JVMs. Where application-client behavior matters, the current documented Python baselines are elasticsearch-py 9.5.1 and opensearch-py 3.2.0; the mandatory migration lab itself uses curl/HTTP plus Python 3 standard-library code so the data path is inspectable and client-neutral. The generation environment did not execute live clusters, so timing, throughput, sync-lag, and relevance values in this chapter are acceptance criteria to measure locally—not fabricated results.

1. AtlasMart problem: the code compiles, the migration still fails

The AtlasMart application wraps search in a repository class. It creates a client, sends a Query DSL body, catches one exception type, retries 429s, and parses aggregations. A URL switch may still fail because client version checks, API namespaces, auth middleware, retry defaults, request serialization, response wrappers, and feature-specific APIs have diverged.

A migration-safe application boundary exposes business operations—searchProducts, bulkUpsertCatalog, healthProbe—rather than leaking product-specific client objects throughout the codebase.

2. Current client-family boundary

Client family Use with Current guidance relevant to migration
elasticsearch-py 9.5.1 Elasticsearch 9.x Elastic documents 9.x client compatibility with Elasticsearch 9.x; feature parity still follows client/server versions.
opensearch-py 3.2.0 OpenSearch 3.x Use the OpenSearch client family for OpenSearch; validate its own compatibility matrix.
Elasticsearch clients against OpenSearch 2+ Do not assume compatibility OpenSearch explicitly says no Elasticsearch clients are fully compatible with OpenSearch 2.0 and later.
Raw HTTP/curl diagnostics and narrow portable subsets Useful for migration probes, but endpoint semantic drift still requires tests.

3. Build a product-neutral application contract

Python protocol-style boundary
from typing import Protocol, Iterable

class SearchBackend(Protocol):
    def search_products(self, tenant_id: str, text: str, limit: int) -> list[dict]: ...
    def bulk_upsert(self, docs: Iterable[dict]) -> dict: ...
    def health(self) -> dict: ...

# Implement ElasticBackend with elasticsearch-py.
# Implement OpenSearchBackend with opensearch-py.
# Run the same contract tests against both.

The adapters may generate different requests internally. That is a strength: the business contract stays stable while product-specific behavior is explicit and testable.

4. Query DSL overlap needs semantic tests

Many basic bool, term, match, range, sort, and aggregation shapes remain familiar. Advanced search is where drift accumulates: vector mappings/query parameters, semantic fields, hybrid ranking, search pipelines, rank fusion, query insights/profile details, data streams, security filters, and product-specific query features. Maintain a test corpus for every production query template.

Golden query contract test
{
  "name":"catalog-hiking-tenant-a",
  "request": {
    "query": {
      "bool": {
        "filter":[{"term":{"tenant_id":"tenant-a"}},{"term":{"available":true}}],
        "must":[{"match":{"name":"hiking"}}]
      }
    },
    "size":3,
    "sort":[{"_score":"desc"},{"sku":"asc"}]
  },
  "expected": {
    "must_include":["P-1001"],
    "must_exclude":["P-1004"],
    "minimum_recall_at_3":1.0
  }
}

5. Query languages are not portability layers

Source feature Target migration treatment
Elasticsearch ES|QL rewrite in Query DSL/aggregations, application SQL, or OpenSearch PPL/SQL after semantic tests
OpenSearch PPL/SQL rewrite in Query DSL/aggregations or ES|QL where the analytical contract truly maps
Elastic semantic/vector APIs rebuild with target vector/neural features and re-evaluate relevance
OpenSearch neural/search pipelines rebuild with Elastic semantic/vector/hybrid features and re-evaluate

Word-for-word syntax translation is especially dangerous for ranking and analytics because similarly named functions can differ in null handling, time bucketing, limits, result shape, or execution cost.

6. Deprecation/replacement ledger

Example migration ledger
contracts:
  - id: api-001
    source: "ES 9.5.3 _search bool/match"
    target: "OS 3.8.0 _search bool/match"
    action: test
    evidence: tests/golden/catalog_hiking.json
  - id: analytics-004
    source: "ES|QL sales query"
    target: "OpenSearch PPL or DSL aggregation"
    action: rewrite
    evidence: tests/analytics/sales_parity.json
  - id: lifecycle-002
    source: "Elastic ILM atlasmart-logs-retention"
    target: "OpenSearch ISM atlasmart-logs-retention"
    action: translate
    evidence: tests/lifecycle/retention_state.md
  - id: client-009
    source: "elasticsearch-py 9.5.1"
    target: "opensearch-py 3.2.0"
    action: adapter
    evidence: tests/backend_contract/

7. Error behavior belongs in the contract

Test authentication failures, authorization 403s, 404s, malformed queries, connection timeout, request timeout, 429/rejection behavior, bulk per-item failures, and server unavailability. A client migration that changes retry count or retries non-idempotent writes can corrupt behavior even if every search request succeeds in staging.

Bulk acceptance rule
DO NOT treat HTTP 200 from _bulk as success.
Parse every item:
- 2xx item => accepted
- 409 conflict => classify by write/idempotency policy
- 429 => bounded retry with backoff/jitter if safe
- 4xx mapping/auth => fail fast, do not infinite-retry
- 5xx => bounded retry only when the operation is idempotent/replayable
Record retry_count, final_status, and source event/document identity.

8. Plugins are version-coupled software, not copied files

Inventory every installed plugin and whether it is first-party, third-party, custom, or managed-service-only. Reinstall the correct target product/version plugin or redesign the feature. Native binaries, Java APIs, security hooks, and plugin descriptors are not cross-product promises. A plugin absence can be a migration blocker even when the index data is portable.

Wrong approach. Keep the Elasticsearch client, suppress its version/product check, and declare success because basic search works. Repair: use the supported client for each product behind a contract boundary, run the complete query/error/bulk/security suite, and retire source-specific client code only after parity is demonstrated.

9. Application integration test matrix

Test Pass condition
startup/version probe expected product/version recognized; no hidden compatibility header dependency
auth application identity authenticates with least privilege
search golden top-k and facets satisfy judgments
bulk per-item errors surfaced and retry policy bounded
timeouts client deadline honored; server work/cancellation assumptions documented
pagination stable sort/PIT or equivalent behavior validated for the chosen workflow
telemetry request ID/opaque ID/logging does not leak secrets and remains traceable

10. Local lab baseline for this lesson

Use the established disposable local endpoints: Elasticsearch 9.5.3 at https://localhost:9200 with ELASTIC_PASSWORD and CA file atlasmart-es-http-ca; OpenSearch 3.8.0 at https://localhost:9201 with OPENSEARCH_INITIAL_ADMIN_PASSWORD. Both remain on Docker network atlasmart-search. The OpenSearch demo certificate may be bypassed with -k only in this local lab. Production migration requires verified TLS and separate least-privilege source, target, application, and migration identities.

11. Mini lab: run one application contract suite against both products

  1. Create the same five-document AtlasMart migration fixture on both local endpoints.
  2. Run the golden tenant-a hiking query and record top IDs, facet output, HTTP status, and only the response fields the application actually consumes.
  3. Implement the same SearchBackend business interface with elasticsearch-py 9.5.1 for Elasticsearch and opensearch-py 3.2.0 for OpenSearch.
  4. Inject one 403 and one malformed query. Verify both adapters normalize failures into the application-level error contract instead of leaking client-specific exception types.
  5. Send a Bulk request with one deliberately invalid document and prove the application detects the per-item failure even if the HTTP response itself is successful.
  6. Cleanup only the disposable atlasmart-migrate-source-v1 and atlasmart-migrate-target-v1 indices after saving the contract-test report.

Expected invariant: product-specific client code may differ, but the application contract, security decisions, idempotency rules, and acceptance evidence remain stable.

Check your understanding

  1. Why not share one Elasticsearch client across both products?
  2. What should an application adapter expose?
  3. Why test bulk item responses?
  4. Why are ES|QL and PPL not migration shortcuts?
  5. What does Lesson 4 add?
Review the answers

1. OpenSearch explicitly warns Elasticsearch clients are not fully compatible with OpenSearch 2.0+; supported product-specific clients reduce hidden drift.

2. Business operations and explicit error semantics, not product-specific client internals.

3. The bulk endpoint can return HTTP 200 while individual operations failed.

4. They are distinct languages/interfaces with different commands, semantics, limits, and integrations.

5. Managed-service networking, IAM, encryption, repository, endpoint, DNS, and rollback constraints around the application/data migration.

Summary and next step

Preserve the evidence, assumptions, version boundaries, and safety checks established in this lesson. Carry them into the next lesson—or, at the end of the capstone, into the production runbook—rather than treating this lesson as an isolated recipe.

References and current-version checks

Keep knowledge open

Help the academy stay free and grow.

If these tutorials save you time, a small donation supports new lessons, technical review, diagrams, examples, and long-term maintenance.

ETHEthereum / ERC-20 only
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0

Send only Ethereum or ERC-20 compatible assets to this address.