Skip to content
NewComposite primary keys6 min read

Full-Text Search

$text searches natural-language text using each database’s own full-text engine. It goes at the top level of $where, alongside regular field conditions.

You write
import { pool } from './uql.config.js';
import { Item } from './shared/models/index.js';
const items = await pool.findMany(Item, {
$where: {
$text: { $fields: ['name', 'description'], $value: 'wireless keyboard' },
isActive: true,
},
});

$fields lists the columns to search; $value is the user’s query text.

Without $fields, $text searches the columns of the entity’s fulltext index, declared with @Index(['name', 'description'], { type: 'fulltext' }). On SQL, an entity with no such index, or more than one, has to name $fields. PostgreSQL has no fulltext index type and its migrations refuse one, so name $fields there.

SELECT * FROM "Item"
WHERE TO_TSVECTOR("name" || ' ' || "description") @@ WEBSEARCH_TO_TSQUERY($1)
AND "isActive" = $2

MongoDB maps $text to its own $text operator ({ $text: { $search: value } }).

$config selects the text-search configuration (the dictionary that drives stemming and stop-words) for both the document and the query. It defaults to the server’s default_text_search_config.

You write
const items = await pool.findMany(Item, {
$where: {
$text: { $fields: ['name'], $value: 'running shoes', $config: 'english' },
},
});
SELECT * FROM "Item" WHERE TO_TSVECTOR($1::regconfig, "name") @@ WEBSEARCH_TO_TSQUERY($1::regconfig, $2)

The value is bound as a parameter (never interpolated) and its placeholder is reused by both calls. Other dialects ignore $config.

Full-text search needs dialect-specific setup; $text generates the query, not the index:

Dialect Requirement
PostgreSQL / CockroachDB Works without an index, but TO_TSVECTOR(col) computed per row cannot use one. For large tables, add a stored tsvector column (or expression index) with a GIN index.
MySQL / MariaDB Requires a FULLTEXT index covering exactly the columns listed in $fields, in that order; otherwise the query errors with “Can’t find FULLTEXT index matching the column list”. Declare it with @Index(['title', 'body'], { type: 'fulltext' }) and UQL generates CREATE FULLTEXT INDEX for you.
SQLite Requires an FTS5 virtual table; MATCH only applies to one.
MongoDB Requires a text index, which declares the fields it covers, so $fields is accepted for API consistency and ignored, as $distance is for $vectorSearch.
MSSQL Not supported. CONTAINS needs a full-text catalogue, which UQL does not create, so $text is refused.