← All @molecule/* packages · App templates

@molecule/api-search-typesense

Provider bond · search · API (Node) · v1.0.1 · Apache-2.0

Typesense search provider for molecule.dev

npm install @molecule/api-search-typesense

npm · Source on GitHub · Implements @molecule/api-search

How it works

@molecule/api-search-typesense 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-typesense'

setProvider(provider)

Works with: @molecule/api-search, @molecule/api-secrets

Secrets: TYPESENSE_HOST, TYPESENSE_API_KEY

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.

Typesense search provider for molecule.dev.

Implements the SearchProvider interface using the typesense client. Supports full-text search, faceted filtering, bulk indexing, and autocomplete suggestions.

Quick Start

import { setProvider } from '@molecule/api-search'
import { provider } from '@molecule/api-search-typesense'

setProvider(provider)

Type

provider

Installation

npm install @molecule/api-search-typesense @molecule/api-search @molecule/api-secrets typesense

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
}

TypesenseOptions

Configuration options for the Typesense search provider.

interface TypesenseOptions {
  /**
   * Typesense node URL(s).
   *
   * @default [{ host: process.env.TYPESENSE_HOST ?? 'localhost', port: Number(process.env.TYPESENSE_PORT ?? 8108), protocol: process.env.TYPESENSE_PROTOCOL ?? 'http' }]
   */
  nodes?: Array<{ host: string; port: number; protocol: string }>

  /**
   * Typesense API key for authentication.
   *
   * @default process.env.TYPESENSE_API_KEY
   */
  apiKey?: string

  /**
   * Connection timeout in SECONDS (not milliseconds — this is passed straight
   * to the typesense client's `connectionTimeoutSeconds`).
   *
   * @default 5
   */
  connectionTimeoutSeconds?: number

  /**
   * Number of retries for failed requests.
   *
   * @default 3
   */
  numRetries?: number
}

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 a Typesense search provider instance.

function createProvider(options?: TypesenseOptions): SearchProvider
  • options — Provider configuration options.

Returns: A fully configured SearchProvider implementation.

Constants

provider

Default lazily-initialized Typesense search provider. Uses environment variables for configuration.

const provider: SearchProvider

searchTypesenseSecretDefinitions

Secret definitions required by the Typesense search bond.

const searchTypesenseSecretDefinitions: SecretDefinition[]

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-typesense'

export function setupSearchTypesense(): void {
  setProvider(provider)
}

Injection Notes

Requirements

Peer dependencies:

  • @molecule/api-search ^1.0.1
  • @molecule/api-secrets ^1.0.1

Environment Variables

  • TYPESENSE_HOST (required) — Typesense host
    • Setup: Hostname of your Typesense node (Typesense Cloud or self-hosted).
    • Example: localhost
  • TYPESENSE_API_KEY (required) — Typesense API key
    • Setup: The API key you configured when launching Typesense (or from Typesense Cloud).

Runtime Dependencies

  • @molecule/api-search
  • @molecule/api-secrets
  • typesense

Provider-specific behavior to know before debugging (verified against Typesense 29.0):

  • date fields map to int64 — index date values as epoch numbers (e.g. Date.now() or Unix seconds), NOT as Date objects or ISO strings, or the document is rejected by the collection schema.
  • When createIndex() is given a schema, only the keys of schema.fields are indexed — document fields missing from the schema are stored and returned, but not searchable or filterable. Without a schema, an auto-schema collection (.*: auto) indexes every field.
  • Filter string values are backtick-quoted so punctuation (&&, commas, parentheses) in values is safe; a literal backtick inside a filter value is not representable in Typesense filter syntax. Filtering requires the field to be faceted — declare it in filterableFields.
  • Empty search text matches ALL documents (q: '*'), per the core SearchQuery.text browse-mode contract — consistent with every bundled search bond.
  • connectionTimeoutSeconds is in seconds (default 5).

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.