PostgreSQL: Motor de Busca Textual do PostgreSQL

Última atualização: 2026-08-26

1. O Que Você Vai Aprender


2. A História

A plataforma de e-commerce do Charlie tem 500.000 registros de produtos e os usuários precisam buscar produtos. Ele começou com LIKE '%red shoes%', mas buscar "red shoes" só retornava produtos cuja descrição continha exatamente a string consecutiva "red shoes" e a consulta levava 3 segundos. Após migrar para a busca textual do PostgreSQL:


3. Conceito: Conceitos Principais da Busca Textual

(1) Quatro Conceitos Principais

Conceito Descrição Equivalente no PostgreSQL
Documento O conteúdo de texto a ser buscado tsvector
Consulta Os termos de busca do usuário tsquery
Dicionário Define como as palavras são processadas (stop words, stemming) catálogo do sistema pg_ts_dict
Configuração Combina um analisador e dicionários catálogo do sistema pg_ts_config

(2) Pipeline de Processamento da Busca Textual

100%
flowchart LR
    A[Texto Bruto] --> B[Analisador<br/>Tokenizar]
    B --> C[Dicionário<br/>Stemming + Stop words]
    C --> D[tsvector<br/>Lexemas ordenados]
    D --> E[Índice GIN<br/>Busca rápida]
    F[Consulta do Usuário] --> G[to_tsquery<br/>Analisar e normalizar]
    G --> H[tsquery<br/>Lexemas + operadores]
    E --> I["Operador @@<br/>Correspondência"]
    H --> I
    I --> J[ts_rank<br/>Pontuação]
    J --> K[ts_headline<br/>Destaque]
    K --> L[Resultados]

▶ Exemplo: Veja Como o Texto É Processado

SQL
SELECT * FROM ts_debug('english', 'The red shoes are beautiful');
TEXT 📖 Somente leitura
 alias | descriptor | token  |  dictionaries  | dictionary | lexemes 
-------+------------+--------+----------------+------------+---------
 ascii | word       | The    | {english_stem}  | english_stem| {}
 ascii | word       | red    | {english_stem}  | english_stem| {red}
 ascii | word       | shoes  | {english_stem}  | english_stem| {shoe}
 ascii | word       | are    | {english_stem}  | english_stem| {}
 ascii | word       | beautiful | {english_stem}| english_stem| {beauti}

4. Conceito: tsvector e tsquery

(1) tsvector — O Resultado Pré-processado de um Documento

tsvector é uma lista ordenada e sem duplicatas de lexemas, cada um com informação posicional.

▶ Exemplo: Converter Texto para tsvector

SQL
SELECT to_tsvector('english', 'The red shoes are beautiful shoes');
TEXT 📖 Somente leitura
'beauti':5 'red':2 'shoe':3,6
Etapa Entrada Saída
Tokenizar The red shoes are beautiful shoes [The, red, shoes, are, beautiful, shoes]
Remover stop words [The, red, shoes, are, beautiful, shoes] [red, shoes, beautiful, shoes]
Stemming [red, shoes, beautiful, shoes] [red, shoe, beauti, shoe]
Remover duplicatas + posição [red, shoe, beauti, shoe] 'beauti':5 'red':2 'shoe':3,6

(2) tsquery — O Resultado Pré-processado de uma Consulta

tsquery é uma combinação de lexemas e operadores booleanos.

Operador Significado Exemplo
& AND red & shoe
| OR red | blue
! NOT !broken
<-> SEGUIDO POR (adjacente) red <-> shoe
`<N>` Distância N red <2> shoe

▶ Exemplo: Construir um tsquery

SQL
SELECT to_tsquery('english', 'red & shoe');
-- 'red' & 'shoe'

SELECT to_tsquery('english', 'red | blue');
-- 'red' | 'blue'

Output:

TEXT 📖 Somente leitura
 id | name     | value 
----+----------+-------
  1 | example  | 42
(1 row)

5. Conceito: Funções de Consulta de Busca Textual

(1) Cinco Funções de Construção de Consulta

