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.
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
$ 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
$ 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
$ 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
$ 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
$ 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
$ 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.
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.
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.
$ 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 |
|---|---|---|
| OpenSearch | 2.11, 2.9, 2.7, 2.5, 1.3 | opensearchproject/opensearch |
| Elasticsearch | 7.10, 7.9, 7.7 | elasticsearch-oss |
How it works
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. Endpoint field, so the AWS-shaped response and the actual TCP port stay in sync. 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. delete-domain stops and removes the container, the metadata record is reaped from the OpenSearch store. 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.