Elasticsearch examples
← All examplesCopy-paste recipes for the Elasticsearch API, using the official @elastic client. Each one reads from or writes to /v1/tenants/:t/es/. New here? Set the client up in Connect an Elasticsearch client, or see the raw endpoints in the HTTP API reference.
These use the Node client; the same requests work from any Elasticsearch 7.x client or straight over HTTP. Writes commit with the row, so a document is searchable immediately, and row-level security and column masking apply to every query.
1Create an index with a mapping
Declare the fields up front. Types are the Elasticsearch types your client already sends.
await es.indices.create({
index: 'shop.products',
mappings: { properties: {
name: { type: 'text' },
brand: { type: 'keyword' },
price: { type: 'integer' }
} }
})2Index a document
Create or replace a document by _id. It is searchable the instant the call returns — there is no refresh interval to wait on.
await es.index({
index: 'shop.products',
id: 'sku-8842',
document: { name: 'Carbon Marathon', brand: 'Aero', price: 149 }
})3Bulk-load many documents
The fast path for ingest and backfills. One NDJSON action line per document (the client builds it for you from the operations array).
await es.bulk({ operations: [
{ index: { _index: 'shop.products', _id: 'sku-1207' } },
{ name: 'Trail 24', brand: 'Aero', price: 89 },
{ index: { _index: 'shop.products', _id: 'sku-3355' } },
{ name: 'City Runner', brand: 'Metro', price: 72 }
]})4Partial update (merge)
Send only the fields that change. Everything else on the document is preserved.
await es.update({
index: 'shop.products',
id: 'sku-8842',
doc: { price: 139 } // name, brand, ... untouched
})5Full-text match
Run the Query DSL you already write. match analyzes the query the same way the field was indexed.
await es.search({
index: 'shop.products',
query: { match: { name: 'marathon' } }
})6Bool query with a filter
Combine a scoring must with non-scoring filter clauses — a term and a numeric range.
await es.search({
index: 'shop.products',
query: { bool: {
must: [{ match: { name: 'runner' } }],
filter: [{ term: { brand: 'Metro' } },
{ range: { price: { lte: 100 } } }]
} }
})7Aggregate (terms)
Set size: 0 to skip the hits and get just the buckets — the shape Kibana and Grafana panels use.
await es.search({
index: 'shop.products',
size: 0,
aggs: { by_brand: { terms: { field: 'brand' } } }
})8Count matches
A count without the documents.
await es.count({
index: 'shop.products',
query: { term: { brand: 'Aero' } }
})9Paginate with search_after
Deep paging within the 10,000-hit window. Sort by a field plus _id, then pass the previous page’s last sort tuple.
const page1 = await es.search({
index: 'shop.products', size: 20,
sort: [{ price: 'asc' }, { _id: 'asc' }],
query: { match_all: {} }
})
const last = page1.hits.hits.at(-1).sort // e.g. [72, 'sku-3355']
await es.search({
index: 'shop.products', size: 20,
sort: [{ price: 'asc' }, { _id: 'asc' }],
search_after: last,
query: { match_all: {} }
})10Delete by query (bounded)
max_docs caps how many are deleted. An unbounded whole-table delete is refused rather than run — use _reindex into a fresh index for that.
await es.deleteByQuery({
index: 'shop.products',
max_docs: 100,
query: { term: { brand: 'Aero' } }
})11Read a document back
Fetch a document by id. _version is the number the write reported, so you can carry it straight into a conditional write. _source filtering works here too, and _source/:id returns the bare document with no envelope.
await es.get({ index: 'shop.products', id: 'sku-8842' })
// -> { _index, _id, _version: 2, found: true, _source: { ... } }
await es.getSource({
index: 'shop.products', id: 'sku-8842', _source_includes: 'price'
}) // -> { price: 139 }
await es.exists({ index: 'shop.products', id: 'sku-8842' }) // true / false12Bucket by calendar day
Group documents into calendar buckets over a date field. The value can be an ISO-8601 string or epoch milliseconds — both bucket the same way. Buckets are UTC and calendar-aligned, so calendar_interval is what you pass; fixed_interval is refused rather than approximated with the wrong boundaries.
await es.search({
index: 'shop.orders', size: 0,
aggs: { per_day: { date_histogram: {
field: 'placed_at', calendar_interval: 'day', time_zone: 'UTC'
} } }
})13Named filter buckets
One bucket per named query, each counting the documents that match both your search and that filter, with sub-aggregations folded over exactly those documents. other_bucket_key adds a bucket for everything that matched none of them. Send a filters aggregation in its own request: it is answered with one search per filter, so it does not share a request with other top-level aggregations.
await es.search({
index: 'shop.products', size: 0,
aggs: { by_price: {
filters: {
other_bucket_key: 'mid_range',
filters: {
clearance: { range: { price: { lt: 50 } } },
premium: { range: { price: { gte: 200 } } }
}
},
aggs: { avg_price: { avg: { field: 'price' } } }
} }
})14Top documents inside an aggregation
Return the best matching documents, with their _source, from inside an aggregation. It works on its own at the top level, or nested under a bucket aggregation so every bucket carries its own examples.
await es.search({
index: 'shop.products', size: 0,
query: { match: { name: 'runner' } },
aggs: { best: { top_hits: { size: 3 } } }
})
// nested: one top hit per brand
aggs: { by_brand: { terms: { field: 'brand' },
aggs: { top: { top_hits: { size: 1 } } } } }15Did you mean (term suggester)
Correct a typo against the words actually indexed in a field. suggest_mode defaults to missing, which corrects only words the index does not already hold; always corrects every word. Each option carries the corrected text and a score. The term suggester is the one available — phrase and completion suggesters are refused by name rather than answered approximately.
await es.search({
index: 'shop.products', size: 0,
suggest: { did_you_mean: {
text: 'marathn', term: { field: 'name' }
} }
})
// suggest.did_you_mean[0].options
// -> [ { text: 'marathon', score: 0.875 } ]16Concurrency-safe bulk writes
Carry if_seq_no and if_primary_term on a bulk action and that write lands only if nobody changed the document first. A stale precondition comes back as a per-item 409 and every other item in the batch still applies, so a losing writer retries one document rather than the whole load.
await es.bulk({ operations: [
{ index: { _index: 'shop.products', _id: 'sku-8842',
if_seq_no: 1, if_primary_term: 1 } },
{ name: 'Carbon Marathon', brand: 'Aero', price: 129 }
]})
// stale precondition -> items[0].index.status === 409
// version_conflict_engine_exceptionElasticsearch is a trademark of Elasticsearch B.V., registered in the U.S. and in other countries. OriginChainDB is not affiliated with, endorsed by, or sponsored by Elasticsearch B.V. OriginChainDB implements a compatible HTTP API so that existing Elasticsearch clients can talk to it; it does not distribute Elasticsearch software.