Skip to content

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.

Terminal window
npm install @pgshift/search
import { createClient } from '@pgshift/search'
const db = createClient({ url: process.env.DATABASE_URL })

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',
})
OptionTypeDefaultDescription
fieldsstring[]requiredFields to include in the search index
weightsRecord<string, 'A' | 'B' | 'C' | 'D'>all DPer-field ranking weight. A is highest.
fuzzybooleanfalseEnable trigram fuzzy matching
languagestring'english'Stemming language

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.

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,
})
OptionTypeDefaultDescription
fuzzybooleanindex defaultEnable fuzzy matching for this query
filtersRecord<string, unknown>noneEquality filters applied alongside search
limitnumber20Max results to return
offsetnumber0Pagination offset
languagestringindex defaultOverride stemming language

Returns SearchResult<T>[]:

interface SearchResult<T> {
id: string
rank: number // 0 to 1 relevance score
data: T
}

Removes a document from the search index. Does not touch your main application table.

await db.search('products').delete('1')

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.

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.