Elasticsearch
The Elasticsearch service lets your agent talk to a mock Elasticsearch 8.x cluster during simulations, using the same APIs and query languages it uses in production. Your agent connects with elasticsearch-py, any Elastic SDK, or plain HTTP — nothing about the client code needs to change.
Scope: real Elasticsearch is a general-purpose search and analytics engine (product search, vector search, full-text documents, geospatial, APM, SIEM, and more). The Veris mock is currently targeted at log and event-shaped data — it’s backed by an event store that infers a time field (_time, @timestamp, timestamp, …) and ingests CSV/NDJSON. If your production use case is vector search, full-text document retrieval, or another non-log workload, this service will not model it faithfully today.
Requirements
Most apps need two steps:
If your agent only calls cluster / index management endpoints, the dataset is optional — but any call to _search, _query, or _count needs data to return anything meaningful. The upload format is log-oriented (see the note above) — CSV rows or NDJSON events with a time field.
Enable the service
Add the service to veris.yaml:
services:
- name: elasticThat gives your simulation a mock Elasticsearch endpoint. Requests to *.elastic-cloud.com are routed to the mock service automatically via DNS interception.
Provide your log data
Upload a log file via the Datasets section on your Environment page in the Console. Select the elastic service, choose CSV or NDJSON as the format, and upload your file.
One dataset per service per environment. The data is loaded into a queryable store that compiles Query DSL into SQL at request time, so schema is inferred from your upload — you don’t need to declare index mappings.
To replace an existing dataset, delete the current one from the Datasets section first, then upload the new file. Attempting to upload a second dataset for the same service returns a 409 conflict.
Query languages
The service supports both of Elasticsearch’s query surfaces.
Query DSL
Standard JSON queries via POST /_search or POST /{index}/_search. These are compiled deterministically (no LLM in the hot path) into SQL against the uploaded dataset.
{
"query": {
"bool": {
"must": [
{"term": {"status": "error"}},
{"range": {"@timestamp": {"gte": "2025-01-01T00:00:00Z"}}}
]
}
}
}Supported query clauses:
| Clause | Notes |
|---|---|
bool | must, should, must_not, filter |
term / terms | Exact-match on keyword fields |
match / match_phrase | Full-text match against indexed fields |
match_all | Returns everything |
range | Numeric and time ranges. Use absolute ISO-8601 timestamps — date-math expressions like now-1h are passed through as literal strings and will not match. |
exists | Field presence check |
wildcard | * and ? patterns |
query_string | Lucene-style query strings |
Supported aggregations:
| Aggregation | Notes |
|---|---|
terms | Bucket by field value |
date_histogram | Time-bucketed counts |
avg, sum, min, max | Metric aggregations |
cardinality | Distinct-value counts |
Dotted field names (http.response.status_code) are resolved against the flattened column schema automatically.
If the deterministic compiler fails on a query it doesn’t recognize, the request falls through to an LLM-backed translator as a top-level safety net. Within a compiled query, however, unknown sub-clauses (e.g. prefix, fuzzy, span_*) are logged and silently dropped rather than LLM-translated — a query that mixes supported and unsupported clauses will match more broadly than intended. Stick to the clauses in the table above.
ES|QL
Text-based pipe queries via POST /_query:
{
"query": "FROM logs-* | WHERE status == \"error\" | STATS count = COUNT(*) BY host"
}ES|QL is translated via LLM rather than deterministic compilation, so expect a little more latency than Query DSL for equivalent queries.
Endpoints
Beyond search, the mock implements the endpoints most SDKs need during startup, ingest, and teardown:
| Endpoint | Purpose |
|---|---|
GET / | Cluster info — version, cluster name, build metadata |
GET /_cluster/health | Cluster health (always reports green) |
GET /_nodes | Single-node cluster description |
GET /_resolve/index/{name} | Resolve an index pattern to concrete indices |
HEAD /{index} | Check index existence |
PUT /{index} | Create index (no-op, always acknowledged) |
DELETE /{index} | Delete index (no-op, always acknowledged) |
POST /{index}/_doc | Index a single document |
PUT /{index}/_doc/{id} | Index a document with a known id |
POST /_bulk | Bulk ingest. index and create write to the store; update and delete are acknowledged no-ops. |
POST /_search, POST /{index}/_search, GET /{index}/_search | Query DSL search |
POST /_query | ES |
POST /_count, POST /{index}/_count | Count documents |
The cluster reports itself as Elasticsearch 8.15.0 with a single data/ingest/master node, which is enough to satisfy most client libraries’ handshakes.
Connect your app
Point your Elasticsearch client at the mock the same way you’d point it at Elastic Cloud. For example, with elasticsearch-py:
services:
- name: elastic
agent:
environment:
ELASTICSEARCH_URL: https://my-cluster.es.us-east-1.elastic-cloud.comThe hostname doesn’t matter — DNS interception routes any *.elastic-cloud.com request to the mock service. Use whatever URL your production code already expects.
Troubleshooting
Searches return zero hits. Check that a dataset is uploaded for the elastic service on this environment and that the fields your query references actually exist in the uploaded file. The service infers schema from the upload, so a typo’d field name silently matches nothing.
ES|QL queries are slow or flaky. ES|QL uses an LLM translator. For hot-path queries, prefer Query DSL, which compiles deterministically.
Unsupported Query DSL clause. Top-level unsupported shapes fall back to the LLM translator, but unknown sub-clauses inside an otherwise supported query are dropped silently — the query still runs, just without that filter. If you’re getting more hits than you expect, check that every clause you’re using is in the supported table above.
Date-math expressions match nothing. range filters using now-1h / now-1d style expressions are not translated to real timestamps — they’re treated as literal strings and compared lexicographically against your time column, which effectively matches nothing. Use absolute ISO-8601 timestamps instead.