Função Versão PG Formato de entrada Descrição
to_tsquery(config, text) todas red & shoe Você escreve operadores manualmente
plainto_tsquery(config, text) todas red shoes Texto simples, unido automaticamente com &
phraseto_tsquery(config, text) 9.6+ red shoes Texto simples, unido automaticamente com <->
websearch_to_tsquery(config, text) 11+ "red shoes" -broken Sintaxe de motor de busca web
tsvector @@ tsquery todas Operador de correspondência

▶ Exemplo: plainto_tsquery — Texto Simples Auto-AND

SQL
SELECT plainto_tsquery('english', 'red shoes');
-- 'red' & 'shoe'  (encontra docs com AMBOS os termos)

Output:

TEXT 📖 Somente leitura
 id | name     | value 
----+----------+-------
  1 | example  | 42
(1 row)

▶ Exemplo: phraseto_tsquery — Busca por Frase

SQL
SELECT phraseto_tsquery('english', 'red shoes');
-- 'red' <-> 'shoe'  (encontra docs onde 'red' está imediatamente antes de 'shoe')

Output:

TEXT 📖 Somente leitura
 id | name     | value 
----+----------+-------
  1 | example  | 42
(1 row)

▶ Exemplo: websearch_to_tsquery — Sintaxe de Motor de Busca

SQL
SELECT websearch_to_tsquery('english', '"red shoes" -broken OR new');
-- 'red' <-> 'shoe' & !'broken' | 'new'

Output:

TEXT 📖 Somente leitura
 id | name     | value 
----+----------+-------
  1 | example  | 42
(1 row)
Sintaxe websearch Significado
"frase exata" Correspondência de frase (<->)
-palavra Excluir (!)
palavra1 OR palavra2 OR (|)
palavra1 palavra2 AND (&)

▶ Exemplo: Buscar com o Operador @@

SQL
CREATE TABLE catalog (
  product_id SERIAL PRIMARY KEY,
  name TEXT NOT NULL,
  description TEXT,
  search_vector tsvector
);

INSERT INTO catalog (name, description, search_vector) VALUES
  ('Red Running Shoes', 'Lightweight red shoes for running', 
   to_tsvector('english', 'Lightweight red shoes for running')),
  ('Blue Casual Shoes', 'Comfortable blue shoes for daily wear',
   to_tsvector('english', 'Comfortable blue shoes for daily wear')),
  ('Red Dress', 'Elegant red dress for special occasions',
   to_tsvector('english', 'Elegant red dress for special occasions'));

-- Buscar por "red shoes"
SELECT name, description
FROM catalog
WHERE search_vector @@ plainto_tsquery('english', 'red shoes');

Output:

TEXT 📖 Somente leitura
INSERT 0 1

6. Conceito: Ranqueamento e Destaque

(1) ts_rank Ranqueamento

ts_rank calcula a relevância de uma consulta para um documento, retornando um float de 0 a 1 — quanto maior o valor, mais relevante.

▶ Exemplo: Ordenar por Relevância

SQL
SELECT name,
       ts_rank(search_vector, plainto_tsquery('english', 'red shoes')) AS rank
FROM catalog
WHERE search_vector @@ plainto_tsquery('english', 'red shoes')
ORDER BY rank DESC;

Output:

TEXT 📖 Somente leitura
 id | name     | value 
----+----------+-------
  1 | example  | 42
(1 row)
Função de ranqueamento Descrição Caso de uso
ts_rank(vector, query) Ranqueamento baseado em frequência Ranqueamento geral
ts_rank_cd(vector, query) Ranqueamento de densidade de cobertura Ranqueamento mais preciso para docs curtos
ts_rank(vector, query, weights) Ranqueamento ponderado Título com peso maior que corpo

▶ Exemplo: Ranqueamento Ponderado (título acima da descrição)

SQL
ALTER TABLE catalog ADD COLUMN title_vector tsvector;

UPDATE catalog SET title_vector = to_tsvector('english', name);

-- Definir pesos: title=D(padrão), body=A(mais alto)
SELECT name,
       ts_rank(
         setweight(title_vector, 'A') || setweight(search_vector, 'B'),
         plainto_tsquery('english', 'red shoes')
       ) AS rank
FROM catalog
WHERE setweight(title_vector, 'A') || setweight(search_vector, 'B')
      @@ plainto_tsquery('english', 'red shoes')
