PostgreSQL full-text search cobre muitos casos. Use uma regra simples, uma query inicial e uma checklist de indexação para saber quando adicionar um motor de busca.

A maioria das pessoas não pede “full-text search”. Elas querem uma caixa de busca que pareça rápida e encontre o que o usuário quis na primeira página. Se os resultados são lentos, bagunçados ou estranhamente ordenados, os usuários não ligam se você usou PostgreSQL full-text search ou um motor separado. Eles simplesmente deixam de confiar na busca.
A decisão é uma só: manter a busca dentro do Postgres ou adicionar um motor dedicado. O objetivo não é perfeição na relevância. É uma base sólida que seja rápida de entregar, fácil de operar e boa o suficiente para como seu app é realmente usado.
Para muitos apps, PostgreSQL full-text search é suficiente por bastante tempo. Se você tem poucos campos de texto (título, descrição, notas), ranqueamento básico e um ou dois filtros (status, categoria, tenant), o Postgres pode dar conta sem infraestrutura extra. Você tem menos partes móveis, backups mais simples e menos incidentes do tipo “por que a busca está fora e o app está no ar?”.
“Suficiente” normalmente significa que você consegue atingir três metas ao mesmo tempo:
Um exemplo concreto: um dashboard SaaS onde usuários buscam projetos por nome e notas. Se uma busca como “onboarding checklist” retorna o projeto certo entre os 5 primeiros, em menos de um segundo, e você não está constantemente afinando analisadores ou reindexando, isso é “suficiente”. Quando você não alcança essas metas sem aumentar a complexidade, aí sim “busca embutida vs motor de busca” vira uma dúvida real.
Times costumam descrever busca em termos de recursos, não de resultados. O movimento útil é traduzir cada recurso para quanto custa construir, ajustar e manter confiável.
Pedidos iniciais normalmente soam como: tolerância a erros tipográficos, facetas e filtros, highlights, ranqueamento “inteligente” e autocomplete. Para uma primeira versão, separe o que é essencial do que é desejável. Uma caixa de busca básica normalmente só precisa encontrar itens relevantes, lidar com formas comuns das palavras (plural, tempo verbal), respeitar filtros simples e continuar rápida à medida que a tabela cresce. É exatamente aí que PostgreSQL full-text search costuma se encaixar.
O Postgres brilha quando seu conteúdo vive em campos de texto normais e você quer a busca perto dos seus dados: artigos de ajuda, posts de blog, tickets de suporte, docs internas, títulos e descrições de produto, ou notas em registros de cliente. Esses são, em sua maioria, problemas de “encontre o registro certo”, não de “construa um produto de busca”.
Os itens desejáveis é que trazem complexidade. Tolerância a erros tipográficos e autocomplete avançado normalmente empurram você para ferramentas extras. Facetas são possíveis no Postgres, mas se você quer muitas facetas, análises profundas e contagens instantâneas em grandes volumes, um motor dedicado começa a ficar mais atraente.
O custo oculto raramente é a licença. É o segundo sistema. Quando você adiciona um motor de busca, também adiciona sincronização de dados e backfills (e bugs que isso cria), monitoramento e upgrades, trabalho de suporte “por que a busca mostra dados antigos?”, e dois conjuntos de botões de ajuste de relevância.
Se estiver em dúvida, comece com Postgres, entregue algo simples e só adicione outro engine quando um requisito claro não puder ser atendido.
Use uma regra de três cheques. Se passar nos três, fique com PostgreSQL full-text search. Se falhar num deles de forma grave, considere um motor dedicado.
Necessidades de relevância: resultados “bom o suficiente” são aceitáveis, ou você precisa de ranqueamento quase perfeito em muitos casos de borda (typos, sinônimos, “as pessoas também buscaram”, resultados personalizados)? Se você tolera ordenação imperfeita ocasional, o Postgres costuma funcionar.
Volume de consultas e latência: quantas buscas por segundo você espera no pico e qual seu orçamento real de latência? Se busca é uma fatia pequena do tráfego e você consegue manter queries rápidas com índices corretos, o Postgres dá conta. Se a busca virar a carga principal e começar a competir com leituras e escritas essenciais, isso é um sinal de alerta.
Complexidade: você está buscando em um ou dois campos de texto, ou combinando muitos sinais (tags, filtros, decaimento por tempo, popularidade, permissões) e múltiplos idiomas? Quanto mais complexa a lógica, mais atrito você sentirá dentro do SQL.
Um ponto de partida seguro é simples: entregue uma base no Postgres, registre queries lentas e buscas sem resultado, e só então decida. Muitos apps nunca saem disso, e você evita rodar e sincronizar um segundo sistema cedo demais.
Sinais de alerta que normalmente apontam para um motor dedicado:
Sinais verdes para ficar no Postgres:
PostgreSQL full-text search é uma forma embutida de transformar texto em algo que o banco pode buscar rápido, sem escanear cada linha. Funciona melhor quando seu conteúdo já está no Postgres e você quer busca rápida e decente com operações previsíveis.
Há três peças que valem conhecer:
ts_rank (ou ts_rank_cd) para colocar linhas mais relevantes primeiro.A configuração de idioma importa porque altera como o Postgres trata as palavras. Com a configuração certa, “running” e “run” podem casar (stemming) e palavras comuns podem ser ignoradas (stop words). Com a configuração errada, a busca pode parecer quebrada porque a redação normal do usuário não bate com o que foi indexado.
Prefix matching é o recurso que as pessoas buscam para comportamento tipo “typeahead”, como casar “dev” com “developer”. No Postgres FTS, isso é feito tipicamente com um operador de prefixo (por exemplo, term:*). Pode melhorar a qualidade percebida, mas costuma aumentar o trabalho por query, então trate como upgrade opcional, não como padrão.
O que o Postgres não quer ser: uma plataforma de busca completa com todo recurso. Se você precisa de correção ortográfica fuzzy, autocomplete avançado, learning-to-rank, analisadores complexos por campo ou indexação distribuída em muitos nós, você está fora da zona de conforto embutida. Para muitos apps, porém, PostgreSQL full-text search oferece a maior parte do que os usuários esperam com muito menos partes móveis.
Aqui está uma forma pequena e realista para conteúdo que você quer buscar:
-- Minimal example table
CREATE TABLE articles (
id bigserial PRIMARY KEY,
title text NOT NULL,
body text NOT NULL,
updated_at timestamptz NOT NULL DEFAULT now()
);
Uma boa base para PostgreSQL full-text search é: construir uma query a partir do que o usuário digitou, filtrar linhas primeiro (quando possível), depois ranquear as correspondências restantes.
-- $1 = user search text, $2 = limit, $3 = offset
WITH q AS (
SELECT websearch_to_tsquery('english', $1) AS query
)
SELECT
a.id,
a.title,
a.updated_at,
ts_rank_cd(
setweight(to_tsvector('english', coalesce(a.title, '')), 'A') ||
setweight(to_tsvector('english', coalesce(a.body, '')), 'B'),
q.query
) AS rank
FROM articles a
CROSS JOIN q
WHERE
a.updated_at >= now() - interval '2 years' -- example safe filter
AND (
setweight(to_tsvector('english', coalesce(a.title, '')), 'A') ||
setweight(to_tsvector('english', coalesce(a.body, '')), 'B')
) @@ q.query
ORDER BY rank DESC, a.updated_at DESC, a.id DESC
LIMIT $2 OFFSET $3;
Alguns detalhes que economizam tempo depois:
WHERE antes do ranqueamento (status, tenant_id, intervalos de data). Você ranqueia menos linhas, então fica rápido.\ORDER BY (como updated_at, depois id). Isso mantém a paginação estável quando muitos resultados têm o mesmo rank.\websearch_to_tsquery para entrada do usuário. Ela trata aspas e operadores simples do jeito que as pessoas esperam.Quando essa base funcionar, mova a expressão to_tsvector(...) para uma coluna armazenada. Isso evita recalculá-la a cada query e torna o indexamento direto.
A maioria das histórias de “PostgreSQL full-text search é lento” se resume a uma coisa: o banco está construindo o documento de busca em cada query. Corrija isso primeiro armazenando um tsvector pré-construído e indexando-o.
tsvector: coluna gerada ou trigger?Uma coluna gerada é a opção mais simples quando seu documento de busca é construído a partir de colunas da mesma linha. Ela se mantém correta automaticamente e é difícil de esquecer nas atualizações.
Use um tsvector mantido por trigger quando o documento depende de tabelas relacionadas (por exemplo, combinando uma linha de produto com o nome da categoria), ou quando você quer lógica customizada que não é fácil de expressar como uma única expressão gerada. Triggers adicionam partes móveis, então mantenha-os pequenos e teste bem.
Crie um índice GIN na coluna tsvector. Esse é o baseline que faz o PostgreSQL full-text search parecer instantâneo para buscas típicas de app.
Uma configuração que funciona para muitos apps:
tsvector na mesma tabela das linhas que você consulta com mais frequência.\tsvector.\@@ contra o tsvector armazenado, não to_tsvector(...) calculado na hora.\VACUUM (ANALYZE) após grandes backfills para que o planner entenda o novo índice.Manter o vetor na mesma tabela geralmente é mais rápido e simples. Uma tabela de busca separada pode fazer sentido se a tabela base tiver muitas escritas, ou se você estiver indexando um documento combinado que abrange várias tabelas e quiser atualizá-lo no seu próprio ritmo.
Índices parciais podem ajudar quando você só busca um subconjunto de linhas, como status = 'active', um único tenant em um app multi-tenant, ou um idioma específico. Eles reduzem o tamanho do índice e podem acelerar buscas, mas só valem se suas queries sempre incluírem o mesmo filtro.
Você consegue resultados surpreendentemente bons com PostgreSQL full-text search se mantiver regras de relevância simples e previsíveis.
A vitória mais fácil é pesar campos: corresponder no título deve contar mais que corresponder no corpo. Construa um tsvector combinado onde o título tem peso maior que a descrição, depois ranqueie com ts_rank ou ts_rank_cd.
Se você precisa que itens “frescos” ou “populares” subam, faça com cuidado. Um pequeno impulso é ok, mas não deixe que ele substitua a relevância do texto. Um padrão prático é: ranquear por texto primeiro, depois desempatar por recência, ou adicionar um bônus limitado para que um item novo irrelevante não vença uma correspondência perfeita antiga.
Sinônimos e correspondência de frase são onde as expectativas costumam divergir. Sinônimos não são automáticos; só os tem se você adicionar um thesaurus ou dicionário customizado, ou expandir os termos da query manualmente (por exemplo, tratar “auth” como “authentication”). Correspondência de frase também não é padrão: queries simples casam palavras em qualquer lugar, não “essa frase exata”. Se usuários digitam frases entre aspas ou perguntas longas, considere phraseto_tsquery ou websearch_to_tsquery para casar melhor com como as pessoas pesquisam.
Conteúdo em múltiplas línguas precisa de uma decisão. Se você sabe o idioma por documento, armazene-o e gere o tsvector com a configuração certa (English, Russian, etc.). Se não souber, um fallback seguro é indexar com a configuração simple (sem stemming), ou manter dois vetores: um específico por idioma quando conhecido e um simple para tudo.
Para validar relevância, mantenha pequeno e concreto:
Isso normalmente é suficiente para PostgreSQL full-text search em caixas de busca de apps como “templates”, “docs” ou “projetos”.
A maioria das histórias de “PostgreSQL full-text search é lento ou irrelevante” vem de alguns erros evitáveis. Corrigi-los geralmente é mais simples do que adicionar um novo sistema de busca.
Uma armadilha comum é tratar tsvector como um valor calculado que se mantém correto sozinho. Se você armazena tsvector em uma coluna mas não a atualiza em cada insert e update, os resultados vão parecer aleatórios porque o índice não bate mais com o texto. Se você calcula to_tsvector(...) na hora dentro da query, os resultados podem estar corretos mas mais lentos, e você perde o benefício do índice dedicado.
Outra forma fácil de prejudicar performance é ranquear antes de reduzir o conjunto de candidatos. ts_rank é útil, mas normalmente deve rodar depois que o Postgres usou o índice para encontrar as linhas que batem. Se você calcular rank para uma grande parte da tabela (ou fizer joins antes), pode transformar uma busca rápida em um full table scan.
Pessoas também esperam que “contains” se comporte como LIKE '%term%'. Wildcards iniciais não se mapeiam bem ao full-text search porque FTS é baseado em palavras (lexemas), não substrings arbitrárias. Se você precisa de busca por substring para códigos de produto ou IDs parciais, use uma ferramenta diferente (por exemplo, indexação trigram) em vez de culpar o FTS.
Problemas de performance muitas vezes vem do tratamento dos resultados, não do matching. Dois padrões para observar:
OFFSET grande, que faz o Postgres pular cada vez mais linhas conforme você avança.\Questões operacionais importam também. Bloat de índice pode aparecer após muitas atualizações, e reindexar pode ser caro se você esperar até a situação ficar ruim. Meça tempos reais de query (e cheque EXPLAIN ANALYZE) antes e depois das mudanças. Sem números, é fácil “corrigir” o PostgreSQL full-text search piorando de outra forma.
Antes de culpar o PostgreSQL full-text search, rode essas verificações. A maioria dos bugs “Postgres search é lento ou irrelevante” vem de básicos faltando, não do recurso em si.
Construa um tsvector real: armazene-o em uma coluna gerada ou mantida, use a configuração de idioma certa (english, simple, etc.) e aplique pesos se misturar campos (título > subtítulo > corpo).
Normalize o que você indexa: mantenha campos ruidosos (IDs, boilerplate, texto de navegação) fora do tsvector, e corte blobs enormes se usuários nunca os pesquisam.
Crie o índice certo: adicione um índice GIN na coluna tsvector e confirme que ele é usado em EXPLAIN. Se apenas um subconjunto é pesquisável (por exemplo status = 'published'), um índice parcial pode reduzir tamanho e acelerar leituras.
Mantenha tabelas saudáveis: tuplas mortas podem desacelerar scans de índice. Vacuum regular importa, especialmente em conteúdo frequentemente atualizado.
Tenha um plano de reindex: migrações grandes ou índices inchados às vezes precisam de uma janela controlada para reindex.
Quando dados e índice estiverem corretos, foque na forma da query. PostgreSQL full-text search é rápido quando consegue reduzir o conjunto de candidatos cedo.
Filtre primeiro, depois ranqueie: aplique filtros estritos (tenant, idioma, published, category) antes do ranqueamento. Ranquear milhares de linhas que depois você descarta é trabalho desperdiçado.
Use ordenação estável: ordene por rank e depois um desempate como updated_at ou id para que resultados não saltem entre atualizações.
Evite “a query faz tudo”: se precisar de matching fuzzy ou tolerância a typos, faça isso intencionalmente (e meça). Não force scans sequenciais por acidente.
Teste queries reais: colete as top 20 buscas, verifique relevância manualmente e mantenha uma pequena lista de resultados esperados para pegar regressões.
Observe caminhos lentos: registre queries lentas, reveja EXPLAIN (ANALYZE, BUFFERS) e monitore tamanho do índice e hit rate de cache para perceber quando o crescimento muda o comportamento.
Uma central de ajuda SaaS é um bom ponto de partida porque o objetivo é simples: ajudar pessoas a encontrar o artigo que responde sua pergunta. Você tem alguns milhares de artigos, cada um com título, resumo curto e corpo. A maioria dos visitantes digita 2 a 5 palavras como “reset password” ou “billing invoice”.
Com PostgreSQL full-text search, isso pode ficar pronto surpreendentemente rápido. Você armazena um tsvector para os campos combinados, adiciona um índice GIN e ranqueia por relevância. Sucesso parece com: resultados em menos de 100 ms, os 3 primeiros geralmente corretos e sem necessidade de babysitting do sistema.
Então o produto cresce. Suporte quer filtrar por área do produto, plataforma (web, iOS, Android) e plano (free, pro, business). Redatores querem sinônimos, “did you mean” e melhor tratamento de typos. Marketing quer analytics como “top searches com zero resultados”. O tráfego sobe e a busca vira um dos endpoints mais usados.
Esses sinais indicam que um motor dedicado pode valer o custo:
Um caminho prático de migração é manter o Postgres como fonte da verdade, mesmo após adicionar um search engine. Comece registrando queries e casos sem resultado, então rode um job assíncrono que copia apenas os campos pesquisáveis para o novo índice. Rode ambos em paralelo por um tempo e faça a troca gradualmente, em vez de apostar tudo no dia da migração.
Se sua busca é basicamente “encontrar documentos que contenham essas palavras” e seu conjunto de dados não é massivo, PostgreSQL full-text search normalmente é suficiente. Comece por ele, faça funcionar e só adicione um engine dedicado quando você conseguir nomear o recurso ou a dor de escala que falta.
Um resumo útil para guardar:
tsvector, adicionar um índice GIN e suas necessidades de ranqueamento forem básicas.\Um passo prático: implemente a query e o índice iniciais das seções anteriores, depois registre algumas métricas simples por uma semana. Monitore p95 da query, queries lentas e um sinal rudimentar de sucesso como “search -> clique -> sem bounce imediato” (mesmo um contador básico de eventos ajuda). Você verá rápido se precisa de melhor ranqueamento ou apenas melhor UX (filtros, highlights, snippets melhores).
Comece a planejar um motor de busca dedicado quando um destes se tornar um requisito real (não um desejo): autocomplete forte ou busca instantânea a cada tecla em escala, forte tolerância a erros e correção ortográfica, facetas e agregações com contagens rápidas em muitos campos, ferramentas avançadas de relevância (conjuntos de sinônimos, learning-to-rank, boosts por query), ou carga sustentada e índices grandes difíceis de manter rápidos.
Se quiser avançar rápido no lado do app, Koder.ai (koder.ai) pode ser uma forma prática de prototipar a UI e a API de busca via chat, iterando com snapshots e rollback enquanto você mede o comportamento real dos usuários.
PostgreSQL full-text search é “suficiente” quando você consegue atingir três coisas ao mesmo tempo:
Se você conseguir isso com um tsvector armazenado + um índice GIN, normalmente está em uma ótima posição.
Prefira PostgreSQL full-text search por padrão. Ele entrega mais rápido, mantém seus dados e busca no mesmo lugar e evita construir e manter uma pipeline de indexação separada.
Migre para um engine dedicado quando tiver um requisito claro que o Postgres não atende bem (tolerância a erros tipográficos de alta qualidade, autocomplete avançado, faceting pesado ou carga de busca que disputa recursos com o banco de dados principal).
Uma regra simples: permaneça no Postgres se você passar estes três cheques:
Se falhar gravemente em um (especialmente recursos de relevância como typos/autocomplete, ou tráfego alto), considere um motor dedicado.
Use Postgres FTS quando sua busca for basicamente “encontrar o registro certo” em alguns campos (title/body/notes), com filtros simples (tenant, status, category).
É ideal para centrais de ajuda, docs internas, tickets, busca de artigos/blog e dashboards SaaS onde usuários procuram por nomes de projetos e notas.
Um bom shape básico de query geralmente:
websearch_to_tsquery.Armazene um tsvector pré-construído e adicione um índice GIN. Isso evita recomputar to_tsvector(...) a cada requisição.
Configuração prática:
Use uma coluna gerada quando o documento de busca for construído a partir de colunas da mesma linha (simples e difícil de quebrar).
Use uma coluna mantida por trigger quando o texto de busca depende de tabelas relacionadas ou lógica customizada.
Escolha padrão: coluna gerada primeiro; triggers somente quando realmente precisar compor entre tabelas.
Comece com relevância previsível:
Depois valide usando uma pequena lista de consultas reais de usuários e os resultados esperados no topo.
Postgres FTS é baseado em palavras, não em substrings. Por isso não se comporta como LIKE '%term%' para strings parciais.
Se você precisa de busca por substring (IDs, códigos, fragmentos), trate isso separadamente (por exemplo, com indexação trigram) em vez de forçar o FTS a fazer um trabalho a que ele não se destina.
Sinais comuns de que você superou o Postgres FTS:
Caminho prático: mantenha Postgres como fonte da verdade e adicione indexação assíncrona quando o requisito for claro.
@@ contra um tsvector armazenado.ts_rank/ts_rank_cd e um desempate estável como updated_at, id.Isso mantém os resultados relevantes, rápidos e estáveis para paginação.
tsvector na mesma tabela que você consulta.\tsvector_column @@ tsquery.Essa é a correção mais comum quando a busca parece lenta.