similarity
Similarity operations enable fuzzy text searching using PostgreSQL's pg_trgm extension. Dream provides three similarity operators with configurable scoring thresholds for different matching strategies.
Setup
Before using similarity operations, you need to set up the pg_trgm extension and optionally create GIN indexes for better performance:
// In a migration file
import { DreamMigrationHelpers } from '@rvoh/dream'
import { Kysely } from 'kysely'
export async function up(db: Kysely<any>): Promise<void> {
// Create the pg_trgm extension (only needed once per database)
await DreamMigrationHelpers.createExtension(db, 'pg_trgm')
// Create GIN index for better performance (optional but recommended)
await DreamMigrationHelpers.createGinIndex(db, 'users_name_gin_index', {
table: 'users',
column: 'name',
})
}
export async function down(db: Kysely<any>): Promise<void> {
await db.schema.dropIndex('users_name_gin_index').execute()
}
Similarity Operators
PostgreSQL's pg_trgm extension provides three distinct similarity functions: similarity() compares the overall similarity between two strings using trigrams, word_similarity() finds the best matching word within a string by comparing each word individually, and strict_word_similarity() performs a more rigorous word-by-word comparison that requires stronger matches. Each function uses different algorithms and scoring methods, making them suitable for different types of fuzzy search scenarios.
ops.similarity()
The similarity() operator uses trigram similarity with a default score threshold of 0.3. It's the most flexible similarity operator and works well for general fuzzy matching.
import { ops } from '@rvoh/dream'
const titleSearchedRecipes = await Recipe.passthrough({ locale: 'en-US' })
.innerJoin('currentRecipeText', {
and: {
title: ops.strictWordSimilarity(this.query),
},
})
.scrollPaginate({ cursor })
ops.wordSimilarity()
The wordSimilarity() operator uses word-based similarity with a default score threshold of 0.5. It's better at matching individual words within longer text.
import { ops } from '@rvoh/dream'
const fullTextSearchedRecipes = await Recipe.passthrough({ locale: 'en-US' })
.innerJoin('currentAggregateRecipeText', {
and: {
aggregate: ops.wordSimilarity(this.query),
},
})
.scrollPaginate({ cursor })
ops.strictWordSimilarity()
The strictWordSimilarity() operator uses strict word-based similarity with a default score threshold of 0.6. It provides the most precise matching.
import { ops } from '@rvoh/dream'
const fullTextSearchedRecipes = await Recipe.passthrough({ locale: 'en-US' })
.innerJoin('currentAggregateRecipeText', {
and: {
aggregate: ops.strictWordSimilarity(this.query),
},
})
.scrollPaginate({ cursor })
Score Thresholds
All similarity operators accept an optional score parameter that controls the matching threshold:
- 0.0 - Very loose matching (matches almost anything)
- 0.3 - Default for
similarity()- good balance of recall and precision - 0.5 - Default for
wordSimilarity()- moderate precision - 0.6 - Default for
strictWordSimilarity()- high precision - 1.0 - Exact match only
// Low threshold - more results, less precise
const looseResults = await User.where({
name: ops.similarity('john', { score: 0.1 }),
}).all()
// High threshold - fewer results, more precise
const preciseResults = await User.where({
name: ops.similarity('john', { score: 0.8 }),
}).all()
GIN Indexes
For optimal performance on large datasets, create GIN indexes on columns you'll search frequently:
// In your migration
await DreamMigrationHelpers.createGinIndex(db, 'posts_title_gin_index', {
table: 'posts',
column: 'title',
})
await DreamMigrationHelpers.createGinIndex(db, 'posts_content_gin_index', {
table: 'posts',
column: 'content',
})