Full text search in PolarDB requires three building blocks: converting documents into searchable tsvector values, converting user queries into tsquery values, and ranking or highlighting results. This page covers the functions behind each step.
Parsing documents
to_tsvector converts a text document into a tsvector — a sorted list of lexemes (stemmed word forms) together with their positions in the document.
to_tsvector([ config regconfig, ] document text) returns tsvector
How the parser works:
-
Splits the document into tokens and assigns each a type (word, number, punctuation, etc.).
-
Looks up each token in a chain of dictionaries determined by the text search configuration.
-
The first dictionary that recognizes the token emits one or more normalized lexemes. For example,
ratsbecomesratbecause the dictionary recognizesratsas a plural form. -
Tokens recognized as stop words (such as
a,on,it) are discarded — they occur too frequently to be useful. -
Tokens with no matching dictionary entry are also discarded (punctuation such as
-falls into this category).
SELECT to_tsvector('english', 'a fat cat sat on a mat - it ate a fat rats'); to_tsvector
-----------------------------------------------------
'ate':9 'cat':3 'fat':2,11 'mat':7 'rat':12 'sat':4
The config argument selects a text search configuration — a combination of parser, dictionaries, and token types to index. Omit it to use the database default.
Assigning weights to document fields
Use setweight to label lexemes with a weight (A, B, C, or D), typically to reflect the importance of different document sections (title, keywords, abstract, body). Ranking functions use these weights when scoring results.
Because to_tsvector(NULL) returns NULL, wrap nullable columns in coalesce:
UPDATE tt SET ti =
setweight(to_tsvector(coalesce(title,'')), 'A') ||
setweight(to_tsvector(coalesce(keyword,'')), 'B') ||
setweight(to_tsvector(coalesce(abstract,'')), 'C') ||
setweight(to_tsvector(coalesce(body,'')), 'D');
The || operator concatenates the labeled tsvector values into a single indexed representation.
Default weights used by ranking functions:
| Weight label | Default value |
|---|---|
| A | 1.0 |
| B | 0.4 |
| C | 0.2 |
| D | 0.1 |
Parsing queries
Four functions convert user input into a tsquery. Choose based on your input source:
| Function | Input format | Operators inserted | Never raises error | Use when |
|---|---|---|---|---|
to_tsquery |
Explicit operators required | As written | No | Building queries programmatically with full control |
plainto_tsquery |
Plain text | & (AND) between all terms |
No | Simple keyword search |
phraseto_tsquery |
Plain text | <-> (FOLLOWED BY) between terms |
No | Exact phrase search |
websearch_to_tsquery |
Web search syntax | Interpreted from syntax | Yes | Accepting raw user input from a search box |
to_tsquery
to_tsquery([ config regconfig, ] querytext text) returns tsquery
to_tsquery normalizes each token into a lexeme and discards stop words, just like to_tsvector. The input must already use tsquery operators: & (AND), | (OR), ! (NOT), and <-> (FOLLOWED BY). Parentheses are supported for grouping. Tokens not separated by a valid operator cause a syntax error.
SELECT to_tsquery('english', 'The & Fat & Rats'); to_tsquery
---------------
'fat' & 'rat'
Attach weight labels to restrict matching to specific labeled lexemes:
SELECT to_tsquery('english', 'Fat | Rats:AB'); to_tsquery
------------------
'fat' | 'rat':AB
Append * to a lexeme for prefix matching — it matches any word in a tsvector that begins with that string:
SELECT to_tsquery('supern:*A & star:A*B'); to_tsquery
--------------------------
'supern':*A & 'star':*AB
When the text search configuration includes a thesaurus dictionary, to_tsquery accepts single-quoted phrases. In this example the thesaurus maps supernovae stars to sn:
SELECT to_tsquery('''supernovae stars'' & !crab'); to_tsquery
---------------
'sn' & !'crab'
plainto_tsquery
plainto_tsquery([ config regconfig, ] querytext text) returns tsquery
plainto_tsquery accepts unformatted text and inserts & between all surviving words after normalization. It does not recognize tsquery operators, weight labels, or prefix-match labels in its input — all punctuation is treated as plain text and discarded.
SELECT plainto_tsquery('english', 'The Fat Rats'); plainto_tsquery
-----------------
'fat' & 'rat'SELECT plainto_tsquery('english', 'The Fat & Rats:C'); plainto_tsquery
---------------------
'fat' & 'rat' & 'c'
phraseto_tsquery
phraseto_tsquery([ config regconfig, ] querytext text) returns tsquery
phraseto_tsquery behaves like plainto_tsquery but inserts <-> (FOLLOWED BY) between surviving words, enforcing word order. Stop words are not simply discarded — they are accounted for by inserting <> operators rather than <-> operators to preserve positional accuracy.
SELECT phraseto_tsquery('english', 'The Fat Rats'); phraseto_tsquery
------------------
'fat' <-> 'rat'SELECT phraseto_tsquery('english', 'The Fat & Rats:C'); phraseto_tsquery
-----------------------------
'fat' <-> 'rat' <-> 'c'
websearch_to_tsquery
websearch_to_tsquery([ config regconfig, ] querytext text) returns tsquery
websearch_to_tsquery accepts web search syntax and never raises a syntax error, making it safe to pass raw user input directly. This is the recommended function for accepting input from a search box. Supported syntax:
-
Unquoted text — converted to
&-separated terms (likeplainto_tsquery) -
"Quoted text" — converted to
<->-separated terms (likephraseto_tsquery) -
OR— converted to| -
-(dash) — converted to!(NOT)
Other punctuation is ignored. Operators, weight labels, and prefix-match labels in the input are not recognized.
SELECT websearch_to_tsquery('english', 'The fat rats'); websearch_to_tsquery
----------------------
'fat' & 'rat'SELECT websearch_to_tsquery('english', '"supernovae stars" -crab'); websearch_to_tsquery
----------------------------------
'supernova' <-> 'star' & !'crab'SELECT websearch_to_tsquery('english', '"sad cat" or "fat rat"'); websearch_to_tsquery
-----------------------------------
'sad' <-> 'cat' | 'fat' <-> 'rat'SELECT websearch_to_tsquery('english', 'signal -"segmentation fault"'); websearch_to_tsquery
---------------------------------------
'signal' & !( 'segment' <-> 'fault' )SELECT websearch_to_tsquery('english', '""" )( dummy \\ query <->'); websearch_to_tsquery
----------------------
'dummi' & 'queri'
Ranking search results
ts_rank and ts_rank_cd score how well a document matches a query. Both consider lexical and structural information; that is, they consider how often the query terms appear in the document and how important is the part of the document where they occur. The score is application-relative — there is no universal scale.
ts_rank([ weights float4[], ] vector tsvector, query tsquery [, normalization integer ]) returns float4
Ranks vectors based on the frequency of matching lexemes.
ts_rank_cd([ weights float4[], ] vector tsvector, query tsquery [, normalization integer ]) returns float4
Also factors in the proximity of matching lexemes to each other (cover density). Requires positional information; if the tsvector has no unstripped lexemes, returns zero.
The weights argument
Pass a float4[] array to adjust the influence of each weight label:
{D-weight, C-weight, B-weight, A-weight}
Default: {0.1, 0.2, 0.4, 1.0}
Lexemes labeled A (typically the document title) contribute up to 10x more than D-labeled lexemes (body text).
The normalization option
By default, longer documents score higher simply because they contain more words. The normalization integer option adjusts for document length. It is a bit mask — combine values with |:
| Value | Effect |
|---|---|
0 (default) |
No length adjustment |
1 |
Divide rank by 1 + log(document length) |
2 |
Divide rank by document length |
4 |
Divide rank by mean harmonic distance between extents (ts_rank_cd only) |
8 |
Divide rank by number of unique words |
16 |
Divide rank by 1 + log(number of unique words) |
32 |
Divide rank by rank + 1 (scales all ranks to the range 0-1) |
When multiple flags are set, the transformations apply in the order listed.
The32normalization (rank/(rank+1)) scales ranks into the 0-1 range, but it does not affect the ordering of results — it is a cosmetic transformation only.
Example: top 10 matches
SELECT title, ts_rank_cd(textsearch, query) AS rank
FROM apod, to_tsquery('neutrino|(dark & matter)') query
WHERE query @@ textsearch
ORDER BY rank DESC
LIMIT 10; title | rank
-----------------------------------------------+----------
Neutrinos in the Sun | 3.1
The Sudbury Neutrino Detector | 2.4
A MACHO View of Galactic Dark Matter | 2.01317
Hot Gas and Dark Matter | 1.91171
The Virgo Cluster: Hot Plasma and Dark Matter | 1.90953
Rafting for Solar Neutrinos | 1.9
NGC 4650A: Strange Galaxy and Dark Matter | 1.85774
Hot Gas and Dark Matter | 1.6123
Ice Fishing for Cosmic Neutrinos | 1.6
Weak Lensing Distorts the Universe | 0.818218
With normalization=32:
SELECT title, ts_rank_cd(textsearch, query, 32 /* rank/(rank+1) */ ) AS rank
FROM apod, to_tsquery('neutrino|(dark & matter)') query
WHERE query @@ textsearch
ORDER BY rank DESC
LIMIT 10; title | rank
-----------------------------------------------+-------------------
Neutrinos in the Sun | 0.756097569485493
The Sudbury Neutrino Detector | 0.705882361190954
A MACHO View of Galactic Dark Matter | 0.668123210574724
Hot Gas and Dark Matter | 0.65655958650282
The Virgo Cluster: Hot Plasma and Dark Matter | 0.656301290640973
Rafting for Solar Neutrinos | 0.655172410958162
NGC 4650A: Strange Galaxy and Dark Matter | 0.650072921219637
Hot Gas and Dark Matter | 0.617195790024749
Ice Fishing for Cosmic Neutrinos | 0.615384618911517
Weak Lensing Distorts the Universe | 0.450010798361481
Ranking can be expensive since it requires consulting the tsvector of each matching document, which can be I/O bound and therefore slow. Unfortunately, it is almost impossible to avoid since practical queries often result in large numbers of matches.
Highlighting results
ts_headline returns an excerpt from the original document with query terms marked up.
ts_headline([ config regconfig, ] document text, query tsquery [, options text ]) returns text
ts_headline accepts a document along with a query, and returns an excerpt from the document in which terms from the query are highlighted. Specifically, the function will use the query to select relevant text fragments, and then highlight all words that appear in the query, even if those word positions do not match the query's restrictions. Unlike ranking functions, ts_headline works on the original document text, not a tsvector summary. This makes it accurate but slower — call it only after filtering the result set.
Omit config to use the default_text_search_config setting.
Options
Pass a comma-separated string of option=value pairs:
| Option | Type | Default | Description |
|---|---|---|---|
MaxWords |
integer | 35 | Maximum headline length in words |
MinWords |
integer | 15 | Minimum headline length in words |
ShortWord |
integer | 3 | Words of this length or shorter are dropped at headline boundaries unless they are query terms |
HighlightAll |
boolean | false | Use the entire document as the headline, ignoring MaxWords, MinWords, and ShortWord |
MaxFragments |
integer | 0 | Maximum number of fragments to display. 0 uses non-fragment mode; any positive value enables fragment mode |
StartSel |
string | <b> |
Opening delimiter for highlighted query terms |
StopSel |
string | </b> |
Closing delimiter for highlighted query terms |
FragmentDelimiter |
string | ... |
Separator between fragments when MaxFragments > 0 |
Option names are case-insensitive. Double-quote string values that contain spaces or commas.
Non-fragment mode (default, MaxFragments=0): selects the single best excerpt that contains the most query terms within the headline length limit.
Fragment mode (MaxFragments > 0): locates all query matches, splits each into fragments of at most MaxWords words, and prefers fragments with more query terms. More useful when matches span large sections of the document.
In either mode, if no query terms are found, the first MinWords words of the document are returned.
Examples
SELECT ts_headline('english',
'The most common type of search
is to find all documents containing given query terms
and return them in order of their similarity to the
query.',
to_tsquery('english', 'query & similarity')); ts_headline
------------------------------------------------------------
containing given <b>query</b> terms +
and return them in order of their <b>similarity</b> to the+
<b>query</b>.SELECT ts_headline('english',
'Search terms may occur
many times in a document,
requiring ranking of the search matches to decide which
occurrences to display in the result.',
to_tsquery('english', 'search & term'),
'MaxFragments=10, MaxWords=7, MinWords=3, StartSel=<<, StopSel=>>'); ts_headline
------------------------------------------------------------
<<Search>> <<terms>> may occur +
many times ... ranking of the <<search>> matches to decide