Skip to content

feat(orm): add fuzzy search and relevance ordering (PostgreSQL)#2573

Open
docloulou wants to merge 2 commits intozenstackhq:devfrom
docloulou:fuzzysearch
Open

feat(orm): add fuzzy search and relevance ordering (PostgreSQL)#2573
docloulou wants to merge 2 commits intozenstackhq:devfrom
docloulou:fuzzysearch

Conversation

@docloulou
Copy link
Copy Markdown

@docloulou docloulou commented Apr 10, 2026

Summary

  • Add fuzzy and fuzzyContains filter operators for String fields in where clauses, using PostgreSQL's pg_trgm extension with unaccent for accent-insensitive trigram matching
  • Add _relevance ordering in orderBy to sort results by fuzzy similarity score, supporting single and multiple fields
  • MySQL and SQLite explicitly throw NotSupported errors for these operators

New API

// Fuzzy similarity match (pg_trgm %)
await client.flavor.findMany({
  where: { name: { fuzzy: 'creme' } }
});

// Fuzzy substring match (pg_trgm <%)
await client.flavor.findMany({
  where: { name: { fuzzyContains: 'choco' } }
});

// Relevance ordering
await client.flavor.findMany({
  where: { name: { fuzzy: 'creme' } },
  orderBy: { _relevance: { fields: ['name'], search: 'creme', sort: 'desc' } }
});

Prerequisites (PostgreSQL)

The user must enable the following extensions in their PostgreSQL database:

CREATE EXTENSION IF NOT EXISTS unaccent;
CREATE EXTENSION IF NOT EXISTS pg_trgm;

Files changed

File Changes
packages/orm/src/client/crud-types.ts fuzzy, fuzzyContains in StringFilter; RelevanceOrderBy type
packages/orm/src/client/constants.ts Fuzzy filter kind in FILTER_PROPERTY_TO_KIND
packages/orm/src/client/crud/dialects/base-dialect.ts Handle fuzzy, fuzzyContains, _relevance in filter/orderBy builders; 3 abstract methods
packages/orm/src/client/crud/dialects/postgresql.ts pg_trgm + unaccent implementation (%, <%, similarity(), GREATEST())
packages/orm/src/client/crud/dialects/mysql.ts NotSupported errors
packages/orm/src/client/crud/dialects/sqlite.ts NotSupported errors
packages/orm/src/client/zod/factory.ts Zod validation schemas for fuzzy, fuzzyContains, _relevance
tests/e2e/orm/schemas/basic/schema.zmodel Added Flavor model
tests/e2e/orm/client-api/fuzzy-search.test.ts 35 E2E tests

Implementation details

fuzzy filter

Uses PostgreSQL trigram similarity operator % with unaccent and lower for accent-insensitive, case-insensitive matching:

unaccent(lower("name")) % unaccent(lower('creme'))

fuzzyContains filter

Uses PostgreSQL word similarity operator <% to check if the search term is approximately contained as a substring:

unaccent(lower('choco')) <% unaccent(lower("name"))

_relevance ordering

Uses similarity() function for single fields, GREATEST() for multiple fields:

-- Single field
ORDER BY similarity(unaccent(lower("name")), unaccent(lower('creme'))) DESC

-- Multiple fields
ORDER BY GREATEST(
  similarity(unaccent(lower("name")), unaccent(lower('chocolate'))),
  similarity(unaccent(lower("description")), unaccent(lower('chocolate')))
) DESC

Test plan

  • Basic fuzzy search (English words with typos, transpositions, truncation)
  • Accent-insensitive fuzzy search (French words: crème, café, éclair, pâté)
  • Nullable field handling
  • Combined filters (fuzzy + contains, fuzzy + startsWith, AND/OR/NOT)
  • fuzzyContains (substring fuzzy matching)
  • _relevance ordering (single field, multiple fields, with pagination)
  • Mutations (updateMany, deleteMany with fuzzy/fuzzyContains)
  • GroupBy and count with fuzzy filters
TEST_DB_PROVIDER=postgresql pnpm vitest run tests/e2e/orm/client-api/fuzzy-search.test.ts

Documentation : zenstackhq/zenstack-docs#596

