← all parts

Part · search.fulltext

What's actually behind search.fulltext

The part exactly as partkit add search.fulltext vendors it into your repo — verified, locked, every byte readable. Nothing here is mocked.

search.fulltextv1.1.0

✓ attested🔒 read-onlysearch.fulltext@1

Lives at parts/search.fulltext/ in your repo — open, owned, readable. Not buried in node_modules. 374 lines of source you can audit.

15 conformance tests passedverified 2026-06-15↗ CI run
content hash 093c9291e2…765cdbpinned in parts.lockctrlai guard fails CI if a single byte changes
tested against node 25.3.0

Public API — what your seam calls

  • search(db: SqlExecutor): SearchIndex
  • SearchIndex { index(doc: IndexDocInput): Promise<void>; remove(ref: string): Promise<void>; query(input: SearchQuery): Promise<SearchResult[]> }
  • class SearchError extends Error { code: SearchErrorCode }
  • types: SqlExecutor, SearchIndex, IndexDocInput, SearchQuery, SearchResult, SearchErrorCode

Invariants — guarantees the contract pins

  1. Importing the part performs no I/O and never throws; index/query validate input with a typed SearchError, and a storage failure surfaces as SearchError("storage") (raw driver errors never escape)
  2. index upserts by ref — re-indexing the same ref replaces the document (never a duplicate); remove is idempotent (removing an absent ref is a no-op)
  3. query accepts a RAW user query string and never throws on syntax — operators, quotes, an unbalanced & or :, accents — are handled as a search expression (websearch_to_tsquery), not as tsquery syntax that can error
  4. Results are ranked by relevance (ts_rank), title outranks body (weighted A vs B), filtered by type, paginated by limit/offset, and each carries a highlighted snippet of the match
  5. A query that matches nothing returns an empty array (not an error); multiple matches come back best-first deterministically (stable secondary sort)
  6. The part operates solely through the provided SqlExecutor seam — it imports no database driver — and every statement targets only its own search_documents table; every input, including the raw query string, is parameterized (no injection)

Owns in your Postgres

search_documents

Dependencies

zero-dep — runs on your Postgres
SOURCEparts/search.fulltext/14 files · click to read
parts/search.fulltext/src/index.tstypescript · 1,433 bytes
/**
 * search.fulltext — public interface. The ONLY legal import surface.
 * Contract: ../contract.json · What your app must provide: ../seams.md
 *
 * Postgres-native full-text search: index documents, then search them with raw
 * user query strings safely, ranked (title over body) with highlighted snippets
 * — on plain Postgres, no separate search vendor. Bind it to a database
 * connection (the SqlExecutor seam); constructing it performs no I/O.
 */
import { createSearch } from "./internal/search";
import type { SearchIndex, SqlExecutor } from "./internal/types";

export { SearchError } from "./internal/errors";
export type { SearchErrorCode } from "./internal/errors";
export type {
  IndexDocInput,
  SearchIndex,
  SearchQuery,
  SearchResult,
  SqlExecutor,
} from "./internal/types";

/**
 * Bind the search index to a database connection (the SqlExecutor seam).
 * Constructing it performs no I/O and never throws (contract invariant 1).
 *
 *   const idx = search(db);
 *   await idx.index({ ref: listing.id, type: "listing", title: listing.name, body: listing.description });
 *   const hits = await idx.query({ q: 'leather "office chair" -broken', type: "listing", limit: 20 });
 *
 * NOTE: result.snippet is highlighted (<mark>…</mark>) but NOT HTML-escaped —
 * escape it before rendering as HTML (seams.md §5).
 */
export function search(db: SqlExecutor): SearchIndex {
  return createSearch(db);
}