Search
Most teams add a search bar, realize their SQL LIKE queries don’t scale, and immediately reach for Elasticsearch. That’s a managed cluster, index mappings, a sync pipeline, and a new failure point, for a feature that Postgres already handles natively.
Full-text search via TSVector and TSQuery, with optional fuzzy matching via pg_trgm. No Elasticsearch cluster required.
Install
Section titled “Install”npm install @pgshift/searchimport { createClient } from '@pgshift/search'
const db = createClient({ url: process.env.DATABASE_URL })db.search(entity).index(config)
Section titled “db.search(entity).index(config)”Creates the search index for an entity. Idempotent, safe to call on every startup.
await db.search('products').index({ fields: ['name', 'description', 'category'], weights: { name: 'A', description: 'B', category: 'C' }, fuzzy: true, language: 'english',})| Option | Type | Default | Description |
|---|---|---|---|
fields | string[] | required | Fields to include in the search index |
weights | Record<string, 'A' | 'B' | 'C' | 'D'> | all D | Per-field ranking weight. A is highest. |
fuzzy | boolean | false | Enable trigram fuzzy matching |
language | string | 'english' | Stemming language |
db.search(entity).upsert(id, data)
Section titled “db.search(entity).upsert(id, data)”Inserts or updates a document in the search index. Rebuilds the tsvector automatically.
await db.search('products').upsert('1', { name: 'Nike Air Max 90', description: 'Classic sneaker with visible Air unit.', category: 'shoes',})Call this after creating or updating a record in your main database.
db.search(entity).query(term, options?)
Section titled “db.search(entity).query(term, options?)”Searches the index and returns ranked results.
const results = await db.search('products').query('air max', { fuzzy: true, filters: { category: 'shoes' }, limit: 20, offset: 0,})| Option | Type | Default | Description |
|---|---|---|---|
fuzzy | boolean | index default | Enable fuzzy matching for this query |
filters | Record<string, unknown> | none | Equality filters applied alongside search |
limit | number | 20 | Max results to return |
offset | number | 0 | Pagination offset |
language | string | index default | Override stemming language |
Returns SearchResult<T>[]:
interface SearchResult<T> { id: string rank: number // 0 to 1 relevance score data: T}db.search(entity).delete(id)
Section titled “db.search(entity).delete(id)”Removes a document from the search index. Does not touch your main application table.
await db.search('products').delete('1')How fuzzy matching works
Section titled “How fuzzy matching works”When fuzzy: true, PgShift splits the search term into individual words and compares each one against the indexed text using word_similarity from pg_trgm. A document matches if any word exceeds a similarity threshold of 0.5.
This allows queries like "air maxx" to match "Nike Air Max 90" even with typos.
Migration hints
Section titled “Migration hints”PgShift emits a migration hint when average query latency exceeds 200ms over 100 consecutive queries.
const db = createClient({ url: process.env.DATABASE_URL, onMigrationHint(hint) { console.warn(hint.reason) },})See Migration Hints for details.