Summary by CodeRabbit

  • New Features

    • Fuzzy search added (approximate, accent-insensitive matching) with both whole-term and substring modes.
    • Relevance-based ordering (_relevance) to rank results by similarity.
    • PostgreSQL: full support for fuzzy search and relevance ordering; MySQL and SQLite: not supported.
  • Tests

    • Added comprehensive end-to-end tests covering fuzzy filtering, relevance ordering, pagination, mutations, and aggregations.

… only)

- Introduced fuzzy search operators (`fuzzy`, `fuzzyContains`) in the ORM.
- Added `RelevanceOrderBy` type for sorting based on fuzzy search relevance.
- Implemented fuzzy search filters in PostgreSQL dialect.
- Added error handling for unsupported fuzzy search features in MySQL and SQLite dialects.
- Updated Zod schema factory to include fuzzy search fields.
- Created a new `Flavor` model in the schema for testing purposes.
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 10, 2026

📝 Walkthrough

Walkthrough

Adds fuzzy text search operators and relevance-based ordering to the ORM: types, Zod schemas, dialect extension points and implementations (Postgres), unsupported stubs (MySQL/SQLite), client query builder updates, test schema/model additions, and a comprehensive PostgreSQL-only e2e test suite.

Changes

Cohort / File(s) Summary
Filter Operator Mappings
packages/orm/src/client/constants.ts
Added fuzzy and fuzzyContains mapped to new 'Fuzzy' filter kind.
Type & Schema
packages/orm/src/client/crud-types.ts, packages/orm/src/client/zod/factory.ts
Extended StringFilter with fuzzy/fuzzyContains, added RelevanceOrderBy and integrated _relevance into orderBy types and Zod schemas.
Query Builder / Dialect Base
packages/orm/src/client/crud/dialects/base-dialect.ts
Added abstract methods buildFuzzyFilter, buildFuzzyContainsFilter, buildRelevanceOrderBy; added handling/validation for fuzzy filters and _relevance ordering; forbid cursor pagination with _relevance.
Postgres Dialect
packages/orm/src/client/crud/dialects/postgresql.ts
Implemented fuzzy filters using unaccent(lower(...)) with trigram operators and buildRelevanceOrderBy using similarity(...) / GREATEST(...) for multi-field relevance.
MySQL / SQLite Dialects
packages/orm/src/client/crud/dialects/mysql.ts, packages/orm/src/client/crud/dialects/sqlite.ts
Added overrides that throw provider "not supported" errors for fuzzy, fuzzyContains, and _relevance ordering.
Tests & Test Schema
tests/e2e/orm/client-api/fuzzy-search.test.ts, tests/e2e/orm/schemas/basic/*
Added PostgreSQL-only e2e fuzzy-search suite and new Flavor model plus generated types/models used by tests (schema.zmodel, schema.ts, models.ts, input.ts).

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Poem

🐇 I nibble code and chase a clue,

fuzzy matches, accents few,
Postgres trigrams hum and sing,
MySQL, SQLite kindly cling—no fling,
Hop, test, order by relevance true.

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main feature addition: fuzzy search and relevance ordering with PostgreSQL-specific implementation across the ORM.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (1)
packages/orm/src/client/crud/dialects/postgresql.ts (1)

561-590: Well-implemented PostgreSQL fuzzy search using pg_trgm.

The implementation correctly uses:

  • Trigram similarity operator (%) for fuzzy
  • Word similarity operator (<%) for fuzzyContains with proper operand ordering
  • GREATEST() aggregation for multi-field relevance scoring

The use of sql template tags is appropriate here as these are PostgreSQL-specific operators not available in Kysely's type-safe API. The sql template is Kysely's escape hatch mechanism.

Note: Extension dependencies (pg_trgm and unaccent) are already documented in the type definitions (crud-types.ts). Consider adding runtime error handling if extensions are missing, similar to the createNotSupportedError pattern used for MySQL/SQLite, to provide users with a clearer message instead of a generic PostgreSQL error.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/orm/src/client/crud/dialects/postgresql.ts` around lines 561 - 590,
Add runtime checks for the required PostgreSQL extensions and throw a clear
user-facing error if missing: implement an internal check (e.g.,
ensurePostgresExtensionsAvailable) that queries pg_extension for 'pg_trgm' and
'unaccent' and call it from the PostgreSQL dialect initialization or lazily
before using fuzzy features; update buildFuzzyFilter, buildFuzzyContainsFilter,
and buildRelevanceOrderBy to call this check (or ensure it's called beforehand)
and throw a createNotSupportedError-style error with a clear message and
remediation steps if either extension is absent.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/orm/src/client/crud-types.ts`:
- Around line 912-930: Update the RelevanceOrderBy type and its JSDoc to match
runtime behavior: change the _relevance.fields type from plain array to a
NonEmptyArray<NonRelationFields<Schema, Model>> so an empty fields list is
rejected at the type level, and revise the comment for _relevance to indicate
that relevance uses PostgreSQL similarity() (and that MySQL is not supported /
throws NotSupported at runtime) so IntelliSense reflects actual provider
constraints; locate the RelevanceOrderBy type and the _relevance field
declaration to make these edits.

In `@packages/orm/src/client/crud/dialects/base-dialect.ts`:
- Around line 1110-1131: The _relevance branch adds complex ordering but cursor
pagination still assumes simple {field: 'asc'|'desc'} entries; update handling
so cursor with a _relevance order is either rejected early or supported: modify
the code path that constructs cursor filters (function buildCursorFilter) to
detect order entries where field === '_relevance' (created via
buildRelevanceOrderBy / buildFieldRef / negateSort) and generate a comparison
that first compares computed relevance (value.search against the same fields)
then applies a deterministic tie-breaker (e.g., primary key) in the same sort
direction, or alternatively throw a clear validation error when a cursor is
supplied alongside an _relevance order; ensure tests cover both rejection and
correct SQL generation if you implement support.

In `@packages/orm/src/client/zod/factory.ts`:
- Around line 1180-1192: The _relevance.fields enum is currently built from all
scalar fields (scalarFieldNames) which allows non-string types; change the
scalarFieldNames computation in the getModelFields/filter pipeline to include
only string-typed scalar fields (e.g., filter by the field metadata indicating
type === 'String' or equivalent in your field definition) so that
_relevance.fields contains only string fields, and keep the z.enum(...) usage
but fed from the new string-only scalarFieldNames; update the code around
getModelFields, scalarFieldNames, and the _relevance strictObject construction
to reflect this restriction.

---

Nitpick comments:
In `@packages/orm/src/client/crud/dialects/postgresql.ts`:
- Around line 561-590: Add runtime checks for the required PostgreSQL extensions
and throw a clear user-facing error if missing: implement an internal check
(e.g., ensurePostgresExtensionsAvailable) that queries pg_extension for
'pg_trgm' and 'unaccent' and call it from the PostgreSQL dialect initialization
or lazily before using fuzzy features; update buildFuzzyFilter,
buildFuzzyContainsFilter, and buildRelevanceOrderBy to call this check (or
ensure it's called beforehand) and throw a createNotSupportedError-style error
with a clear message and remediation steps if either extension is absent.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 5c89d679-7173-415f-83ce-5738308b98ee

📥 Commits

Reviewing files that changed from the base of the PR and between 39a0a28 and 62fc9d7.

📒 Files selected for processing (12)
  • packages/orm/src/client/constants.ts
  • packages/orm/src/client/crud-types.ts
  • packages/orm/src/client/crud/dialects/base-dialect.ts
  • packages/orm/src/client/crud/dialects/mysql.ts
  • packages/orm/src/client/crud/dialects/postgresql.ts
  • packages/orm/src/client/crud/dialects/sqlite.ts
  • packages/orm/src/client/zod/factory.ts
  • tests/e2e/orm/client-api/fuzzy-search.test.ts
  • tests/e2e/orm/schemas/basic/input.ts
  • tests/e2e/orm/schemas/basic/models.ts
  • tests/e2e/orm/schemas/basic/schema.ts
  • tests/e2e/orm/schemas/basic/schema.zmodel

- _relevance.fields restreint aux champs String dans le schéma Zod
- Rejet du cursor pagination combiné avec _relevance ordering
- Type RelevanceOrderBy restreint aux StringFields avec tuple non-vide
- JSDoc mis à jour pour refléter le support PostgreSQL uniquement
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant