Files
mcpedia/content/notes/postgres/full-text-search.md
T

2.0 KiB

id, title, type, tags, status, author, created_at, updated_at
id title type tags status author created_at updated_at
postgres-full-text-search PostgreSQL Full-Text Search documentation
postgres
fts
tsvector
search
published asep 2026-08-20 2026-08-20

PostgreSQL Full-Text Search

MCPedia's keyword search is backed by PostgreSQL's native full-text search (FTS), not an external engine. The documents table carries a generated tsvector column that combines the title (weight A) and body (weight B).

Generated search vector

The column is generatedAlwaysAs, so it is always consistent with the row and needs no trigger:

searchVector: tsvector("search_vector")
  .notNull()
  .generatedAlwaysAs(
    sql`setweight(to_tsvector('simple', coalesce(${documents.title}, '')), 'A') ||
        setweight(to_tsvector('simple', coalesce(${documents.body}, '')), 'B')`,
  ),

The 'simple' config disables stemming, so mixed identifier/English queries (e.g. websocket, tsvector) match literally. A GIN index on searchVector keeps lookups fast.

Query + ranking

Search parses the user query with websearch_to_tsquery (or plainto_tsquery), then ranks with ts_rank:

SELECT *, ts_rank(search_vector, q) AS rank
FROM documents, websearch_to_tsquery('simple', $1) q
WHERE search_vector @@ q
ORDER BY rank DESC;
Config Stemming Use case
simple none Identifiers, ports, exact codes
english yes Natural-language body text
websearch yes Google-like queries ("a" OR b)

A headline snippet for the UI comes from ts_headline, which bolds the matched lexemes.

Hybrid fusion

Semantic search (embedding cosine) and FTS are fused with Reciprocal Rank Fusion (RRF) in packages/search. Each result set is ranked, scored 1/(k + rank), and the summed scores re-rank the union — no cross-score normalization needed, which is robust when the two signals live on different scales.