OriginChainDB docs
examples · elasticsearch

Elasticsearch examples

← All examples

Copy-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 / false

12Bucket 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_exception

Elasticsearch 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.