Busca full-text: como implementar pesquisa de verdade
Por que um LIKE no SQL não escala nem entende relevância, como o full-text search nativo do Postgres resolve isso com ranking, e quando vale a pena trocar por uma ferramenta dedicada como Meilisearch.
Já falei aqui sobre índice de banco de dados pra acelerar busca por valor exato, mas aquele tipo de índice não resolve um problema diferente: buscar um termo dentro de um texto longo, tipo encontrar posts que mencionam "autenticação" em qualquer lugar do conteúdo, entendendo variação de palavra e ordenando pelo que é mais relevante primeiro.
Por que LIKE não é a solução
A primeira tentativa, bastante comum, é usar LIKE com curinga nos dois lados:
SELECT * FROM posts WHERE conteudo LIKE '%autenticação%';Isso funciona, tecnicamente, mas tem dois problemas sérios. O primeiro é performance: um LIKE com % no início impede o banco de usar qualquer índice tradicional, porque não existe um ponto de partida fixo pra buscar, é obrigado a varrer o texto inteiro de cada linha, de novo, a cada busca.
O segundo problema é mais profundo: LIKE não entende nada sobre o idioma. Uma busca por "autenticação" não encontra um post que só menciona "autenticar" ou "autenticado", porque pra ele isso são strings completamente diferentes, sem relação nenhuma entre si. E mesmo quando encontra múltiplos resultados, não existe noção de qual deles é mais relevante, a ordem que vem de volta é só a ordem física das linhas na tabela.
Full-text search nativo do Postgres
O Postgres tem suporte nativo a busca de texto que resolve os dois problemas. A ideia central é transformar o texto num formato especial, chamado tsvector, que já quebra o conteúdo em palavras normalizadas (removendo variação de plural, conjugação verbal, esse tipo de coisa):
SELECT to_tsvector('portuguese', 'Autenticação de usuários com JWT');
-- 'autentic':1 'jwt':5 'usuári':3Repara que "Autenticação" virou autentic, a raiz da palavra. Isso significa que uma busca por "autenticar" ou "autenticado" também bate nesse mesmo radical, porque os dois normalizam pra raiz parecida.
A busca em si usa to_tsquery pra transformar o termo pesquisado no mesmo formato, e o operador @@ confere se o texto contém aquilo:
SELECT titulo FROM posts
WHERE to_tsvector('portuguese', conteudo) @@ to_tsquery('portuguese', 'autenticação');Indexando pra não recalcular a cada busca
Calcular to_tsvector em cada linha, a cada busca, ainda é caro numa tabela grande. A solução é guardar esse valor já calculado numa coluna separada, com um índice GIN por cima:
ALTER TABLE posts ADD COLUMN busca tsvector
GENERATED ALWAYS AS (to_tsvector('portuguese', conteudo)) STORED;
CREATE INDEX idx_posts_busca ON posts USING GIN (busca);Com isso, a busca passa a usar o índice em vez de recalcular o tsvector de cada linha na hora, voltando a ter a mesma vantagem de velocidade que qualquer índice tradicional oferece.
Ranking: ordenando pelo que é mais relevante
Além de encontrar, o Postgres também sabe dizer o quão relevante cada resultado é, usando ts_rank:
SELECT titulo, ts_rank(busca, to_tsquery('portuguese', 'autenticação')) AS relevancia
FROM posts
WHERE busca @@ to_tsquery('portuguese', 'autenticação')
ORDER BY relevancia DESC;Um post que menciona o termo buscado várias vezes, ou logo no título, recebe uma pontuação de relevância maior do que um post que menciona o termo uma única vez, de passagem, no meio de um parágrafo grande. Isso aproxima bastante a experiência de uma busca de verdade, parecida com o que o próprio usuário espera de qualquer motor de busca.
Quando vale trocar por uma ferramenta dedicada
O full-text search do Postgres resolve muito bem a maioria dos casos de um site ou aplicação de porte médio, sem precisar de infraestrutura nova. Mas ele compete com o resto das consultas da aplicação pelo mesmo banco, e não tem alguns recursos que uma ferramenta dedicada de busca oferece de forma mais refinada: tolerância a erro de digitação, busca facetada (filtrar por categoria junto da busca textual), ou ranking que aprende com o comportamento de clique do usuário.
Ferramentas como Meilisearch ou Elasticsearch são desenhadas especificamente pra isso, rodando separadas do banco principal, geralmente sincronizando os dados que precisam ser buscáveis. A troca compensa quando a busca é uma funcionalidade central do produto, com volume alto e expectativa de experiência bem refinada, não só um campo de pesquisa secundário.
Fechando
A escolha natural pra buscar um valor exato é um índice tradicional, mas buscar dentro de texto livre, entendendo variação de palavra e relevância, pede uma estrutura diferente. O full-text search nativo do Postgres já resolve isso sem precisar de peça nova na infraestrutura, e só vale trocar por uma ferramenta dedicada quando a busca em si se torna um diferencial importante do produto, não só uma funcionalidade a mais.
Direto na sua
caixa de entrada.
Um aviso por e-mail sempre que eu publicar um post novo. Sem spam, sem newsletter chata, só isso.
Leia também
- Artboards no Claude Code: a skill /design e como evitar o AI Slop7 de outubro de 2026 · 5 min
- Teste unitário na prática com Vitest6 de outubro de 2026 · 4 min
- Logging estruturado: por que console.log não basta em produção5 de outubro de 2026 · 4 min
- Gauntlet Loop: como usar múltiplos agentes para melhorar o design no Claude Code4 de outubro de 2026 · 8 min