tin extension adds an inverted index type built for search, BM25 ranking, and the TINQL query language.
A search engine inside Postgres
A TIN index is a normal Postgres index, so searches see a consistent snapshot while writes continue. You also get ranking, highlighting, exact counts, and a query language that can express phrases, proximity, and spans. TIN offers richer retrieval features and drastically better performance than typical Postgres search extensions and external services.Compared with other Postgres search
TINQL is a broader retrieval language thantsquery or pg_textsearch, and TIN is substantially faster than pg_textsearch and ParadeDB pg_search on the count and top-k workloads those engines can run.
An external engine such as Elasticsearch has a similar feature set, but requires a second cluster and a sync path with Postgres.
Key features
- The
==>operator.WHERE body ==> '…'searches a TIN-indexed column with a TINQL string.==>is boolean: the row matches if the text satisfies the query. - TIN index. Create a TIN index with
CREATE INDEX ... USING tinon atextcolumn. - BM25 ranking with top-k retrieval.
tin.score(ctid)returns the BM25 relevance of each matching row, andORDER BY tin.score(ctid) DESC LIMIT 10returns the 10 best documents in score order. The same row gets the same score however the query is executed.tin.max_score(ctid)gives a denominator for normalized relevance, andk1andbcan be tuned per index or per query. - The TINQL query language. One query string expresses phrases with word gaps, ordered and unordered proximity (
THEN/5,NEAR/5), span relations (ENCLOSES,OVERLAPPING,BEFORE,AFTER), positional filters (IN FIRST 100 WORDS), wildcards, fuzzy terms, regular expressions, term ranges,AT LEAST 2 OF, and boosts. Every expression produces spans, so the operators compose freely. - Exact counts.
count(*)withWHERE body ==> '…'is answered from the index and stays exact under concurrent writes,VACUUM, and on read replicas. - Highlighting.
tin.highlightadds configurable markers around the text that produced the match. When highlighted, a match onpineapplereturns'<b>pineapple</b>'.tin.highlight_ansidoes the same for terminals. - Cross-column scoring. Search several TIN-indexed columns in one query, and
tin.score(ctid)combines relevance across them. - Configurable tokenization. The default tokenizer runs on indexed columns and queries, splitting text on Unicode word boundaries, folding case and accents, and indexes emoji as terms.
- Parallel build and parallel query. Index builds use parallel workers, and based on cost, queries and
count(*)can run in parallel too.
TIN example
Create a table, insert some rows, index the text column, and query it:fuji apple, has citrus within five words of melon, and excludes peel. Row 3 has all three terms but is excluded by AND NOT peel, and rows 4 and 5 lack the phrase. tin.score(ctid) ranks the matches by BM25 relevance, and tin.highlight(body) adds <b>/</b> tags around the text that produced the match.
Hybrid search with pgvector
Hybrid search requires a keyword (lexical) search and a meaning (semantic) search over the same documents, then combining the two ranked lists. TIN does the lexical half. Semantic search needs an embedding model outside Postgres and a vector index on the same table. The model embeds each document when you write it, and embeds the user’s query at search time. pgvector stores those vectors and returns the nearest rows.id from either query, and treating an id that appears in both as a stronger match.
Next steps
- Get started — install the extension, index a column, run your first query
- Scoring — BM25 relevance and top-k retrieval
- Highlighting — mark the text that matched
- TINQL — the query language
- Operational guidance — vacuum settings for tables with churn
- Reference — index options and the SQL API

