Busca textual
Índice invertido, relevância e os motores de busca como Elasticsearch e OpenSearch.
O problema: o usuário não digita o que está no banco
O produto se chama "Tênis de corrida Veloz". O usuário digita "tenis corrida", sem acento e sem o "de". Ou "tênis para correr". Ou "tenys corida", com pressa no celular. Ele espera achar o tênis, e espera que o tênis venha antes da "Camiseta de corrida".
A primeira tentativa costuma ser WHERE nome ILIKE '%tenis%'. Medimos num catálogo de 500 mil produtos no PostgreSQL 18:
| Busca | Produtos encontrados | Tempo |
|---|---|---|
nome ILIKE '%tenis%' | 0 | 116 ms |
nome ILIKE '%tênis%' | 50.000 | 113 ms |
nome ILIKE '%tênis%corrida%' | 5.000 | 161 ms |
Nome ou descrição ILIKE '%correr%' | 0 | 222 ms |
Sem o acento, nenhum tênis. "correr" não encontra "corrida". E cada busca varre a tabela inteira, porque um índice comum não serve para % no começo, como mostra o tópico de plano de execução. Mesmo quando acerta, o LIKE não diz qual resultado é mais relevante.
O índice invertido
A busca textual inverte a pergunta. Em vez de procurar a palavra em cada documento, ela monta, uma vez, uma lista de cada palavra com os documentos que a contêm:
"corr" → produtos 1, 2, 3, 4, ...
"ten" → produtos 100, 110, 120, ...
"mochil" → produtos 4, 14, 24, ...Buscar "tenis corrida" vira cruzar duas listas, o que leva milissegundos com milhões de documentos. É o índice invertido, a estrutura por trás do PostgreSQL com índice GIN, do Elasticsearch, do OpenSearch e de quase todo motor de busca.
A análise do texto
Antes de entrar no índice, o texto passa por uma análise, e a busca do usuário passa pela mesma análise. É ela que faz "Tênis", "tenis" e "TÊNIS" virarem a mesma palavra:
| Etapa | O que faz | "Tênis de corrida para corredores" |
|---|---|---|
| Separar palavras | Quebra o texto em termos | Tênis, de, corrida, para, corredores |
| Minúsculas | Ignora maiúsculas | tênis, de, corrida, para, corredores |
| Tirar acentos | Ignora acentos | tenis, de, corrida, para, corredores |
| Palavras vazias | Descarta "de", "para", "o", "a" | tenis, corrida, corredores |
| Radicalização | Reduz ao radical | ten, corr, corredor |
O PostgreSQL, com o dicionário de português e a extensão unaccent, transformou "Tênis de corrida para corredores que gostam de correr" em ten, corr (de "corrida" e de "correr"), corredor e gost. Com essa análise e um índice GIN:
| Busca no PostgreSQL | Encontrados | Tempo |
|---|---|---|
| "tenis corrida" | 5.000 tênis de corrida | 13 ms |
| "correr" | 50.000 produtos "de corrida" | 17 ms |
| "tenys corida" (com erros) | 0 | 2 ms |
Relevância
Encontrar não basta: entre 5 mil resultados, o usuário olha os primeiros. A relevância dá uma nota a cada documento. As ideias principais:
- Frequência do termo: o documento que menciona "corrida" mais vezes combina mais.
- Raridade do termo: "corrida" num catálogo de roupas esportivas diz pouco; "impermeável" diz muito. Termos raros pesam mais.
- Tamanho do campo: uma ocorrência num título curto vale mais que uma num texto longo.
- Peso do campo: o nome do produto conta mais que a descrição.
Os motores de busca usam o BM25, uma fórmula que combina as três primeiras ideias. O PostgreSQL tem o ts_rank, mais simples, e pesos por campo (setweight). Nos exemplos, o nome tem peso A e a descrição peso B: os "Tênis de corrida" vieram antes de qualquer produto que só fala de corrida na descrição.
Erros de digitação: o difícil
Nenhuma das duas buscas acima achou "tenys corida". O erro de digitação pede outra técnica, e os testes mostraram que ela é mais traiçoeira do que parece.
Trigramas (a extensão pg_trgm) comparam trechos de três letras. Com dois erros, a semelhança entre "tenys corida" e "Tênis de corrida Veloz" foi de 0,40, exatamente a mesma que com "Mochila de corrida Veloz". O trecho "corida" domina, e os trigramas não distinguem o tênis da mochila. Com um erro só ("tenis corida"), a semelhança subiu para 0,67.
Busca aproximada (fuzzy) no OpenSearch aceita uma ou duas letras diferentes em cada palavra. O primeiro teste, num campo com radicalização, encontrou zero resultados. O analisador mostrou por quê:
| Texto | Termos depois da análise (radicalizador leve de português) |
|---|---|
| Tênis de corrida | tenil, corrid |
| tenys corida | teny, corid |
O radicalizador transformou "tênis" em tenil, aplicando a regra de plurais como "papéis", que vira "papel". "tenys" ficou teny: duas letras de distância, mais do que a busca aproximada aceita numa palavra de quatro letras. A busca aproximada roda depois da análise, sobre os radicais. A saída foi um segundo campo, só com minúsculas e sem acentos, sem radicalização: nele, "tenys corida" encontrou os tênis de corrida em 13 ms.
O mesmo radicalizador leve não juntou "correr" e "corrida", que o dicionário do PostgreSQL juntou. Cada radicalizador tem regras próprias, com acertos e erros diferentes. Teste com as palavras do seu catálogo.
O índice de busca é uma cópia
Num motor de busca separado, os dados vivem no banco principal e são copiados para o índice. A cópia atrasa: no OpenSearch, um produto recém-gravado não apareceu numa busca feita logo em seguida, e apareceu 1,5 segundo depois. Por padrão, o índice fica visível para buscas a cada segundo. E toda cópia pode divergir da fonte, se uma gravação falhar no meio do caminho.
Por que isso importa quando a IA escreve o código
Um assistente que implementa "buscar produtos" costuma entregar o LIKE:
ILIKE '%termo%', que varre a tabela e não ignora acentos.- Nenhuma ordenação por relevância.
- O texto do usuário concatenado na consulta de busca, em vez de parâmetro.
- Busca aproximada aplicada sobre campos com radicalização, que falha de formas difíceis de entender.
- Um motor de busca separado sem plano para manter o índice igual ao banco.
- Nenhum teste com as palavras reais do domínio, com acento, plural e erro de digitação.
O próximo nível compara as opções de busca e como manter o índice sincronizado.
Prompts para aprofundar
Ao abrir no ChatGPT ou no Claude, o prompt é enviado ao serviço escolhido pela URL e também fica copiado na sua área de transferência.
Busca textual em cinco minutos
Explique em no máximo cinco minutos de leitura como funciona a busca textual, para alguém que sempre usou LIKE '%texto%'. Cubra só o essencial: por que o LIKE não serve, o índice invertido, a análise do texto (acentos, radicais, palavras vazias), a relevância e os erros de digitação. Use o exemplo da busca de uma loja online. No final, me faça três perguntas curtas para eu verificar se entendi, e corrija as minhas respostas.
Análise de texto por dentro
Quero entender o que acontece com o texto antes de ele entrar num índice de busca. Aja como um mentor e vá por etapas, esperando a minha resposta em cada uma: 1. Separação em termos, minúsculas e remoção de acentos. 2. Palavras vazias, e quando elas importam no domínio. 3. Radicalização em português, com exemplos de acertos e de erros. 4. Sinônimos. 5. Por que a busca do usuário passa pela mesma análise. Em cada etapa, me dê uma frase de um catálogo e peça que eu diga os termos que sairiam da análise.
Relevância e BM25
Quero entender como um motor de busca decide a ordem dos resultados. Explique: 1. Frequência do termo, raridade do termo e tamanho do campo. 2. A fórmula BM25, de forma intuitiva, sem precisar decorá-la. 3. Pesos por campo e combinação de consultas. 4. O ts_rank do PostgreSQL e o que ele não faz. 5. Sinais de negócio na relevância, como popularidade e estoque. Depois me mostre três documentos e uma busca e peça que eu os ordene, explicando a ordem certa.
Erros de digitação na busca
Quero entender as técnicas para a busca tolerar erros de digitação. Explique, com exemplos: 1. Distância de edição e a busca aproximada (fuzziness). 2. Por que a busca aproximada sobre termos radicalizados falha de formas inesperadas. 3. Trigramas, e por que eles se confundem com vários erros e palavras comuns. 4. Sugestões do tipo "você quis dizer". 5. A combinação de uma busca exata com uma tolerante, como segunda tentativa. Depois me dê três buscas com erros e pergunte qual técnica acharia cada uma.