Search
One search() call composes Postgres full-text, trigram similarity, and vector distance into a single ranked query. Each method is optional; weights decide how much each one contributes to the final ordering.
A first query
import { search } from 'zanith'; const q = search({ table: 'posts', text: { column: 'body_tsv', query: 'rust web', weight: 0.6 }, trigram: { column: 'title', query: 'rust web', weight: 0.3 }, vector: { column: 'embedding', query: queryEmbedding, weight: 0.1 }, limit: 20,}); const result = await adapter.execute(q);// → SELECT … FROM "posts"// WHERE (body_tsv @@ plainto_tsquery('rust web'))// OR (title % 'rust web')// OR (embedding <=> $N <= …)// ORDER BY (0.6 * ts_rank(...) + 0.3 * similarity(...) + 0.1 * (1 - cosine(...))) DESC// LIMIT 20search() returns a CompiledQuery — hand it to adapter.execute(). Wiring it onto ModelAPI directly is on the roadmap.
The three matchers
text — full-text
| Option | Default | Effect |
|---|---|---|
column | required | The tsvector column (or text — set language for runtime to_tsvector). |
query | required | The user's query string. |
language | undefined | Pass null for a stored tsvector (more efficient). Pass a language for runtime conversion. |
parser | 'plain' | 'plain' · 'phrase' · 'websearch' · 'raw' — maps to plainto_tsquery / phraseto_tsquery / websearch_to_tsquery / to_tsquery. |
weight | 1 | Contribution to the final rank. |
trigram — pg_trgm similarity
| Option | Default | Effect |
|---|---|---|
column | required | Text column compared via the % operator. |
query | required | The query string. |
threshold | 0.1 | Drop rows below this similarity score. |
weight | 1 | Contribution to the final rank. |
vector — pgvector distance
| Option | Default | Effect |
|---|---|---|
column | required | The vector(N) column. |
query | required | Query embedding — same dimensionality as the column. |
metric | 'cosine' | 'cosine' (<=>) · 'l2' (<->) · 'inner' (<#>). |
weight | 1 | Contribution to the final rank (lower distance ranks higher). |
match: 'and' vs 'or'
By default, rows match if any configured matcher succeeds ('or'). Pass match: 'and' to require every configured matcher.
search({ table: 'posts', text: { column: 'body_tsv', query: 'rust' }, trigram: { column: 'title', query: 'rust' }, match: 'and', // both must match limit: 20,});Setting up tsvector — tsvectorSetupSql
tsvectorSetupSql() emits the DDL for a generated tsvector column, the GIN index, and a trigger that maintains it on INSERT / UPDATE. Run it once in a migration.
import { tsvectorSetupSql } from 'zanith'; const sql = tsvectorSetupSql({ table: 'posts', column: 'body_tsv', sources: ['title', 'body'], language: 'english',});await adapter.executeRaw(sql); // Emits, in order:// 1. ALTER TABLE … ADD COLUMN IF NOT EXISTS "body_tsv" tsvector;// 2. CREATE INDEX IF NOT EXISTS "posts_body_tsv_idx" … USING gin(...);// 3. CREATE OR REPLACE FUNCTION posts_body_tsv_trigger_fn() …// 4. CREATE TRIGGER posts_body_tsv_trigger BEFORE INSERT OR UPDATE …// 5. UPDATE "posts" SET "body_tsv" = to_tsvector(...); -- backfill| Option | Default | Effect | |||
|---|---|---|---|---|---|
sources | required | Columns concatenated into the tsvector. Each is wrapped in coalesce(_, '') so NULLs don't break it. | |||
language | 'english' | Postgres dictionary. | |||
triggerName | |