Vai no Google e pesquisa "Java Script" — assim, com espaço, sem o C. Ele encontra. Não só encontra como identifica que você tava falando de JavaScript. Pesquisa "cafe" sem acento — ele entende que é "café". Pesquisa "como fazer deploy" — e te retorna resultados que não necessariamente têm esse texto exato escrito em lugar nenhum.
É assim que um sistema de buscas funciona. E é assim que ele deveria funcionar.
Agora olha o que a maioria dos devs faz quando precisa buscar alguma coisa no banco:
1SELECT * FROM products WHERE name LIKE '%termo%';
Isso não é um sistema de buscas. Isso é um filtro de string.
Funciona? Funciona — pra sistemas pequenos, que não precisam de muita precisão. Mas no momento que seu banco cresce, que o usuário erra uma letra, esquece um acento ou pesquisa algo que não é exatamente o que tá escrito no banco, tudo desmorona.
Nesse artigo eu vou te mostrar, passo a passo, tudo o que acontece por trás de um sistema de buscas de verdade. São 5 conceitos que, juntos, transformam uma busca amadora numa busca profissional. Vamos lá.
O problema do LIKE
Antes de entender a solução, vale entender direito o problema. O LIKE com %termo% parece inofensivo, mas ele tem dois problemas sérios que escalam rápido:
1. Performance: Full Table Scan
Quando você usa LIKE '%celular%', o banco de dados faz o que chamamos de Full Table Scan — ele literalmente varre todos os registros da tabela, um por um, comparando string por string pra ver se bate.
1-- Isso aqui varre TODA a tabela, registro por registro 2SELECT * FROM products WHERE name LIKE '%celular%';
Com 100 registros? De boa. Com 10 mil? Começa a ficar lento. Com 100 mil, um milhão de registros? Inviável. Cada busca do usuário vai ser uma operação pesada no banco, e isso não escala.
E não adianta colocar índice B-Tree na coluna — o LIKE com % no começo invalida qualquer índice tradicional. O banco não tem como otimizar isso. Ele precisa ler tudo.
2. Precisão: bytes, não linguagem
O LIKE não entende linguagem. Ele entende bytes. E isso significa que qualquer variação mínima entre o que o usuário digitou e o que tá no banco vai quebrar a busca:
- O usuário pesquisa "celular" com C minúsculo, mas no banco tá "Celular" com C maiúsculo → não encontra
- O usuário pesquisa "cafe" sem acento, mas no banco tá "Café" com acento → não encontra
- O usuário erra uma letra: "celuar" em vez de "celular" → não encontra
1-- Nenhuma dessas queries retorna "Café com Leite" 2SELECT * FROM products WHERE name LIKE '%cafe%'; -- sem acento 3SELECT * FROM products WHERE name LIKE '%caffe%'; -- typo 4SELECT * FROM products WHERE name LIKE '%CAFÉ%'; -- case diferente
O LIKE compara texto exato. Mas busca não é sobre texto exato — busca é sobre intenção. Quando o usuário pesquisa alguma coisa, ele quer encontrar algo que às vezes ele nem sabe como se escreve. O sistema precisa ser inteligente o bastante pra lidar com isso, porque o sistema é pra ele, não pra você.
Normalização: preparando o texto antes da busca
Tudo começa antes do usuário pesquisar. Antes da busca acontecer, o texto precisa ser trabalhado, tratado. O nome disso é normalização — o processo de pegar um texto e transformar ele numa versão padronizada.
É como se o texto guardado no banco e o que o usuário vai usar pra buscar fossem coisas diferentes. Os dois passam pelo mesmo processo de normalização, e aí sim a comparação faz sentido.
O pipeline de normalização
Pega esse texto como exemplo: "JavaScript é INCRÍVEL, programação 100%!"
Ele tem J maiúsculo, S maiúsculo, "INCRÍVEL" todo em caps, acentuação, cedilha, til, número, porcentagem. Tudo isso precisa ser tratado.
Passo 1 — Lowercase (tudo pra minúsculo)
"javascript é incrível, programação 100%!"
Passo 2 — Remoção de acentos e diacríticos
"javascript e incrivel, programacao 100%!"
Sai o acento do "é", do "í", o cedilha do "ç", o til do "ã".
Passo 3 — Remoção de caracteres especiais
"javascript e incrivel programacao 100"
Tudo que for símbolo especial — exclamação, porcentagem, vírgula, ponto — some nessa fase.
Depois disso, pode escrever "JavaScript" tudo maiúsculo, "jAvAsCrIpT" alternando case, "JAVASCRIPT" gritando — tudo vai ser normalizado pro mesmo resultado. A comparação passa a funcionar independente de como o usuário escreveu.
Stemming: reduzindo palavras à raiz
Alguns sistemas vão um passo além e aplicam stemming — o processo de pegar uma palavra e reduzir ela pra sua versão base, sua raiz.
programando → program
programador → program
programação → program
programar → program
Todas essas palavras diferentes viram "program". Então se o usuário pesquisa "programando" e o texto no banco tem "programar", os dois batem — porque os dois compartilham a mesma raiz.
Isso é extremamente poderoso. O usuário não precisa acertar a conjugação exata, o tempo verbal, o sufixo. O sistema entende que são variações da mesma ideia.
Stopwords: tirando o ruído
Também tem a remoção de stopwords — palavras que são interjeições, artigos, preposições e conectivos que não agregam valor semântico à busca:
Removidas: o, a, de, para, é, um, uma, em, com, que, do, da, no, na...
Se o usuário pesquisa "como fazer deploy de uma aplicação", depois da remoção de stopwords fica: "fazer deploy aplicação". As palavras "como", "de", "uma" não ajudam a busca — elas só geram volume. Remover elas melhora a performance sem perder o sentido.
Na prática, o texto original:
"JavaScript é uma linguagem de programação popular"
Depois de normalização completa (lowercase + acentos + especiais + stemming + stopwords) vira:
["javascript", "linguagem", "program", "popular"]
Limpo, padronizado e pronto pra ser indexado.
Índice Invertido: o coração do sistema de buscas
Se você for tirar uma coisa desse artigo, tira essa. Isso aqui é o conceito mais importante de qualquer sistema de busca.
O problema da busca linear
Imagina que você tem 3 documentos no banco:
| Doc | Título | Conteúdo |
|---|---|---|
| 1 | Intro a JS | "JavaScript é uma linguagem de programação popular" |
| 2 | Intro a Python | "Python é uma linguagem de programação moderna" |
| 3 | Comparativo | "JavaScript e Python são linguagens de programação modernas" |
Quando o usuário pesquisa "JavaScript", num sistema com LIKE ou busca linear, você teria que varrer documento por documento, abrir cada um, procurar a palavra "JavaScript" dentro do texto, e retornar os que batem.
Com 3 docs? De boa. Com 3 milhões? Absurdo.
A inversão da lógica
O índice invertido inverte essa lógica completamente. Em vez de ir no documento e procurar a palavra, você vai na palavra e descobre em quais documentos ela tá.
Funciona assim: quando um documento é inserido no sistema, ele passa por todo aquele processo de normalização que a gente viu, e cada palavra resultante é registrada num índice separado, apontando pros documentos que a contêm:
| Termo | Documentos |
|---|---|
| javascript | 1, 3 |
| python | 2, 3 |
| linguagem | 1, 2, 3 |
| program | 1, 2, 3 |
| popular | 1 |
| moderna | 2, 3 |
Agora, quando o usuário pesquisa "JavaScript", o sistema:
- Normaliza o termo de busca → "javascript"
- Vai direto no índice invertido → encontra que "javascript" está nos docs 1 e 3
- Retorna os docs 1 e 3
Sem ler nenhum documento. Sem varrer nada. Direto ao ponto.
E se o usuário pesquisa "JavaScript Python"? O sistema busca os dois termos no índice:
- "javascript" → docs 1, 3
- "python" → docs 2, 3
Intersecção: doc 3 tem os dois. Ele vem primeiro. Docs 1 e 2 têm só um dos termos — vêm depois, com score menor.
A analogia do glossário
É exatamente a mesma ideia dos glossários de livros. Você vai lá no final do livro, procura um termo, e ele te diz: "página 42, parágrafo 3". Você não precisa ler o livro inteiro pra achar onde aquela palavra foi mencionada.
O índice invertido é o glossário do seu banco de dados — só que digital, automático, e absurdamente rápido.
Quem usa isso?
É exatamente por isso que ferramentas como Elasticsearch, Algolia, MeiliSearch e Typesense implementam índice invertido por baixo dos panos. Quando você faz uma busca nessas ferramentas, o que tá acontecendo é isso: o termo é normalizado, o índice é consultado, e os documentos são retornados sem precisar ler nenhum deles na hora da query.
Até o Postgres Full Text Search usa uma variação dessa lógica internamente quando você cria um índice GIN em colunas tsvector.
Fuzzy Matching: tolerando erros de digitação
O índice invertido resolve velocidade e precisão — quando o usuário digita a palavra certa. Mas e quando ele erra? "Javasript" sem o C. "Pythn" sem o O. "Rect" em vez de "React".
O índice não vai encontrar "Javasript" porque essa palavra simplesmente não existe nele. E aí?
Distância de Levenshtein
Aí entra o Fuzzy Matching — busca aproximada. O conceito por trás é a distância de Levenshtein (ou distância de edição): um cálculo que mede quantas operações mínimas são necessárias pra transformar uma palavra em outra.
As operações são três: inserir, alterar e remover uma letra.
"Javasript" → "Javascript"
Inserir o 'c' entre 's' e 'r'
Distância: 1
"Pythn" → "Python"
Inserir o 'o' entre 'h' e 'n'
Distância: 1
"Rect" → "React"
Trocar 'c' por 'a' + inserir 'c' na posição correta
Distância: 2
Tolerância configurável
Na prática, o sistema define uma tolerância. Tipo: se a distância entre o que o usuário digitou e uma palavra do índice for até 2, a gente aceita como match.
O Elasticsearch, por exemplo, tem isso nativo. Quando você faz uma query, pode definir o nível de fuzziness:
1{ 2 "query": { 3 "match": { 4 "title": { 5 "query": "javasript", 6 "fuzziness": "AUTO" 7 } 8 } 9 } 10}
O "AUTO" define a tolerância automaticamente baseado no tamanho da palavra:
- Palavras com 1-2 caracteres: distância 0 (match exato)
- Palavras com 3-5 caracteres: distância 1
- Palavras com 6+ caracteres: distância 2
O Google faz isso numa escala muito maior — ele não só tolera o erro como ainda sugere: "Você quis dizer JavaScript?"
O custo computacional
Um detalhe importante: fuzzy matching é caro. Você tá essencialmente comparando a palavra do usuário com potenciais milhares (ou milhões) de palavras no índice, calculando a distância de cada uma.
Pra não explodir em performance, os sistemas usam otimizações:
- N-grams: quebrar palavras em pedaços de N caracteres e comparar os pedaços. "javascript" com bigrams vira ["ja", "av", "va", "as", "sc", "cr", "ri", "ip", "pt"]. Palavras similares compartilham muitos n-grams, então dá pra filtrar candidatos antes de calcular a distância completa.
- Autômatos de Levenshtein: uma estrutura que permite verificar todas as palavras dentro de uma distância X de forma muito mais eficiente do que calcular par a par.
Você não vai implementar isso do zero (e nem deveria). Mas saber que existe te ajuda a entender por que ferramentas como Elasticsearch conseguem fazer fuzzy search em milissegundos mesmo com índices gigantes.
Ranking com TF-IDF: quem aparece primeiro?
Beleza, o sistema encontrou resultados. Mas e quando a busca retorna 500, 1.000 registros? Qual vem primeiro? Qual é mais relevante?
Busca sem ranking é só uma lista aleatória. E o que separa um sistema de busca bom de um ruim é exatamente isso: a capacidade de ranquear os resultados por relevância.
O algoritmo TF-IDF
O algoritmo clássico pra ranking de busca se chama TF-IDF — Term Frequency, Inverse Document Frequency. São duas métricas combinadas:
TF (Term Frequency) — quantas vezes o termo aparece no documento.
TF("kubernetes", doc_A) = 15 → aparece 15 vezes
TF("kubernetes", doc_B) = 1 → aparece 1 vez
Quanto mais vezes a palavra aparece no documento, mais relevante esse documento é pra aquele termo.
IDF (Inverse Document Frequency) — o quão rara aquela palavra é no conjunto total de documentos.
IDF = log(total de documentos / documentos que contêm o termo)
Se a palavra aparece em poucos documentos, ela é rara — o IDF é alto, ela é mais relevante. Se aparece em quase todos os documentos, ela é comum — o IDF é baixo, ela é irrelevante pra diferenciar resultados.
O score final é TF × IDF.
Exemplo concreto
Imagina que o usuário busca "Kubernetes deploy" e você tem 1.000 artigos no banco.
Artigo A: "Kubernetes" aparece 15 vezes, "deploy" aparece 8 vezes.
- TF alto pros dois termos
- IDF de "Kubernetes" é alto (aparece em poucos artigos)
- Score final: alto → esse artigo é super relevante
Artigo B: "Kubernetes" aparece 1 vez, lá no rodapé, num link lateral.
- TF baixo
- Score final: baixo → esse artigo mal fala do assunto
E a palavra "como"? Ela aparece em 95% dos artigos do banco. O IDF dela é praticamente zero. Se o usuário pesquisa "Kubernetes deploy como fazer", o sistema basicamente ignora o "como" — ele não ajuda em nada a diferenciar resultados.
Combinando os scores de "Kubernetes" e "deploy", o sistema consegue varrer todos os artigos e trazer exatamente o mais relevante no topo. Sem heurísticas manuais, sem gambiarras — pura matemática.
Além do TF-IDF
O TF-IDF é o algoritmo básico, mas sistemas modernos usam variações mais sofisticadas:
- BM25 (usado pelo Elasticsearch por padrão): uma evolução do TF-IDF que normaliza pelo tamanho do documento (evita que documentos longos tenham vantagem injusta)
- Boosting por campo: dar mais peso pro título do que pro corpo do texto — se "Kubernetes" tá no título, é mais relevante do que se tiver só no rodapé
- Recency: documentos mais recentes podem ter boost no score
- Popularidade: documentos mais acessados podem receber um boost
Mas todos partem da mesma ideia base: TF-IDF.
Autocomplete: a Trie (árvore de prefixo)
Sabe quando você vai pesquisar no Google, digita "pro", e antes de apertar Enter ele já sugere "programação", "professor auxiliar", "prouni", "procon"?
Isso não faz parte do sistema de buscas em si — é o autocomplete, praticamente um sistema à parte. Mas é igualmente necessário pra uma experiência de busca completa.
Como funciona
Enquanto o sistema de buscas usa o índice invertido, o autocomplete usa uma estrutura de dados chamada Trie (pronuncia-se "trai"), também conhecida como árvore de prefixo.
A ideia é simples: cada letra é um nó da árvore. Quando o usuário digita "pro", o sistema percorre os nós P → R → O e, a partir daí, explora todos os caminhos possíveis:
P
|
R
|
O
/ | \
G D J
| | |
R U E
| | |
A T T
| | |
M O O
|
A
|
Ç
|
Ã
|
O
→ programação
→ produto
→ projeto
A Trie permite encontrar todas as palavras que começam com um prefixo de forma extremamente rápida — a complexidade é O(m), onde m é o tamanho do prefixo, independente de quantas palavras existem no dicionário.
Relevância no autocomplete
Mas não adianta retornar 500 palavras que começam com "pro". O sistema precisa saber quais são as mais relevantes — e relevância aqui pode significar:
- Palavras mais buscadas (popularidade global)
- Palavras mais buscadas por aquele usuário (personalização)
- Palavras mais recentes (trending)
- Palavras que fazem sentido no contexto da aplicação
Por isso cada nó da Trie geralmente armazena não só a letra, mas também um score de popularidade. Assim, ao percorrer a árvore, o sistema já retorna as sugestões ordenadas por relevância.
Não é LIKE
O ponto é: o autocomplete não é um LIKE no banco de dados. Não é um SELECT * FROM words WHERE word LIKE 'pro%'. É uma estrutura de dados inteira, pensada e otimizada especificamente pra esse caso de uso, e que faz diferença real na experiência do usuário.
Na prática: qual ferramenta usar?
Agora que você entende os conceitos, a pergunta prática: qual banco, qual ferramenta usar pro seu cenário?
Postgres Full Text Search — pra maioria dos casos
Se você tem uma aplicação pequena ou média, o Postgres com Full Text Search já é suficiente. Sério. Ele já faz:
- Normalização e tokenização
- Stemming (com dicionários por idioma)
- Remoção de stopwords
- Ranking com
ts_rank - Índice GIN pra performance
1-- Criando uma coluna de busca otimizada 2ALTER TABLE articles ADD COLUMN search_vector tsvector; 3 4UPDATE articles SET search_vector = 5 to_tsvector('portuguese', title || ' ' || content); 6 7CREATE INDEX idx_search ON articles USING GIN(search_vector); 8 9-- Buscando com ranking 10SELECT title, ts_rank(search_vector, query) AS rank 11FROM articles, to_tsquery('portuguese', 'javascript & deploy') query 12WHERE search_vector @@ query 13ORDER BY rank DESC;
Pra muitos cenários, isso é mais do que suficiente. Você não precisa de nenhuma ferramenta externa, nenhum serviço adicional, nenhum custo extra. Tá tudo ali no banco que você já usa.
Elasticsearch — quando busca é core do produto
Se a busca é uma ferramenta central no seu produto — tipo um e-commerce, um marketplace, uma plataforma de conteúdo — aí recomendo ir pro Elasticsearch.
Ele tem tudo nativo: fuzzy matching, autocomplete, highlighting, agregações, filtros complexos. É poderoso, flexível, extremamente popular, e tem uma comunidade enorme.
1{ 2 "query": { 3 "bool": { 4 "must": [ 5 { 6 "multi_match": { 7 "query": "javascript tutorial", 8 "fields": ["title^3", "content"], 9 "fuzziness": "AUTO" 10 } 11 } 12 ] 13 } 14 }, 15 "highlight": { 16 "fields": { 17 "content": {} 18 } 19 } 20}
O title^3 ali dá 3x mais peso pro título do que pro conteúdo. O fuzziness: "AUTO" tolera erros de digitação. O highlight retorna os trechos com a palavra encontrada marcada. Tudo num request.
Mas: ele é mais pesado, precisa de atenção, manutenção, monitoramento. Você precisa cuidar dele com um pouco mais de carinho.
Alternativas mais leves
Se você quer algo mais simples que o Elasticsearch mas mais poderoso que o Postgres:
- MeiliSearch — open source, setup ridiculamente simples, ótimo pra aplicações onde você precisa de busca rápida sem muita configuração. Fuzzy search nativo, typo tolerance, e uma API bem intuitiva.
- Typesense — também open source, focado em facilidade de uso e performance. Muito bom pra search-as-you-type.
- Algolia — SaaS (pago), mas extremamente rápido e com SDKs pra tudo. Se não quiser gerenciar infraestrutura, é uma opção sólida.
A complexidade deles é bem mais baixa do que a do Elasticsearch. O trade-off é que você perde flexibilidade e poder de customização.
O fluxo completo: tudo junto
Pra fechar, vamos ver como todas essas peças se conectam num fluxo real.
O usuário vai lá no seu sistema e digita: "javasript tutorial"
1. Normalização A busca é quebrada em tokens normalizados:
"javasript tutorial" → ["javasript", "tutorial"]
2. Fuzzy Matching O sistema compara cada token com o índice:
- "tutorial" → match 100%, distância 0 ✓
- "javasript" → match com "javascript", distância 1 (falta o 'c') ✓
3. Índice Invertido Com os termos corrigidos, o sistema consulta o índice:
- "javascript" → docs [1, 3, 7, 12, 45, ...]
- "tutorial" → docs [3, 8, 12, 23, 45, ...]
- Intersecção: docs [3, 12, 45, ...] têm os dois termos
4. Scoring / Ranking Pra cada documento encontrado, calcula o TF-IDF (ou BM25):
- Doc 3: "javascript" aparece 12x, "tutorial" aparece 8x → score alto
- Doc 45: "javascript" aparece 2x, "tutorial" aparece 1x → score médio
- Doc 12: "javascript" aparece 1x no rodapé → score baixo
5. Retorno Os resultados são ordenados por score e retornados pro usuário.
Tudo isso rodando em milissegundos. E essa divisão em etapas — normalização, fuzzy, índice, ranking — junto com a velocidade, é o que separa um sistema de buscas amador de um profissional.
Conclusao
A gente tá tão acostumado com sistemas de busca bons — Google, YouTube, Amazon — que acha que é simples. Que é só um LIKE no banco. Mas quando você precisa implementar algo assim, e percebe que o LIKE não aguenta, aí bate aquela realidade.
Agora você sabe o que tá por trás:
- Normalização pra padronizar texto
- Índice invertido pra buscar sem ler documentos
- Fuzzy matching pra tolerar erros
- TF-IDF pra ranquear por relevância
- Trie pra autocomplete inteligente
Você não precisa implementar nada disso do zero. Elasticsearch, MeiliSearch, Typesense, e até o próprio Postgres já fazem o trabalho pesado. Mas entender o que tá acontecendo por trás te dá poder de decisão — saber quando usar o quê, entender os trade-offs, e construir sistemas que escalam de verdade.
Esse tipo de conhecimento de arquitetura vale dinheiro. Não à toa o Google, a Algolia e várias outras empresas construíram negócios bilionários em cima disso.