DevGuideTK
← Trilha Backend

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:

BuscaProdutos encontradosTempo
nome ILIKE '%tenis%'0116 ms
nome ILIKE '%tênis%'50.000113 ms
nome ILIKE '%tênis%corrida%'5.000161 ms
Nome ou descrição ILIKE '%correr%'0222 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:

EtapaO que faz"Tênis de corrida para corredores"
Separar palavrasQuebra o texto em termosTênis, de, corrida, para, corredores
MinúsculasIgnora maiúsculastênis, de, corrida, para, corredores
Tirar acentosIgnora acentostenis, de, corrida, para, corredores
Palavras vaziasDescarta "de", "para", "o", "a"tenis, corrida, corredores
RadicalizaçãoReduz ao radicalten, 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 PostgreSQLEncontradosTempo
"tenis corrida"5.000 tênis de corrida13 ms
"correr"50.000 produtos "de corrida"17 ms
"tenys corida" (com erros)02 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ê:

TextoTermos depois da análise (radicalizador leve de português)
Tênis de corridatenil, corrid
tenys coridateny, 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.