@samveys/search

Searchable models on FTS5, tsvector or FULLTEXT, with an outbox for index updates

Version
1.0.3Nov 5, 2026
Install
pnpm add @samveys/search

This list was read from version 1.0.2; run pnpm sync to refresh it.

Import
import { databaseSearchDriver, searchable, terms } from '@samveys/search'

Function (3)

databaseSearchDriver Function

The built-in `database` driver, one shadow table per index: - SQLite: an FTS5 virtual table ranked with `bm25()` and field weights (built into `node:sqlite`); - PostgreSQL: a `tsvector` column with `setweight()` per field, a GIN index, `ts_rank`; - MySQL / MariaDB: a `FULLTEXT` index per field, scores combined with the field weights. Filters are plain columns. Every query uses bindings; the text is reduced to words first.

function databaseSearchDriver(database: () => Database): SearchDriver;

Exported from packages/search/src/database-driver.ts

searchable Function

Makes a model searchable. Index updates go through an outbox table: `queue()` writes the update in the same transaction as the model, `flushOutbox()` applies it. A crash after commit therefore never leaves the index behind; a rollback never indexes a row that does not exist.

function searchable<M extends ModelCtor>(model: M, options: SearchableOptions<keyof InstanceType<M> & string>, setup: SearchableSetup): Searchable<M>;

Exported from packages/search/src/searchable.ts

terms Function

Words of the query: letters and digits only, so no engine sees query syntax from users.

function terms(text: string): string[];

Exported from packages/search/src/database-driver.ts

Interface (11)

Searchable Interface

What `searchable(Model, options)` returns.

interface Searchable<M extends ModelCtor> {
    readonly model: M;
    readonly index: SearchIndexDefinition;
    search(text: string): SearchBuilder<InstanceType<M>>;
    /**
     * Records an index update in the outbox (`samveys_search_outbox`). Pass the transaction that
     * saved the model, so the update commits with the data and is never lost (#3).
     */
    queue(record: InstanceType<M>, operation: 'upsert' | 'remove', client?: Pick<Database, 'from' | 'rawQuery' | 'dialect'>): Promise<void>;
    /** Applies pending outbox rows for this model. Idempotent; safe to run from several workers. */
  …
Show the whole declaration (16 lines)
interface Searchable<M extends ModelCtor> {
    readonly model: M;
    readonly index: SearchIndexDefinition;
    search(text: string): SearchBuilder<InstanceType<M>>;
    /**
     * Records an index update in the outbox (`samveys_search_outbox`). Pass the transaction that
     * saved the model, so the update commits with the data and is never lost (#3).
     */
    queue(record: InstanceType<M>, operation: 'upsert' | 'remove', client?: Pick<Database, 'from' | 'rawQuery' | 'dialect'>): Promise<void>;
    /** Applies pending outbox rows for this model. Idempotent; safe to run from several workers. */
    flushOutbox(): Promise<number>;
    /** Rebuilds the whole index from the table (`svs search:import`). */
    importAll(chunkSize?: number): Promise<number>;
    /** Clears the index (`svs search:flush`). */
    clear(): Promise<void>;
}

Exported from packages/search/src/contracts.ts

SearchableOptions Interface

No documentation comment.

interface SearchableOptions<A extends string = string> {
    /** Text attributes to index, with weights. */
    readonly fields: Readonly<Partial<Record<A, SearchField>>>;
    /** Attributes that `where()` can filter on (stored in the index, exact match). */
    readonly filters?: readonly A[];
}

Exported from packages/search/src/contracts.ts

SearchableSetup Interface

No documentation comment.

interface SearchableSetup {
    readonly driver: SearchDriver;
    /** The database that holds the outbox and the model table. */
    readonly database: () => Database;

Exported from packages/search/src/searchable.ts

SearchBuilder Interface

No documentation comment.

interface SearchBuilder<I> {
    where(filter: string, value: DbValue): SearchBuilder<I>;
    paginate(page: number, perPage: number): Promise<SearchPage<I>>;
    get(limit?: number): Promise<readonly I[]>;
}

Exported from packages/search/src/contracts.ts

SearchDocument Interface

One indexed record, as handed to a driver.

interface SearchDocument {
    readonly id: string;
    /** Field name → text. */
    readonly fields: Readonly<Record<string, string>>;
    /** Filter name → value. */
    readonly filters: Readonly<Record<string, DbValue>>;
}

Exported from packages/search/src/contracts.ts

SearchDriver Interface

A search backend. Third-party drivers implement this and pass `@samveys/search/conformance`.

interface SearchDriver {
    readonly name: string;
    /** Creates the index if it does not exist. Idempotent. */
    ensureIndex(index: SearchIndexDefinition): Promise<void>;
    /** Inserts or replaces documents. */
    upsert(index: SearchIndexDefinition, documents: readonly SearchDocument[]): Promise<void>;
    remove(index: SearchIndexDefinition, ids: readonly string[]): Promise<void>;
    /** Removes every document of the index. */
    flush(index: SearchIndexDefinition): Promise<void>;
    search(index: SearchIndexDefinition, query: SearchQuery): Promise<SearchHits>;
}

Exported from packages/search/src/contracts.ts

SearchField Interface

Public types of `@samveys/search`. Locked once written: change with an ADR.

interface SearchField {
    /** Relative importance; default 1. */
    readonly weight?: number;
}

Exported from packages/search/src/contracts.ts

SearchHits Interface

No documentation comment.

interface SearchHits {
    /** Record ids, best match first. */
    readonly ids: readonly string[];
    readonly total: number;
}

Exported from packages/search/src/contracts.ts

SearchIndexDefinition Interface

No documentation comment.

interface SearchIndexDefinition {
    /** Index name, e.g. `reports`. */
    readonly name: string;
    readonly fields: Readonly<Record<string, {
        readonly weight: number;
    }>>;
    readonly filters: readonly string[];
}

Exported from packages/search/src/contracts.ts

SearchPage Interface

Result page of `Model.search(...).paginate()`.

interface SearchPage<I> {
    readonly data: readonly I[];
    readonly page: number;
    readonly perPage: number;
    readonly total: number;
}

Exported from packages/search/src/contracts.ts

SearchQuery Interface

No documentation comment.

interface SearchQuery {
    readonly text: string;
    readonly filters: Readonly<Record<string, DbValue>>;
    readonly limit: number;
    readonly offset: number;
}

Exported from packages/search/src/contracts.ts