ORDER BY rank DESC;

Output:

TEXT 📖 Somente leitura
UPDATE 3
Letra de peso Peso padrão Uso típico
A 1.0 Título
B 0.4 Corpo
C 0.2 Resumo
D 0.1 Informação suplementar

(2) ts_headline Destaque

ts_headline marca lexemas correspondentes no texto original.

▶ Exemplo: Destacar Resultados da Busca

SQL
SELECT name,
       ts_headline('english', description, plainto_tsquery('english', 'red shoes')) AS highlighted
FROM catalog
WHERE search_vector @@ plainto_tsquery('english', 'red shoes');
TEXT 📖 Somente leitura
 name              | highlighted
-------------------+-------------------------------------------------
 Red Running Shoes | Lightweight <b>red</b> <b>shoes</b> for running

▶ Exemplo: Tags de Destaque Personalizadas

SQL
SELECT ts_headline(
  'english',
  description,
  plainto_tsquery('english', 'red shoes'),
  'StartSel=<em>,StopSel=</em>,MaxWords=10,MinWords=5'
) AS highlighted
FROM catalog
WHERE search_vector @@ plainto_tsquery('english', 'red shoes');

Output:

TEXT 📖 Somente leitura
 id | name     | value 
----+----------+-------
  1 | example  | 42
(1 row)
Parâmetro de destaque Descrição Padrão
StartSel Tag de início da correspondência <b>
StopSel Tag de fim da correspondência </b>
MaxWords Máximo de palavras no fragmento de destaque 35
MinWords Mínimo de palavras no fragmento de destaque 15
ShortWord Comprimento mínimo de palavra a ignorar 3

7. Conceito: Acelerando com Índices GIN

(1) Três Abordagens de Índice GIN

Abordagem Sintaxe Sobrecarga de atualização Melhor para
Baseada em coluna gin(to_tsvector(config, column)) Reconstruído na atualização Melhor quando valores de coluna não mudam
Baseada em coluna armazenada gin(search_vector) Menor sobrecarga de atualização Atualizações de alta frequência, precisa de trigger sync
FASTUPDATE Ativado por padrão Mesclagem adiada Cenários com muita escrita e leitura

▶ Exemplo: Índice GIN Baseado em Função

SQL
CREATE INDEX idx_catalog_desc_fts
ON catalog USING gin (to_tsvector('english', description));

-- A consulta deve usar a mesma config
SELECT name FROM catalog
WHERE to_tsvector('english', description) @@ plainto_tsquery('english', 'red shoes');

Output:

TEXT 📖 Somente leitura
CREATE TABLE

▶ Exemplo: Índice GIN em Coluna tsvector Armazenada

SQL
CREATE INDEX idx_catalog_search_fts ON catalog USING gin (search_vector);

-- A consulta usa coluna armazenada
SELECT name FROM catalog
WHERE search_vector @@ plainto_tsquery('english', 'red shoes');

Output:

TEXT 📖 Somente leitura
CREATE TABLE

▶ Exemplo: Trigger para Sincronizar Automaticamente a Coluna tsvector

SQL
CREATE FUNCTION catalog_search_vector_update() RETURNS trigger AS $$
BEGIN
  NEW.search_vector :=
    setweight(to_tsvector('english', COALESCE(NEW.name, '')), 'A') ||
    setweight(to_tsvector('english', COALESCE(NEW.description, '')), 'B');
  RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER trg_catalog_search
BEFORE INSERT OR UPDATE OF name, description ON catalog
FOR EACH ROW EXECUTE FUNCTION catalog_search_vector_update();

Output:

TEXT 📖 Somente leitura
INSERT 0 1
Abordagem Estilo de consulta Índice usado Custo de manutenção
Índice de função to_tsvector('english', col) @@ query Sim Zero (automático)
Coluna armazenada + índice stored_col @@ query Sim Precisa de trigger sync
Sem índice to_tsvector('english', col) @@ query Varredura completa da tabela Zero

8. Conceito: Configuração Multilíngue

(1) Configurações Integradas do PostgreSQL

Config Idioma Descrição
english Inglês Padrão; suporta stemming e stop words
simple Sem processamento de idioma Apenas tokenizar; sem stop words, sem stemming
zhpinyin Pinyin chinês Extensão PG; busca chinês-para-pinyin

