Full-Text Search PostgreSQL no Node.js permite criar busca textual com stemming, ranking, dicionários, operadores booleanos e índices, sem depender imediatamente de um serviço externo. Para catálogos, bases de conhecimento, documentação e painéis administrativos, a busca nativa pode oferecer boa relevância com operação simples.
O recurso transforma documentos em tsvector e consultas em tsquery. O PostgreSQL normaliza palavras conforme uma configuração linguística, remove termos pouco úteis e calcula correspondências. A aplicação Node.js envia parâmetros, controla filtros e apresenta resultados destacados.
Conceitos principais
to_tsvector: converte texto em lexemas pesquisáveis;plainto_tsquery: transforma texto comum em consulta;websearch_to_tsquery: aceita sintaxe parecida com busca web;@@: verifica correspondência;ts_rankets_rank_cd: calculam relevância;ts_headline: produz trechos destacados.
Primeira consulta
const result = await pool.query(
`SELECT id, titulo
FROM artigos
WHERE to_tsvector('portuguese',
coalesce(titulo, '') || ' ' || coalesce(conteudo, '')
) @@ plainto_tsquery('portuguese', $1)`,
[termo]
);A configuração portuguese aplica regras linguísticas adequadas ao português. Confirme se o ambiente possui a configuração e teste palavras reais do domínio.
Coluna tsvector armazenada
Calcular o vetor em todas as linhas a cada busca custa CPU. Para tabelas grandes, mantenha uma coluna gerada.
ALTER TABLE artigos
ADD COLUMN search_vector tsvector
GENERATED ALWAYS AS (
setweight(to_tsvector('portuguese', coalesce(titulo, '')), 'A') ||
setweight(to_tsvector('portuguese', coalesce(resumo, '')), 'B') ||
setweight(to_tsvector('portuguese', coalesce(conteudo, '')), 'C')
) STORED;Os pesos dão mais importância ao título e ao resumo.
Índice GIN
CREATE INDEX artigos_search_vector_gin
ON artigos USING gin (search_vector);O índice GIN acelera o operador @@. Meça tamanho, custo de escrita e plano de execução. Consulte Índices PostgreSQL no Node.js.
Busca com ranking
const result = await pool.query(
`WITH query AS (
SELECT websearch_to_tsquery('portuguese', $1) AS q
)
SELECT a.id,
a.titulo,
ts_rank_cd(a.search_vector, query.q) AS rank
FROM artigos a, query
WHERE a.search_vector @@ query.q
ORDER BY rank DESC, a.id DESC
LIMIT $2`,
[termo, 20]
);Use uma ordenação secundária estável. Ranking sozinho pode empatar e produzir páginas inconsistentes.
Sintaxe web
websearch_to_tsquery permite frases entre aspas, exclusão com hífen e operadores semelhantes aos mecanismos de busca. Ele é apropriado para entrada de usuário porque não lança os mesmos erros de sintaxe de uma to_tsquery construída manualmente.
const query = 'node.js "worker threads" -browser';Destaques seguros
SELECT ts_headline(
'portuguese',
conteudo,
websearch_to_tsquery('portuguese', $1),
'StartSel=<mark>, StopSel=</mark>, MaxFragments=2'
) AS trecho
FROM artigos
WHERE id = $2;ts_headline produz texto com marcações, mas a aplicação ainda deve tratar o resultado conforme o contexto de saída. Não confie em HTML armazenado pelo usuário. Escape o conteúdo ou use uma estratégia de sanitização robusta.
Filtros relacionais
A busca textual funciona melhor quando combinada com colunas normais.
SELECT id, titulo
FROM artigos
WHERE tenant_id = $1
AND status = 'published'
AND categoria_id = $2
AND search_vector @@ websearch_to_tsquery('portuguese', $3)
ORDER BY published_at DESC
LIMIT 30;Inclua tenant e autorização em todas as consultas relevantes. A busca não deve expor documentos que o usuário não poderia consultar diretamente.
Prefixos
Autocompletar por prefixo pode usar :*, mas não concatene entrada livre na consulta.
function normalizarPrefixo(value) {
return value
.trim()
.toLowerCase()
.replace(/[^\p{L}\p{N}_-]+/gu, ' ')
.split(/\s+/)
.filter(Boolean)
.slice(0, 5);
}
const termos = normalizarPrefixo(input);
const tsquery = termos.map((_, i) => `$${i + 1}:*`).join(' & ');Para implementações complexas, prefira funções SQL próprias e parâmetros seguros. Também limite frequência e tamanho da entrada.
Sinônimos e vocabulário do domínio
Termos como “JS”, “JavaScript” e “ECMAScript” podem representar a mesma intenção. Há duas estratégias comuns:
- expandir sinônimos na aplicação antes da consulta;
- configurar dicionários do PostgreSQL.
Dicionários oferecem integração profunda, mas exigem operação cuidadosa. Expansão na aplicação é mais simples para conjuntos pequenos.
Busca sem acentos
A extensão unaccent pode ajudar usuários que digitam sem acentuação.
CREATE EXTENSION IF NOT EXISTS unaccent;Combinar unaccent com vetores gerados exige funções imutáveis ou uma coluna atualizada por trigger. Teste o custo e não altere produção sem plano de migration.
Busca parcial e trigramas
Full-text search procura palavras e lexemas, não trechos arbitrários. Para nomes, códigos e erros de digitação, pg_trgm pode complementar a solução.
CREATE EXTENSION IF NOT EXISTS pg_trgm;
CREATE INDEX artigos_titulo_trgm
ON artigos USING gin (titulo gin_trgm_ops);Use trigramas de forma seletiva; índices podem crescer bastante.
Paginação
Resultados ordenados por relevância mudam quando o conteúdo muda. Para páginas estáveis, inclua rank arredondado e ID no cursor ou limite a navegação a uma janela curta.
ORDER BY rank DESC, id DESCConsulte Cursor Pagination no Node.js.
Preparando a API
app.get('/search', async (req, res) => {
const q = String(req.query.q ?? '').trim();
if (q.length < 2 || q.length > 200) {
return res.status(400).json({ error: 'Consulta inválida' });
}
const rows = await buscarArtigos({
tenantId: req.auth.tenantId,
query: q,
limit: 20
});
res.json({ items: rows });
});Aplique timeout, rate limiting e limite de resultados. Consultas caras podem ser usadas para esgotar CPU e I/O.
Observabilidade
Registre duração, quantidade de resultados, filtros e uma versão anonimizada ou hash da consulta quando privacidade exigir. Monitore buscas sem resultado, p95, hit rate de cache e planos lentos.
Use EXPLAIN (ANALYZE, BUFFERS) com consultas representativas e compare antes e depois de índices. Veja Query Optimization no Node.js.
Cache
Buscas populares podem ser armazenadas por poucos segundos. A chave deve incluir tenant, filtros, idioma, página e versão do índice. Não use cache compartilhado sem namespace de autorização.
Atualizações e consistência
Coluna gerada é atualizada na mesma transação da linha. Se a busca for alimentada por uma projeção separada, aceite consistência eventual e monitore atraso.
Testes
- acentos e variações de palavras;
- frases entre aspas;
- termos excluídos;
- documentos sem título ou resumo;
- filtros de tenant;
- ranking esperado;
- HTML e caracteres especiais;
- consultas longas;
- paginação com empates;
- plano usando GIN.
Erros comuns
- calcular
to_tsvectorem toda busca; - não especificar configuração linguística;
- construir
tsquerypor concatenação insegura; - ignorar autorização nos filtros;
- usar ranking sem ordenação secundária;
- mostrar headline sem sanitização;
- esperar correção de erros ortográficos automaticamente;
- criar índices sem medir.
Checklist
- configuração de idioma testada;
- vetor armazenado;
- índice GIN medido;
- consulta parametrizada;
- filtros de acesso aplicados;
- ranking e desempate definidos;
- entrada limitada;
- timeout configurado;
- destaques seguros;
- métricas de qualidade e desempenho.
Conclusão
Full-Text Search PostgreSQL no Node.js oferece uma base sólida para busca textual integrada ao banco. Ela combina relevância, filtros relacionais e transações sem adicionar outro serviço à arquitetura.
Armazene o vetor, use GIN, trate idioma e segurança, e acompanhe consultas sem resultado. Quando requisitos como busca fuzzy avançada, distribuição global ou bilhões de documentos surgirem, um mecanismo dedicado pode ser necessário. Até lá, a busca nativa costuma ser simples, econômica e eficiente.
Consulte a documentação oficial de Full-Text Search do PostgreSQL e a documentação de queries do node-postgres.

