Voltar para a página inicial

Full Text Search no PostgreSQL: Como implementar buscas avançadas e performáticas

Entenda a diferença entre consultas dinâmicas e colunas geradas indexadas com GIN no PostgreSQL. Aprenda a usar Full Text Search com inteligência em português, ranking de relevância e buscas resilientes com websearch_to_tsquery. Chega de usar like nas suas buscas.

Por Allison Verdam07 de agosto de 20266 mins de leitura
postgresqldatabasesqlfull text searchftswebsearch_to_tsquerybackend

Full Text Search no PostgreSQL, como utilizar na prática

Hoje estarei falando um pouco sobre busca textual no PostgreSQL usando Full Text Search (FTS) e dando alguns exemplos práticos de como implementar buscas avançadas, performáticas e inteligentes sem precisar adicionar um serviço externo como o Elasticsearch na sua aplicação.

  • O Full Text Search (FTS) permite realizar buscas por palavras, radicais e frases em grandes volumes de texto diretamente no PostgreSQL.

  • Diferente do tradicional LIKE ou ILIKE, o FTS entende variações de palavras (stemming), ignora palavras irrelevantes (stop words) e utiliza índices invertidos (GIN) para alta performance.

  • Combinado com colunas geradas e índices GIN, você consegue criar um mecanismo de busca profissional mantendo sua arquitetura simples.

O problema do ILIKE '%busca%'

Quem nunca fez uma busca no banco usando WHERE titulo ILIKE '%busca%'? Para projetos pequenos ou tabelas com poucas linhas isso até funciona, mas rapidamente se torna um gargalo enorme por três motivos principais:

  • Performance péssima: O wildcard no início ('%palavra') impede o banco de usar índices B-Tree tradicionais, forçando um Sequential Scan (leitura completa de todas as linhas da tabela).

  • Falta de inteligência textual: Se o usuário buscar por "correndo", o banco não vai encontrar registros contendo "correr" ou "corrida", pois ele faz apenas correspondência exata de caracteres.

  • Ignora relevância: O ILIKE não sabe qual resultado é mais importante ou combina melhor com o termo buscado.

Para resolver exatamente esses pontos, o PostgreSQL nativamente possui o Full Text Search.

Os dois pilares do FTS: tsvector e tsquery

O funcionamento do Full Text Search no Postgres é baseado em dois tipos de dados fundamentais:

  • tsvector (Text Search Vector): Representa o documento formatado para busca. Ele reduz as palavras aos seus radicais (lexemas), remove acentos e palavras comuns (como "de", "com", "para").

  • tsquery (Text Search Query): Representa os termos que você está procurando, permitindo operadores lógicos como AND (&), OR (|) e NOT (!).

Veja como o Postgres transforma um texto simples em um tsvector usando a configuração do idioma português:

SELECT to_tsvector('portuguese', 'O Allison está escrevendo um artigo sobre PostgreSQL!');
-- Resultado: 'artigm':6 'allison':2 'escrev':4 'postgresql':8

Repare como "escrevendo" virou o radical escrev e palavras como "O", "está", "um" e "sobre" foram ignoradas. Agora vamos criar uma consulta com tsquery para verificar a correspondência usando o operador de busca @@:

SELECT to_tsvector('portuguese', 'O Allison está escrevendo um artigo sobre PostgreSQL!') 
   @@ to_tsquery('portuguese', 'escrita & postgresql');
-- Resultado: true

Mesmo que a palavra buscada tenha sido "escrita", a busca retornou true porque ambas compartilham o mesmo radical (escrev / escrit).

Por que NÃO usar to_tsvector direto na cláusula WHERE?

Muitos tutoriais na internet ensinam a fazer consultas no banco da seguinte forma:

-- Abordagem comum, porém ineficiente para produção
SELECT * FROM artigos 
WHERE to_tsvector('portuguese', conteudo) @@ to_tsquery('portuguese', 'postgres');

Embora isso funcione para testes simples, existe um problema grave de performance nessa abordagem. Ao colocar o to_tsvector() direto na consulta, o Postgres é forçado a reprocessar o texto de todas as linhas da sua tabela e convertê-los em vetores no exato momento da busca.