▶ Exemplo: Config simple Não Filtra Stop Words

SQL
SELECT to_tsvector('simple', 'The red shoes');
-- 'red':2 'shoes':3 'the':1  (mantém 'the', sem stemming)

Output:

TEXT 📖 Somente leitura
 id | name     | value 
----+----------+-------
  1 | example  | 42
(1 row)

▶ Exemplo: Comparar Comportamento de Busca entre Configurações

Config to_tsvector('the red shoes are nice') Característica
english 'nice':5 'red':2 'shoe':3 Remove stop words + stemming
simple 'are':4 'nice':5 'red':2 'shoes':3 'the':1 Apenas minúsculas + tokenizar
zhpinyin Depende da implementação da extensão Chinês para pinyin

(2) Opções de Busca Textual em Chinês

O PostgreSQL não vem com um tokenizador nativo para chinês. Abordagens comuns:

Abordagem Extensão Tokenização Complexidade de instalação
zhparser Extensão PostgreSQL Baseada em SimpleChineseSeg Média
pg_jieba Extensão PostgreSQL Baseada no tokenizador jieba Média
zhpinyin Extensão PostgreSQL Busca por pinyin Baixa
Tokenização na camada de aplicação Nenhuma App gera tsvector Baixa

▶ Exemplo: Tokenização na Camada de Aplicação (sem necessidade de extensão)

SQL
-- Pré-segmentar texto chinês com espaços na camada de aplicação
INSERT INTO catalog (name, description, search_vector)
VALUES (
  'Tênis de Corrida Vermelho',
  'tênis de corrida vermelho leve para corrida',
  to_tsvector('simple', 'tênis de corrida vermelho leve para corrida')
);

-- Buscar com consulta segmentada
SELECT name FROM catalog
WHERE search_vector @@ to_tsquery('simple', 'tênis & corrida');

Output:

TEXT 📖 Somente leitura
INSERT 0 1

▶ Exemplo: Busca Mista Chinês-Inglês

SQL
CREATE TABLE articles (
  article_id SERIAL PRIMARY KEY,
  title TEXT NOT NULL,
  content TEXT,
  fts_en tsvector,
  fts_zh tsvector
);

-- Config english para conteúdo em inglês, simple para chinês
UPDATE articles SET
  fts_en = to_tsvector('english', COALESCE(title, '') || ' ' || COALESCE(content, '')),
  fts_zh = to_tsvector('simple', COALESCE(title, '') || ' ' || COALESCE(content, ''));

-- Buscar em inglês
SELECT title FROM articles
WHERE fts_en @@ plainto_tsquery('english', 'database optimization');

-- Buscar em chinês (segmentado pela aplicação)
SELECT title FROM articles
WHERE fts_zh @@ to_tsquery('simple', 'data & optimization');

Output:

TEXT 📖 Somente leitura
UPDATE 3

9. Conceito: Estatísticas e Catálogos do Sistema

(1) ts_stat para Frequência de Palavras

ts_stat ajuda a analisar a distribuição de lexemas em um tsvector — comumente usado para ajustar a configuração de busca.

▶ Exemplo: Visualizar os Lexemas Mais Comuns

SQL
SELECT word, ndoc, nentry
FROM ts_stat(
  'SELECT search_vector FROM catalog'
)
ORDER BY nentry DESC
LIMIT 10;

Output:

TEXT 📖 Somente leitura
 id | name     | value 
----+----------+-------
  1 | example  | 42
(1 row)
Coluna Descrição
word Lexema
ndoc Em quantos documentos aparece
nentry Número total de ocorrências

(2) Catálogos do Sistema

▶ Exemplo: Visualizar Configurações Disponíveis

SQL
SELECT cfgname FROM pg_ts_config;

Output:

TEXT 📖 Somente leitura
 id | name     | value 
----+----------+-------
  1 | example  | 42
(1 row)

▶ Exemplo: Visualizar Dicionários Disponíveis

SQL
SELECT dictname, dictnamespace FROM pg_ts_dict;

Output:

TEXT 📖 Somente leitura
 id | name     | value 
----+----------+-------
  1 | example  | 42
