posts has TIN indexes on title and body and btree indexes on author_id and created_at. authors has a TIN index on bio.
Ranked results
ORDER BY tin.score(ctid) DESC LIMIT k is the shape TIN is built around. It returns the k best rows in score order and stops as soon as it has them. Passing the query as a parameter keeps user input out of the SQL text.
tin.max_score(ctid) is the best score among the matches and has the same value on every row, so dividing by it gives a relative score between 0 and 1. That is the number to show in a relevance bar, or to compare against a threshold when you want to hide weak matches.
Filters and counts
tin.score, ==> is an ordinary boolean filter. A count(*) over a ==> predicate is answered from the index.
Several TIN-indexed columns
==> per column. tin.score(ctid) adds up the relevance from every column that matched. The boost on the title query makes a title hit count twice as much as a body hit, which is the usual way to say that one field matters more than another.
OR, a row qualifies when either column matches, and its score sums whatever matched. This is the shape for a single search box that should look in several fields.
Filtering on an unindexed column
ups has no index. TIN finds the rows that match the text and the ups condition is checked on each of them, so it costs one comparison per candidate and needs no index of its own. When the filter rejects most of the text matches, the search has to look further down the ranking to fill the limit, so this shape suits filters that keep a reasonable share of the matches.
Combining with btree indexes
OR, each index contributes its own rows and the result is the union without duplicates. An OR costs about what its more expensive side costs, so a cheap phrase plus a primary-key lookup stays cheap.
Joins with a score on each side
tin.score takes the ctid of the relation it scores, so a join can carry one score per side. Each side needs its own ==> predicate. A side with no text predicate has nothing to score, and the query is refused. The two scores are independent BM25 values from two different indexes, so combine them deliberately: add them, weight one, or order by one and display the other.
==> can be a column from the outer query, so here the search is bound once per author using that author’s topics as the TINQL query. A LATERAL subquery with ORDER BY tin.score(ctid) DESC LIMIT k returns the top k for each outer row. This is the shape behind “recommended for you” lists and any page that shows the best matches per user, per category, or per saved search.

