Install the extension
On PlanetScale, enable TIN with the SQL below. For a local or CI database, first set up Lead.
The database encoding must be UTF8 or SQL_ASCII. CREATE EXTENSION tin refuses other encodings (for example, LATIN1).
If CREATE EXTENSION fails with permission denied to create extension "tin" and Must be superuser to create this extension., your cluster needs an update before it can install TIN. Go to the Clusters page for your branch, find the “Cluster update available” indicator, and update your cluster. After the update completes, run CREATE EXTENSION again.
Upgrade the extension
When a new TIN version is available, update your cluster so it restarts onto the new library. Then, in every database that already has the extension, run:
The cluster update installs the new binary. ALTER EXTENSION updates the SQL catalog so new options and function signatures are visible. Changing analysis options such as stemming on a populated index also needs REINDEX.
Create a table and index
The example below creates a table, inserts rows, and creates a TIN index on the text column:
Each TIN index covers one text column (or a text-producing expression).
The default tokenizer folds case and accents and indexes emoji as terms, so Jalapeño and jalapeno match, and ”😀” is searchable. The same tokenizer applies to indexed columns and queries. Stemming is off by default. Add WITH (stemmer = 'en') so runs, running, and run match as the same term. See Stemming.
You can preview how a string is tokenized with tin.tokenize:
You can also create partial indexes, expression indexes, and set per-index BM25 and tokenization options with WITH (k1, b, tokenizer, stemmer, …). After changing analysis options on a populated index, REINDEX so existing rows are re-tokenized.
Your first queries
TINQL keywords are UPPERCASE. Lowercase tokens are terms. Quote a multi-word phrase (“fuji apple”).
Filter
Ranked results (BM25)
Order responses in a ranked list with tin.score. Add more sort keys after the score when equal scores must stay in a stable order:
To normalize scores against the query’s best match, divide by tin.max_score(ctid), which is constant for the scan and identical on every row:
tin.score and tin.max_score require a TIN index scan in the same query. Outside that context, they raise an error rather than returning NULL.
Count
count(*) over a TIN predicate is answered from the index, not a heap scan.
Highlight
tin.highlight adds markers around the text that produced the match. A match on apple returns '<b>apple</b>'.
tin.highlight is configurable, pass in additional arguments to customize the markers and perform the search.
Search across columns
Each TIN index covers one text column. Index every column you want to search, then combine ==> in SQL. tin.score(ctid) combines BM25 relevance across those fields for the row. To weight one column higher than another, use TINQL boost (^N) on that field’s query.
For example, name ==> 'fuji^1.5' makes a name match count 1.5 times an unboosted notes match.
Local development and CI
Lead is a Postgres extension for exercising TIN-compatible application SQL in local development and CI. It uses the tin extension name so you can test your application’s search queries without connecting to PlanetScale.
Lead supports Postgres 17 and 18 and is built from source. Follow the Lead README for build prerequisites, local setup, and packaging instructions for your test database.
Lead is intended for small development and test datasets. It scans table rows instead of maintaining TIN’s search index, so it is not suitable for production workloads or benchmarking TIN’s performance. Use TIN on PlanetScale for production search.
Common pitfalls
Need help?
Get help from the PlanetScale Support team, or join our Discord community to see how others are using PlanetScale.