Skip to content

Package reference

Mirrors the package README (single source). Install @basaltkit/search-elasticsearch v2.0.0 — npm · source.

<p align="center"> <a href="https://basaltkit-docs.pages.dev"> <img src="https://basaltkit-docs.pages.dev/social-card.png" alt="Basalt" width="440"> </a> </p>

@basaltkit/search-elasticsearch ​

Elasticsearch / OpenSearch driver for @basaltkit/search.

Targets the REST API directly — no SDK, an injectable fetch, so the requests are unit-tested without a cluster. Works against Elasticsearch 8.x and OpenSearch 2.x, which share this query surface.

Tenant isolation is enforced on every operation: documents are stored under a compound id (<tenantId>:<id>) so ids never collide across tenants, and every search is constrained with a tenantId term filter — results never leak between tenants.

Installation ​

bash
pnpm add @basaltkit/search @basaltkit/search-elasticsearch

Usage ​

ts
import { searchPlugin } from '@basaltkit/search'
import { ElasticsearchDriver } from '@basaltkit/search-elasticsearch'

const driver = new ElasticsearchDriver({
  node: process.env.ES_NODE!,          // e.g. http://localhost:9200
  apiKey: process.env.ES_API_KEY,      // or username + password (Basic auth)
  indexPrefix: process.env.NODE_ENV === 'production' ? '' : 'dev_',
})

createApp({ plugins: [searchPlugin({ driver, indexes: [/* defineIndex(...) */] })] })

Or use it directly:

ts
await driver.register({ name: 'posts', fields: ['title', 'body'], filterable: ['status'] })
await driver.index('posts', { id: 'p1', tenantId: 'acme', title: 'Hello', body: '…', status: 'published' })

const { hits, total } = await driver.search('posts', {
  tenantId: 'acme',
  q: 'hello',
  filters: { status: 'published' },
  limit: 20,
})

Options ​

OptionDefaultNotes
node—Cluster base URL (required)
apiKey—Authorization: ApiKey <key>
username / password—Basic auth (alternative to apiKey)
indexPrefix''Prepended to every index name
refreshfalsetrue / 'wait_for' makes writes immediately visible (dev/tests)
fetchglobal fetchInjectable HTTP client

Mapping ​

register() maps searchable fields as text with a .keyword sub-field (so they're also filterable/sortable), filterable-only fields as keyword, and id / tenantId as keyword. Searches use multi_match (best_fields) over the searchable fields with track_total_hits for an exact total; filters become term / terms clauses under the tenant scope.

Notes ​

  • Document ids are <encodeURIComponent(tenantId)>:<encodeURIComponent(id)>, built once and used identically by index(), bulk() and remove(): the bulk body carries it verbatim, and the /_doc/<id> path percent-encodes it once more because Elasticsearch decodes path segments (until 2.0 the path did not, so remove() of a bulk-indexed id with a URL-special character deleted nothing). Encoding the segments keeps tenant a:b + id c distinct from tenant a + id b:c (which previously overwrote one tenant's document with another's). UUID/slug ids are unaffected.
  • clear() and clearTenant() use _delete_by_query (match_all, or a term on tenantId — the same predicate every search is scoped by), so search.reindex() can rebuild one tenant and leave the rest. A response that lists failures (version conflicts, shard errors) throws ElasticsearchError instead of letting the rebuild write over a half-cleared index. clearTenant relies on tenantId being a keyword, which register() maps.
  • Deep paging is bounded by @basaltkit/search's maxOffset (default 10 000), matching the cluster's default max_result_window.
  • The fetch client is injectable (options.fetch) — the global fetch is used by default. No hard HTTP dependency.
  • Leave refresh: false in production and let the cluster's refresh interval handle visibility; use 'wait_for' only in tests/dev.
  • Validated end-to-end against a live Elasticsearch 8.x cluster — index, search, term/terms filters, paging with an exact total, bulk, remove, and tenant isolation — in addition to the unit tests. Still worth a smoke test against your exact version (especially OpenSearch).

Released under the MIT License.