Docs / Use Cases / OpenSearch

OpenSearch: demo & walkthrough

Run a real OpenSearch cluster locally with Docker. Index documents, run full-text queries, test aggregations. No AWS account, no Java downloads, no configuration headaches.

Looking for the API surface and CLI examples? See OpenSearch: API reference.

What the demo does. Calls opensearch create-domain which pulls and runs a real opensearchproject/opensearch:2.11.1 container, polls describe-domain and the cluster's _cluster/health endpoint until the status is green, indexes three documents with explicit IDs (one via curl HTTP, two via the opensearch-py Python client), forces a refresh, runs a match query and asserts exactly 2 hits, then delete-domain and verifies the Docker container has been torn down. The Docker backend turns on automatically when LocalEmu can reach a Docker socket; no env var required. Source: 13-opensearch-demo/ in the examples repo.
🔍

Real Search Engine

Full Lucene-powered text search with relevance scoring, analyzers, and aggregations.

🐳

Docker-Backed

Each domain runs in its own container. Clean, isolated, automatically managed.

🧪

One Command

awsemu opensearch create-domain. That's it. LocalEmu handles the rest.

Step-by-Step Walkthrough

Step 1: Start LocalEmu

Terminal
$ localemu start

# Docker backend auto-enables when Docker is reachable. No env var needed.
# Without Docker, CreateDomain still succeeds but returns a metadata-only
# record with no search engine behind it.

The OpenSearch Docker backend turns itself on when LocalEmu finds Docker running on the host (same pattern as ECS, EKS and RDS). On a host without Docker, OpenSearch falls back to metadata-only mode: create-domain still returns the AWS-shaped response, but no search engine sits behind the endpoint.

Step 2: Create an OpenSearch domain

Terminal
$ awsemu opensearch create-domain \
    --domain-name my-search \
    --engine-version OpenSearch_2.11

DomainStatus:
  DomainName: my-search
  Endpoint: localhost:49464
  EngineVersion: OpenSearch_2.11
  Processing: false

The OpenSearch metadata record (domain name, engine version, cluster config, tags) is stored in LocalEmu's in-process OpenSearch backend; the actual search engine is a real opensearchproject/opensearch:2.11.1 container started by DockerClusterManager. The Endpoint field returns the published host port (localhost:49464 in this run) so you can talk to it with any OpenSearch / Elasticsearch client.

Step 3: Verify the Docker container is running

Terminal
$ docker ps --filter "label=localemu.service=opensearch"

CONTAINER ID   IMAGE                                  STATUS       PORTS
e3a9665b6cc6   opensearchproject/opensearch:2.11.1     Up 49s       0.0.0.0:49464->9200/tcp

Step 4: Check cluster health

Terminal
$ curl -s "http://localhost:49464/_cluster/health" | python3 -m json.tool

{
    "cluster_name": "docker-cluster",
    "status": "green",
    "number_of_nodes": 1,
    "active_primary_shards": 3,
    "active_shards": 3,
    "active_shards_percent_as_number": 100.0
}

Status green means the cluster is healthy and ready for indexing.

Step 5: Index documents

Terminal
$ curl -s -X PUT "http://localhost:49464/products/_doc/1" \
    -H 'Content-Type: application/json' \
    -d '{"name": "MacBook Pro", "price": 2499, "description": "Apple laptop with M4 chip"}'

{"result": "created"}

$ curl -s -X PUT "http://localhost:49464/products/_doc/2" \
    -H 'Content-Type: application/json' \
    -d '{"name": "ThinkPad X1", "price": 1899, "description": "Lenovo business laptop"}'

{"result": "created"}

The documents land in a Lucene index inside the OpenSearch container, so the usual analyzers, tokenisers and BM25 scoring apply. Indexes auto-refresh on a 1-second interval by default; force a refresh with POST /products/_refresh if you need to search a document the same call.

Step 6: Run a full-text search

Terminal
$ curl -s "http://localhost:49464/products/_search" \
    -H 'Content-Type: application/json' \
    -d '{"query": {"match": {"description": "laptop"}}}' | python3 -m json.tool