Em tabelas pequenas você não notará diferença, mas em tabelas com milhares de registros, isso causa um Sequential Scan extremamente lento, anulando os ganhos de velocidade do banco de dados.

A diferença entre to_tsquery e websearch_to_tsquery

Outro detalhe importante é a forma como capturamos o que o usuário digita no input da aplicação. O to_tsquery é muito rígido e exige operadores lógicos explicítos. Se o usuário digitar direto no campo de busca algo como PostgreSQL Node (com espaço no meio), e você passar essa string diretamente para o to_tsquery, o Postgres vai retornar um erro de sintaxe e quebrar a requisição, pois ele esperava receber PostgreSQL & Node.

Para resolver isso sem precisar tratar strings manualmente no código da sua aplicação, o Postgres disponibiliza a função websearch_to_tsquery:

-- Aceita buscas amigáveis no estilo Google
SELECT websearch_to_tsquery('portuguese', 'artigo "PostgreSQL" -oracle');
-- Resultado interpretado: 'artigm' & 'postgresql' & !'oracl'

Ela ententende aspas para frases exatas, hífens para exclusão e espaços como AND automaticamente, tornando a busca resiliente a entradas de texto do usuário final.

A solução ideal: Coluna gerada e índice GIN para alta performance

Para unir o melhor dos dois mundos (alta velocidade e usabilidade), a melhor prática para produção é armazenar o tsvector pré-calculado em uma coluna dedicada da tabela e criar um índice GIN (Generalized Inverted Index) sobre ela.

Dessa forma, o vetor é calculado apenas uma vez (ao inserir ou atualizar o registro) e a consulta vai direto no índice, respondendo em milissegundos.

Vamos ver a implementação prática:

CREATE TABLE artigos (
  id SERIAL PRIMARY KEY,
  titulo VARCHAR(255) NOT NULL,
  conteudo TEXT NOT NULL,
  
  -- Coluna gerada automaticamente que junta título e conteúdo
  busca_vector tsvector GENERATED ALWAYS AS (
    setweight(to_tsvector('portuguese', coalesce(titulo, '')), 'A') ||
    setweight(to_tsvector('portuguese', coalesce(conteudo, '')), 'B')
  ) STORED
);

-- Criação do índice GIN para busca instantânea
CREATE INDEX idx_artigos_busca ON artigos USING GIN (busca_vector);

Repare que usamos a função setweight(). Ela nos permite definir pesos para cada campo: atribuímos peso 'A' (maior importância) para o título e peso 'B' para o conteúdo.

Consultando e ordenando por Relevância (ts_rank)

Com nossa tabela indexada e usando websearch_to_tsquery, podemos fazer consultas performáticas e ordenar os resultados pelo grau de relevância (rank):

SELECT 
  id, 
  titulo, 
  ts_rank(busca_vector, websearch_to_tsquery('portuguese', 'postgres artigo')) AS relevancia
FROM artigos
WHERE busca_vector @@ websearch_to_tsquery('portuguese', 'postgres artigo')
ORDER BY relevancia DESC;

Com essa estrutura, se o termo buscado aparecer no título (peso A), ele terá uma pontuação de relevância maior do que se aparecer apenas no conteúdo (peso B), garantindo que os melhores resultados venham primeiro.

Conclusões finais

O Full Text Search do PostgreSQL é uma solução nativa, extremamente madura e robusta. Para a esmagadora maioria das aplicações, ele elimina completamente a necessidade de configurar, manter e sincronizar clusters de ferramentas de busca externas como Elasticsearch ou Meilisearch.

Evitar o uso de to_tsvector direto na cláusula WHERE, utilizar colunas geradas com índices GIN e adotar o websearch_to_tsquery para a interface do usuário são os passos essenciais para criar uma busca profissional, rápida e resiliente no Postgres.

Espero que você tenha aproveitado alguma coisa do conteúdo apresentado.

Aqui no blog tem posts falando sobre outros tópicos de backend e banco de dados, da uma olhada.

Acho que é isso, se tiver ficado com alguma dúvida ou tenha alguma sugestão escreve aqui nos comentários.

Comentários