npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@molecule/api-search-postgres

v1.0.1

Published

PostgreSQL full-text search provider for molecule.dev

Readme

@molecule/api-search-postgres

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.

PostgreSQL full-text search provider for molecule.dev.

Implements the SearchProvider interface using PostgreSQL's built-in tsvector/tsquery full-text search capabilities. No external search engine required — uses the existing database bond.

Quick Start

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

setProvider(provider)

Type

provider

Installation

npm install @molecule/api-search-postgres @molecule/api-database @molecule/api-search

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

PostgresSearchOptions

Configuration options for the PostgreSQL search provider.

interface PostgresSearchOptions {
  /**
   * PostgreSQL text search configuration (language).
   *
   * @default 'english'
   */
  searchConfig?: string

  /**
   * Table prefix for search index tables.
   *
   * @default 'search_'
   */
  tablePrefix?: string

  /**
   * Whether to use GIN indexes for faster text search.
   *
   * @default true
   */
  useGinIndex?: boolean
}

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

createProvider(options)

Creates a PostgreSQL full-text search provider instance.

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

Returns: A fully configured SearchProvider implementation.

Constants

provider

Default lazily-initialized PostgreSQL search provider. Uses the bonded database pool for queries.

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

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

Injection Notes

Requirements

Peer dependencies:

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

Runtime Dependencies

  • @molecule/api-database
  • @molecule/api-search

Provider-specific behavior to know before debugging:

  • Empty/whitespace-only search text is "browse" mode — matches ALL documents (filters/sort/pagination still apply), per the core SearchQuery.text contract, consistent with the meilisearch/typesense bonds. suggest()'s partial-query parameter is different: empty input there still returns [] (no autocomplete suggestions), not every document.
  • Search text is punctuation-safe prefix matching. Every whitespace-separated token must match as a prefix (widget cas matches "widget case"). Operators typed by users (!, (, :, &, quotes) are treated as literal text, never as tsquery syntax.
  • Facets run one extra GROUP BY document->>field query per requested facet field (values coerced to text, capped at the top 100 by count — SearchResult.facets[field]). Budget for that extra round trip when requesting facets on a hot path or many fields at once.
  • Filters compare top-level document keys as text equality (document->>field = value, field bound as a query parameter — not a sanitized identifier, so punctuation in field names like 'product-type' works). Nested paths and range filters are not supported.
  • Sorting casts to the field's declared type when that field appears in the IndexSchema.fields passed to createIndex() (numberdouble precision, datetimestamptz, booleanboolean) — the type map is recorded in a companion <prefix>schema_meta table at createIndex() time. Fields NOT in the schema (or indices created without one) still compare as text, so lexicographic ordering ("10" < "9") is still possible for undeclared fields.
  • Highlights run over a dedicated content column (the same plain text index()/bulkIndex() extract for full-text matching), not the raw JSON document — so snippets never contain JSON braces/keys/quotes. They still come back under a single key, _content, rather than per-field like the engine-backed bonds. Rows written before this column existed backfill it (empty string) on the next createIndex() call and get real content on their next index()/bulkIndex() write.
  • Only top-level string values (and strings inside arrays) are indexed for full-text matching; numbers/booleans/nested objects are stored but not searchable.

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.