(1 row)

▶ Exemplo: Comparação de Desempenho LIKE vs Busca Textual

SQL
-- Criar dados de teste
INSERT INTO catalog (name, description, search_vector)
SELECT
  'Product ' || i,
  'High quality product number ' || i || ' with great features and amazing design',
  to_tsvector('english', 'High quality product number ' || i || ' with great features and amazing design')
FROM generate_series(1, 100000) AS i;

-- Consulta LIKE (lenta, sem suporte de índice para curinga inicial)
EXPLAIN ANALYZE
SELECT name FROM catalog WHERE description LIKE '%great features%';

-- Busca textual com índice GIN (rápida)
CREATE INDEX idx_catalog_fts ON catalog USING gin (search_vector);
EXPLAIN ANALYZE
SELECT name FROM catalog
WHERE search_vector @@ plainto_tsquery('english', 'great features');

Output:

TEXT 📖 Somente leitura
INSERT 0 1
Método de consulta Tempo Suporte de índice Capacidade
LIKE '%keyword%' ~3000ms Nenhum Correspondência exata de substring
LIKE 'keyword%' ~5ms B-tree Correspondência de prefixo
@@ busca textual + GIN ~30ms GIN Correspondência de lexema + stemming + ranqueamento

10. Prática: Motor de Busca de Produtos de E-commerce

Charlie precisa implementar uma busca completa de produtos para a plataforma de e-commerce, incluindo busca em múltiplos campos, ranqueamento, destaque e paginação.

SQL
-- Etapa 1: Criar tabela de produtos com tsvector pré-computado
CREATE TABLE shop_products (
  product_id SERIAL PRIMARY KEY,
  name TEXT NOT NULL,
  category TEXT NOT NULL,
  description TEXT,
  price NUMERIC(10,2) NOT NULL,
  search_doc tsvector GENERATED ALWAYS AS (
    setweight(to_tsvector('english', COALESCE(name, '')), 'A') ||
    setweight(to_tsvector('english', COALESCE(category, '')), 'B') ||
    setweight(to_tsvector('english', COALESCE(description, '')), 'C')
  ) STORED
);

-- Etapa 2: Inserir dados de exemplo
INSERT INTO shop_products (name, category, description, price) VALUES
  ('Red Running Shoes', 'Shoes', 'Lightweight red running shoes with cushioned sole', 89.99),
  ('Blue Casual Shoes', 'Shoes', 'Comfortable blue shoes for everyday casual wear', 59.99),
  ('Red Leather Dress Shoes', 'Shoes', 'Premium red leather dress shoes for formal events', 149.99),
  ('Red Silk Dress', 'Dresses', 'Elegant red silk dress for special occasions', 199.99),
  ('Running Watch', 'Accessories', 'GPS running watch with heart rate monitor', 249.99),
  ('Running Shorts', 'Clothing', 'Breathable running shorts with pockets', 39.99);

-- Etapa 3: Criar índice GIN na coluna gerada
CREATE INDEX idx_shop_products_search ON shop_products USING gin (search_doc);

-- Etapa 4: Função de busca com ranqueamento, destaque e paginação
CREATE FUNCTION search_products(
  p_query TEXT,
  p_limit INT DEFAULT 20,
  p_offset INT DEFAULT 0
) RETURNS TABLE (
  product_id INT,
  name TEXT,
  category TEXT,
  price NUMERIC,
  rank REAL,
  headline TEXT
) AS $$
BEGIN
  RETURN QUERY
  SELECT
    s.product_id,
    s.name,
    s.category,
    s.price,
    ts_rank(s.search_doc, websearch_to_tsquery('english', p_query)) AS rank,
    ts_headline(
      'english',
      COALESCE(s.name, '') || '. ' || COALESCE(s.description, ''),
      websearch_to_tsquery('english', p_query),
      'StartSel=<mark>,StopSel=</mark>,MaxWords=20,MinWords=5'
    ) AS headline
  FROM shop_products s
  WHERE s.search_doc @@ websearch_to_tsquery('english', p_query)
  ORDER BY rank DESC
  LIMIT p_limit
  OFFSET p_offset;
END;
$$ LANGUAGE plpgsql;

