@molecule/api-search-elasticsearch
Provider bond · search · API (Node) · v1.0.3 · Apache-2.0
Elasticsearch search provider for molecule.dev
npm install @molecule/api-search-elasticsearchnpm · Source on GitHub · Implements @molecule/api-search
How it works
@molecule/api-search-elasticsearch is a provider bond on the API (Node) side: it implements the search core interface (@molecule/api-search) with a concrete vendor or library behind it.
Your code calls the core; you wire this provider once at startup. Swapping vendors later is one line in that wiring, not a rewrite.
import { setProvider } from '@molecule/api-search'
import { provider } from '@molecule/api-search-elasticsearch'
setProvider(provider)Works with: @molecule/api-search
Secrets: ELASTICSEARCH_URL (optional), ELASTICSEARCH_API_KEY (optional), ELASTICSEARCH_USERNAME (optional), ELASTICSEARCH_PASSWORD (optional)
Reference
Elasticsearch search provider for molecule.dev.
Implements the SearchProvider interface using the @elastic/elasticsearch
client. Supports full-text search, faceted filtering, bulk indexing, and
autocomplete suggestions.
Auto-generated, AI-first package reference for the molecule.dev ecosystem. It is written to be read by coding agents as much as by people, and is generated from this package's source — edit
src/index.tsJSDoc, not this file.
Quick Start
import { setProvider } from '@molecule/api-search'
import { provider } from '@molecule/api-search-elasticsearch'
setProvider(provider)
Type
provider
Installation
npm install @molecule/api-search-elasticsearch @elastic/elasticsearch @molecule/api-proxy-agent @molecule/api-search
API
Interfaces
ElasticsearchOptions
Configuration options for the Elasticsearch search provider.
interface ElasticsearchOptions {
/**
* Elasticsearch node URL.
*
* @default process.env.ELASTICSEARCH_URL ?? 'http://localhost:9200'
*/
node?: string
/**
* Optional API key for authentication.
*
* @default process.env.ELASTICSEARCH_API_KEY
*/
apiKey?: string
/**
* Optional username for basic authentication.
*
* @default process.env.ELASTICSEARCH_USERNAME
*/
username?: string
/**
* Optional password for basic authentication.
*
* @default process.env.ELASTICSEARCH_PASSWORD
*/
password?: string
/**
* Request timeout in milliseconds.
*
* @default 30000
*/
requestTimeout?: number
/**
* Maximum number of retries for failed requests.
*
* @default 3
*/
maxRetries?: number
/**
* Index name prefix to namespace indices.
*/
indexPrefix?: string
}
IndexSchema
Schema definition for a search index, describing the fields and their roles.
interface IndexSchema {
/**
* Map of field names to their types.
*/
fields: Record<string, FieldType>
/**
* Fields that are searchable via full-text queries.
*/
searchableFields?: string[]
/**
* Fields that can be used in filter expressions.
*/
filterableFields?: string[]
/**
* Fields that can be used for sorting results.
*/
sortableFields?: string[]
}
IndexDocument
A document to be indexed in a bulk operation.
interface IndexDocument {
/**
* Unique identifier for the document.
*/
id: string
/**
* The document fields and values.
*/
document: Record<string, unknown>
}
BulkIndexResult
Result of a bulk index operation.
interface BulkIndexResult {
/**
* Number of documents successfully indexed.
*/
indexed: number
/**
* Number of documents that failed to index.
*/
failed: number
/**
* Errors encountered during bulk indexing, keyed by document id.
*/
errors: Record<string, string>
}
SortField
A field to sort search results by.
interface SortField {
/**
* The field name to sort on.
*/
field: string
/**
* Sort direction.
*/
direction: SortDirection
}
SearchQuery
A full-text search query with optional filters, facets, sorting, and pagination.
interface SearchQuery {
/**
* The search text.
*
* Empty or whitespace-only text is "browse" mode: bond implementations
* MUST match ALL documents (subject to `filters`, `sort`, and pagination)
* rather than erroring or returning zero hits. Every bundled bond
* (Elasticsearch, Meilisearch, Typesense, PostgreSQL) follows this
* contract, so swapping providers doesn't silently change what an empty
* search box shows.
*/
text: string
/**
* Filter expressions to narrow results.
*/
filters?: Record<string, unknown>
/**
* Fields to compute facet counts for.
*/
facets?: string[]
/**
* Sort fields and directions.
*/
sort?: SortField[]
/**
* Page number (1-based).
*/
page?: number
/**
* Number of results per page.
*/
perPage?: number
/**
* Whether to include highlighted snippets in results.
*/
highlight?: boolean
}
FacetCount
A single facet count entry.
interface FacetCount {
/**
* The facet value.
*/
value: string
/**
* Number of documents matching this facet value.
*/
count: number
}
SearchHit
A single search result hit.
interface SearchHit {
/**
* Document identifier.
*/
id: string
/**
* Relevance score.
*/
score: number
/**
* The matched document fields.
*/
document: Record<string, unknown>
/**
* Highlighted field snippets, keyed by field name.
*/
highlights?: Record<string, string[]>
}
SearchResult
The result of a search query, including hits, pagination, facets, and timing.
interface SearchResult {
/**
* Matched documents.
*/
hits: SearchHit[]
/**
* Total number of matching documents.
*/
total: number
/**
* Current page number.
*/
page: number
/**
* Number of results per page.
*/
perPage: number
/**
* Facet counts keyed by field name.
*/
facets?: Record<string, FacetCount[]>
/**
* Time taken to process the query in milliseconds.
*/
processingTimeMs: number
}
SuggestOptions
Options for typeahead / autocomplete suggestions.
interface SuggestOptions {
/**
* Maximum number of suggestions to return.
*/
limit?: number
/**
* Fields to generate suggestions from.
*/
fields?: string[]
/**
* Whether to apply fuzzy matching.
*/
fuzzy?: boolean
}
Suggestion
A single autocomplete suggestion.
interface Suggestion {
/**
* The suggested text.
*/
text: string
/**
* Relevance score for ranking suggestions.
*/
score: number
/**
* Optional highlighted version of the suggestion.
*/
highlighted?: string
}
SearchProvider
Search provider interface.
All search providers must implement this interface to provide full-text search, indexing, and suggestion capabilities.
interface SearchProvider {
/**
* Creates a search index with an optional schema.
*
* @param name - Index name.
* @param schema - Optional schema describing field types and roles.
*/
createIndex(name: string, schema?: IndexSchema): Promise<void>
/**
* Deletes a search index and all its documents.
*
* @param name - Index name to delete.
*/
deleteIndex(name: string): Promise<void>
/**
* Indexes a single document.
*
* @param indexName - Target index name.
* @param id - Unique document identifier.
* @param document - The document fields and values.
*/
index(indexName: string, id: string, document: Record<string, unknown>): Promise<void>
/**
* Indexes multiple documents in a single operation.
*
* @param indexName - Target index name.
* @param documents - Array of documents to index.
* @returns Result with indexed/failed counts and errors.
*/
bulkIndex(indexName: string, documents: IndexDocument[]): Promise<BulkIndexResult>
/**
* Executes a full-text search query against an index.
*
* @param indexName - Index to search.
* @param query - Search query with text, filters, pagination, etc.
* @returns Search results with hits, total count, facets, and timing.
*/
search(indexName: string, query: SearchQuery): Promise<SearchResult>
/**
* Deletes a document from an index by id.
*
* @param indexName - Index containing the document.
* @param id - Document identifier to delete.
*/
delete(indexName: string, id: string): Promise<void>
/**
* Returns typeahead/autocomplete suggestions for a partial query.
*
* @param indexName - Index to generate suggestions from.
* @param query - Partial text to complete.
* @param options - Suggestion options (limit, fields, fuzzy).
* @returns Array of suggestions sorted by relevance.
*/
suggest(indexName: string, query: string, options?: SuggestOptions): Promise<Suggestion[]>
/**
* Retrieves a single document from an index by id.
*
* @param indexName - Index containing the document.
* @param id - Document identifier.
* @returns The document fields, or `null` if not found.
*/
getDocument(indexName: string, id: string): Promise<Record<string, unknown> | null>
}
Types
FieldType
Field type for index schema definitions.
type FieldType = 'text' | 'keyword' | 'number' | 'boolean' | 'date' | 'geo'
SortDirection
Sort direction for search results.
type SortDirection = 'asc' | 'desc'
Functions
createProvider(options)
Creates an Elasticsearch search provider instance.
function createProvider(options?: ElasticsearchOptions): SearchProvider
options— Provider configuration options.
Returns: A fully configured SearchProvider implementation.
Constants
provider
Default lazily-initialized Elasticsearch search provider. Uses environment variables for configuration.
const provider: SearchProvider
Core Interface
Implements @molecule/api-search interface.
Bond Wiring
Setup function to register this provider with the core interface:
import { setProvider } from '@molecule/api-search'
import { provider } from '@molecule/api-search-elasticsearch'
export function setupSearchElasticsearch(): void {
setProvider(provider)
}
Injection Notes
Requirements
Peer dependencies:
@molecule/api-search^1.0.1
Environment Variables
ELASTICSEARCH_URL(optional) — Elasticsearch URL — default:http://localhost:9200- Setup: URL of your Elasticsearch node (Elastic Cloud or self-hosted). Defaults to http://localhost:9200 for a local node.
- Example:
http://localhost:9200
ELASTICSEARCH_API_KEY(optional) — Elasticsearch API key- Setup: API key for the node (Kibana → Stack Management → API Keys). Preferred over username/password; leave all three unset for an unsecured local node.
ELASTICSEARCH_USERNAME(optional) — Elasticsearch username- Setup: Basic-auth username — only if not using an API key.
ELASTICSEARCH_PASSWORD(optional) — Elasticsearch password- Setup: Basic-auth password — only if not using an API key.
Runtime Dependencies
@elastic/elasticsearch@molecule/api-proxy-agent@molecule/api-search
Provider-specific behavior to know before debugging:
- Filters are exact
termqueries — declare filterable string fields askeywordin thecreateIndex()schema. Atermfilter against atext-mapped field matches ZERO documents with no error, socreateIndex()THROWS at schema-declaration time if a field is typed'text'and also listed infilterableFields— fix by changing that field to'keyword', or split it into atextfield for full-text search plus a separatekeywordfield for filtering. This only guards schema-declared fields; on a schema-LESS index (nocreateIndex()schema, Elasticsearch dynamic-maps every string totext), filter on the auto-generated sub-field instead:filters: { 'category.keyword': 'electronics' }. - Empty/whitespace-only
SearchQuery.textis "browse" mode — matches ALL documents (filters/sort/pagination still apply, highlighting is skipped since there is no term to highlight), consistent with the coreSearchProvidercontract and the meilisearch/typesense bonds. - Writes are immediately searchable —
index(),bulkIndex(), anddelete()userefresh: 'wait_for', trading write latency for read-your-writes consistency. SearchResult.totalis capped at 10,000 for larger result sets unless the index/query changestrack_total_hits— treat it as "at least N" past that point.- Configuration comes from
ELASTICSEARCH_URL,ELASTICSEARCH_API_KEY, orELASTICSEARCH_USERNAME/ELASTICSEARCH_PASSWORD(orcreateProvider()options). With none set it targetshttp://localhost:9200. Unlike the meilisearch/typesense bonds, this bond does NOT register these keys with@molecule/api-secrets(it would need that package as a new dependency, which is out of scope here), so they never appear in a boot-time configuration report — but an unreachable node or bad credentials no longer surface as a bareECONNREFUSED/401: every method call wraps connectivity and auth failures into an actionable error naming the env var to check. - Runs behind an outbound proxy when
HTTPS_PROXYis set.@elastic/transportbuilds its ownundici.Poolbound to the node origin, which bypasses the global dispatcherNODE_USE_ENV_PROXYinstalls — so on a host whose only egress path is a proxy every request used to fail with a bare connection error. The client now gets the proxy URL through its ownproxyoption (@molecule/api-proxy-agent, resolved againstELASTICSEARCH_URL). A self-hosted cluster listed inNO_PROXY— the common case — keeps connecting directly, and with no proxy configured nothing is passed.
E2E Tests
Integration checklist — drive the real UI (live preview, no mocks), adapt each item to this app's actual screens/flows, and check every box off one by one. A box you can't check is an integration bug to fix — not a skip:
- Searching a term that exists in seeded data returns the matching records in the results UI.
- An empty search box shows the browse-everything view (empty
textmatches ALL documents by contract) — not zero results and not an error. - A term with no matches shows a clear "no results" state.
- Index-on-write is wired: create a new record through the UI, then search for it — it must be findable without a manual reindex.
- If autocomplete/suggestions are surfaced, typing a prefix of a known record shows relevant suggestions.
- Search is scoped to the caller: one user's search never returns another user's private records.