{
    "hits": {
        "total": {"value": 2},
        "hits": [
            {
                "_id": "2",
                "_score": 0.203,
                "_source": {
                    "name": "ThinkPad X1",
                    "price": 1899,
                    "description": "Lenovo business laptop"
                }
            },
            {
                "_id": "1",
                "_score": 0.165,
                "_source": {
                    "name": "MacBook Pro",
                    "price": 2499,
                    "description": "Apple laptop with M4 chip"
                }
            }
        ]
    }
}

Real Lucene scoring. The ThinkPad scored higher (0.203) because its description is shorter: BM25 boosts matches that account for a larger fraction of the document. Both descriptions contain laptop as a standalone token; document length is what tips the scale.

Python Integration

Use the official opensearch-py client to interact with your local domain. The same code works against real AWS OpenSearch - just change the connection settings.

search_app.py
from opensearchpy import OpenSearch

# Connect to LocalEmu OpenSearch domain
client = OpenSearch(
    hosts=[{"host": "localhost", "port": 49464}],
    use_ssl=False,
)

# Create an index with mappings
client.indices.create(
    index="products",
    body={
        "mappings": {
            "properties": {
                "name": {"type": "text"},
                "price": {"type": "float"},
                "description": {"type": "text"},
                "category": {"type": "keyword"},
            }
        }
    },
)

# Index documents
products = [
    {"name": "MacBook Pro", "price": 2499, "description": "Apple laptop with M4 chip", "category": "laptops"},
    {"name": "ThinkPad X1", "price": 1899, "description": "Lenovo business laptop", "category": "laptops"},
    {"name": "AirPods Pro", "price": 249, "description": "Wireless earbuds with noise cancellation", "category": "audio"},
]

for i, product in enumerate(products):
    client.index(index="products", id=i+1, body=product)

# Refresh to make documents searchable
client.indices.refresh(index="products")

# Full-text search
results = client.search(
    index="products",
    body={"query": {"match": {"description": "laptop"}}},
)
print(f"Found {results['hits']['total']['value']} results")
for hit in results["hits"]["hits"]:
    print(f"  {hit['_source']['name']} (score: {hit['_score']:.3f})")

Terraform Integration

Create OpenSearch domains with Terraform. Point the provider at LocalEmu and your infrastructure code works locally.

main.tf
resource "aws_opensearch_domain" "search" {
  domain_name    = "my-app-search"
  engine_version = "OpenSearch_2.11"

  cluster_config {
    instance_type  = "t3.small.search"
    instance_count = 1
  }

  ebs_options {
    ebs_enabled = true
    volume_size = 10
  }
}

output "opensearch_endpoint" {
  value = aws_opensearch_domain.search.endpoint
}

Cleanup

Deleting the domain removes the Docker container automatically. No orphaned processes, no leftover data.

Terminal
$ awsemu opensearch delete-domain --domain-name my-search

DomainStatus:
  DomainName: my-search
  Deleted: true

$ docker ps --filter "label=localemu.service=opensearch"
CONTAINER ID   IMAGE     COMMAND   CREATED   STATUS    PORTS     NAMES
(empty - container removed)

Supported Versions

Engine Versions Docker Image
OpenSearch2.11, 2.9, 2.7, 2.5, 1.3opensearchproject/opensearch
Elasticsearch7.10, 7.9, 7.7elasticsearch-oss

How it works

1. awsemu opensearch create-domain hits the OpenSearch API on LocalEmu, which records the domain (engine version, cluster config, EBS options, tags, advanced security) in its in-process OpenSearch metadata store, the same shape EKS / RDS use for their AWS-control-plane state.
2. That metadata feeds the behavior layer: DockerClusterManager resolves EngineVersion to a Docker image (opensearchproject/opensearch:2.11.1, elasticsearch-oss:7.10.2, ...), pulls it if needed, and launches a single-node cluster with discovery.type=single-node and the security plugin disabled by default.
3. A free host port is published to the container's port 9200 and surfaced back as the domain's Endpoint field, so the AWS-shaped response and the actual TCP port stay in sync.
4. Hitting http://localhost:<port>/ talks to the real OpenSearch REST API: _cluster/health, index APIs, search APIs, aggregations, the opensearch-py / elasticsearch-py clients, ingest pipelines you install yourself, and so on.
5. delete-domain stops and removes the container, the metadata record is reaped from the OpenSearch store.
6. Set OPENSEARCH_DOCKER_SECURITY=1 when you want to test code paths that hit the OpenSearch security plugin (master users, fine-grained access control). Off by default to keep the demo zero-friction.