-- Etapa 5: Testar busca
SELECT * FROM search_products('red shoes');
SELECT * FROM search_products('"running shoes"');
SELECT * FROM search_products('running -shoes');

-- Etapa 6: Buscar com filtro de categoria
SELECT name, price,
       ts_rank(search_doc, plainto_tsquery('english', 'red')) AS rank
FROM shop_products
WHERE search_doc @@ plainto_tsquery('english', 'red')
  AND category = 'Shoes'
ORDER BY rank DESC;

❓ Perguntas Frequentes

P: Qual é a diferença essencial entre busca textual e LIKE? R: LIKE é uma correspondência exata de substring — não entende semântica. A busca textual tokeniza, remove stop words e aplica stemming, correspondendo por lexema com combinações booleanas e ranqueamento. A busca textual tem suporte de índice GIN; LIKE com curinga inicial não pode usar índice.

P: Qual devo usar, to_tsquery ou plainto_tsquery? R: Se os usuários digitam termos de busca diretamente, use plainto_tsquery ou websearch_to_tsquery (PG 11+), que convertem automaticamente texto simples em consulta. to_tsquery precisa que você escreva operadores & | ! manualmente — melhor para consultas geradas programaticamente.

P: Por que devo usar a mesma config para o índice e a consulta? R: Configs diferentes tokenizam e aplicam stemming de forma diferente. Se o índice usa config english mas a consulta usa simple, os lexemas não corresponderão e a busca não retorna nada. Eles devem permanecer consistentes.

P: O que é melhor, colunas GENERATED ALWAYS AS ... STORED ou triggers? R: Colunas GENERATED (PG 12+) são computadas automaticamente — sem necessidade de trigger — e são recomendadas. Se você está no PG abaixo da 12 ou precisa combinar tsvectors entre tabelas, use um trigger.

P: Para que serve a informação posicional no tsvector? R: A informação posicional é usada para busca por frase (phraseto_tsquery usa o operador <-> para corresponder lexemas adjacentes) e busca por proximidade (operador de distância `<N>`). Se você não precisa de busca por frase, pode descartar posições no to_tsvector.

P: Um índice GIN afeta o desempenho de escrita? R: Sim. Atualizações de índice GIN são mais lentas que B-tree porque listas invertidas devem ser mescladas. O PostgreSQL ativa FASTUPDATE por padrão, armazenando atualizações em buffer e depois mesclando em lotes — trocando um pouco de latência de consulta em tempo real por melhor desempenho de escrita.

P: Como faço busca textual em chinês? R: O PostgreSQL não suporta nativamente tokenização em chinês. Recomendado: instalar a extensão zhparser/pg_jieba (tokenizador nativo) ou pré-segmentar texto com um tokenizador na camada de aplicação e armazená-lo com config simple em um tsvector. A abordagem de camada de aplicação é a mais simples — sem necessidade de instalar extensão.


📖 Resumo


📝 Exercícios

  1. ⭐ Crie uma tabela blog_posts com title (TEXT) e body (TEXT), insira 3 linhas e use to_tsvector + plainto_tsquery para buscar artigos contendo "banco de dados".

  2. ⭐⭐ Adicione uma coluna search_vector (tsvector) a blog_posts, crie um índice GIN e escreva um trigger que atualiza automaticamente search_vector em INSERT/UPDATE (título peso A, corpo peso B). Depois busque "performance optimization" com ts_rank + ts_headline e ordene por ranqueamento.

  3. ⭐⭐⭐ Projete uma tabela knowledge_articles suportando busca mista chinês-inglês: conteúdo em inglês com config english, conteúdo em chinês com config simple (pré-segmentação na camada de aplicação), com duas colunas tsvector e índices GIN correspondentes. Escreva uma função de busca que escolhe a coluna certa com base no idioma da consulta de entrada, destaca resultados com ts_headline, ordena por ts_rank e suporta paginação.

Web-Tutorial.com

Equipe Técnica Web-Tutorial

Uma plataforma de tutoriais mantida por diversos desenvolvedores. Cada tutorial é escrito e revisado por profissionais da área correspondente. Trabalhamos para manter nosso conteúdo preciso e confiável — se encontrar algum problema, avise-nos.

100%