← All @molecule/* packages · App templates

@molecule/api-search

Core interface · search · API (Node) · v1.0.1 · Apache-2.0

Full-text search core interface for molecule.dev

npm install @molecule/api-search

npm · Source on GitHub

How 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

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.ts JSDoc, not this file.

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.

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

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>
}

FacetCount

A single facet count entry.

interface FacetCount {
  /**
   * The facet value.
   */
  value: string

  /**
   * Number of documents matching this facet value.
   */
  count: number
}

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>
}

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[]
}

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[]>
}

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>
}

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
}

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
}

SortField

A field to sort search results by.

interface SortField {
  /**
   * The field name to sort on.
   */
  field: string

  /**
   * Sort direction.
   */
  direction: SortDirection
}

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
}

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
}

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

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.

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.

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.

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.

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.

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.

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.

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.

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.

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.

Available Providers

ProviderPackage
Search@molecule/api-search-elasticsearch
Search@molecule/api-search-meilisearch
Search@molecule/api-search-postgres
Search@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 must index(collection, id, doc) each record on create/update (and deleteDocument() 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 bare search() throws and the endpoint 500s. Guard and degrade: call search() only when hasProvider() 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 a search() route no page calls and no writer indexes.

  • Empty/whitespace-only SearchQuery.text is "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 with search('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 BY query per field; the engine-backed bonds use native aggregations), highlight result shape (per-field for Elasticsearch/Meilisearch/Typesense vs. a single _content key for PostgreSQL), and filter semantics (exact term/= matching everywhere — declare filterable string fields as keyword for Elasticsearch). Check the bond's own module @remarks before 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 text matches 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.