@molecule/api-search
Core interface · search · API (Node) · v1.0.3 · Apache-2.0
Full-text search core interface for molecule.dev
npm install @molecule/api-searchHow it works
@molecule/api-search is the search core interface on the API (Node) side: the API your app calls, with no vendor inside.
Choose the implementation by bonding one of its 4 providers: @molecule/api-search-elasticsearch, @molecule/api-search-meilisearch, @molecule/api-search-postgres, @molecule/api-search-typesense.
import { setProvider, search, index, suggest } from '@molecule/api-search'
import { provider as elasticsearch } from '@molecule/api-search-elasticsearch'
setProvider(elasticsearch)
await index('products', '1', { name: 'Widget', price: 9.99 })
const results = await search('products', { text: 'widget', highlight: true })
const suggestions = await suggest('products', 'wid', { limit: 5 })Providers (4): @molecule/api-search-elasticsearch, @molecule/api-search-meilisearch, @molecule/api-search-postgres, @molecule/api-search-typesense
Works with: @molecule/api-bond, @molecule/api-i18n
Reference
Provider-agnostic full-text search interface for molecule.dev.
Defines the SearchProvider interface for indexing, querying, and
autocomplete suggestions. Bond packages (Elasticsearch, Meilisearch,
Typesense, PostgreSQL, etc.) implement this interface. Application code
uses the convenience functions (search, index, suggest, etc.) which
delegate to the bonded provider.
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, search, index, suggest } from '@molecule/api-search'
import { provider as elasticsearch } from '@molecule/api-search-elasticsearch'
setProvider(elasticsearch)
await index('products', '1', { name: 'Widget', price: 9.99 })
const results = await search('products', { text: 'widget', highlight: true })
const suggestions = await suggest('products', 'wid', { limit: 5 })
Type
core
Installation
npm install @molecule/api-search @molecule/api-bond @molecule/api-i18n
API
Interfaces
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
setProvider(provider)
Registers a search provider as the active singleton. Called by bond packages during application startup.
function setProvider(provider: SearchProvider): void
provider— The search provider implementation to bond.
getProvider()
Retrieves the bonded search provider, throwing if none is configured.
function getProvider(): SearchProvider
Returns: The bonded search provider.
hasProvider()
Checks whether a search provider is currently bonded.
function hasProvider(): boolean
Returns: true if a search provider is bonded.
createIndex(name, schema)
Creates a search index with an optional schema.
function createIndex(name: string, schema?: IndexSchema): Promise<void>
name— Index name.schema— Optional schema describing field types and roles.
Returns: A promise that resolves when the index has been created.
deleteIndex(name)
Deletes a search index and all its documents.
function deleteIndex(name: string): Promise<void>
name— Index name to delete.
Returns: A promise that resolves when the index has been deleted.
index(indexName, id, document)
Indexes a single document.
function index(indexName: string, id: string, document: Record<string, unknown>): Promise<void>
indexName— Target index name.id— Unique document identifier.document— The document fields and values.
Returns: A promise that resolves when the document has been indexed.
bulkIndex(indexName, documents)
Indexes multiple documents in a single operation.
function bulkIndex(indexName: string, documents: IndexDocument[]): Promise<BulkIndexResult>
indexName— Target index name.documents— Array of documents to index.
Returns: Result with indexed/failed counts and errors.
search(indexName, query)
Executes a full-text search query against an index.
function search(indexName: string, query: SearchQuery): Promise<SearchResult>
indexName— Index to search.query— Search query with text, filters, pagination, etc.
Returns: Search results with hits, total count, facets, and timing.
deleteDocument(indexName, id)
Deletes a document from an index by id.
function deleteDocument(indexName: string, id: string): Promise<void>
indexName— Index containing the document.id— Document identifier to delete.
Returns: A promise that resolves when the document has been deleted.
suggest(indexName, query, options)
Returns typeahead/autocomplete suggestions for a partial query.
function suggest(indexName: string, query: string, options?: SuggestOptions): Promise<Suggestion[]>
indexName— Index to generate suggestions from.query— Partial text to complete.options— Suggestion options (limit, fields, fuzzy).
Returns: Array of suggestions sorted by relevance.
getDocument(indexName, id)
Retrieves a single document from an index by id.
function getDocument(indexName: string, id: string): Promise<Record<string, unknown> | null>
indexName— Index containing the document.id— Document identifier.
Returns: The document fields, or null if not found.
Available Providers
| Provider | Package |
|---|---|
| Elasticsearch | @molecule/api-search-elasticsearch |
| Meilisearch | @molecule/api-search-meilisearch |
| PostgreSQL (full-text) | @molecule/api-search-postgres |
| Typesense | @molecule/api-search-typesense |
Injection Notes
Requirements
Peer dependencies:
@molecule/api-bond^1.0.1@molecule/api-i18n^1.0.1
Runtime Dependencies
-
@molecule/api-bond -
@molecule/api-i18n -
A
search()route needs BOTH halves wired, or it ships dead. (1) The index starts EMPTY — you mustindex(collection, id, doc)each record on create/update (anddeleteDocument()on delete), or a query returns zero hits forever (nothing was ever indexed). (2) The engine can be ABSENT — unbonded, or no service URL in dev/CI/sandboxes — so a baresearch()throws and the endpoint 500s. Guard and degrade: callsearch()only whenhasProvider()is true, wrap it in try/catch, and on absence or failure fall back to a DataStore query —findMany(coll, { where: [{ field, operator: 'ilike', value:%${text}%}] })— so the feature works WITH or WITHOUT the engine. For a small, already-loaded list, filtering client-side is fine — just don't ship asearch()route no page calls and no writer indexes. -
Empty/whitespace-only
SearchQuery.textis "browse" mode — every bundled bond matches ALL documents (filters/sort/pagination still apply) rather than erroring or returning zero hits. Build an initial "show everything" view withsearch('products', { text: '' })instead of special-casing an empty search box in application code. -
Bonds diverge on details the core contract does NOT standardize: facet support (PostgreSQL supports it via an extra
GROUP BYquery per field; the engine-backed bonds use native aggregations), highlight result shape (per-field for Elasticsearch/Meilisearch/Typesense vs. a single_contentkey for PostgreSQL), and filter semantics (exactterm/=matching everywhere — declare filterable string fields askeywordfor Elasticsearch). Check the bond's own module@remarksbefore debugging a result that looks wrong only on one provider.
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.