Qual a relação entre pesquisa textual e RAG?

Você abre o seu navegador e na barra de endereço digita uma pergunta. Em milésimos de segundos aparecem resposta na sua tela. No seu sistema indexador de logs, você digita uma palavra e quase instantaneamente aparecem várias ocorrências. Enquanto navega entre elas, percebe que você digitou o texto errado. Como o computador conseguiu encontrar mesmo assim? Como o computador sabe diferenciar a manga da sua camisa da manga que se colhe na árvore?

A pesquisa textual mais comum

Se você trabalha com software a tempo suficiente, já deve ter se deparado com uma pesquisa com essa:

select *
from tabela
where campo = 'Texto'

Uma pesquisa simples, que funciona bem quando você sabe exatamente o que quer encontrar. Uma cidade, por exemplo, você sabe o nome exato dela. Logo, digitar “Maringá” potencialmente trará todos os registros que possuem essa cidade. Potencialmente porque o usuário pode ter digitado “Maringá” ou “MARINGÁ” ou “MARINGA” ou … Acho que você já entendeu.

A solução para casos como esse sempre foi ter uma coluna de normalização. Nessa coluna, todos os textos seriam gravados com a mesma caixa (alta ou baixa) e sem acentos. E embora o usuário visse o que ele digitou em tela, a pesquisa sempre seria feita baseada nesse campo invisível especial.

select *
from tabela
where campo_sh = 'MEU TEXTO SEM ACENTO OU CEDILHA'

O problema muda quando eu não tenho precisão sobre a informação e preciso trabalhar com grupos de dados. Suponhamos que você queira fazer um sorteio para todas as pessoas que se chamam “Francisco” (é! chegou a minha vez!). Você escreveria algo como:

select *
from pessoas
where nome_sh like 'FRANCISCO%'

Acontece que, assim como tem o Francisco Thiago, também tem o Thiago Francisco. E tem as pessoas que possuem Francisco como sobrenome. Você pode pensar que alterar o SQL pra isso seja a melhor opção:

select *
from pessoas
where nome_sh like '%FRANCISCO%'

Ou, inocentemente, acredita que um .Where(campo => campo.Contains("FRANCISCO"))vai ser resolvido da melhor e mais indexada maneira pelo seu ORM. Não. Não vai. Ambas pesquisas cometem um crime contra o seu banco:

Banco de dados, índices e o FULL SCAN

A dica principal é: Sempre execute os SQL no banco e analise o retorno. Se você não vir nome de índices, ou ainda, ver as palavras “Table Scan” ou “Full Scan”, tire o dedo imediatamente do teclado e pense outra vez.

Bancos de dados utilizam índices para tornar suas pesquisas mais velozes. O tipo de índice mais comum é o da B-Tree ou “Árvore binária”. Essa estratégia de indexação e pesquisa torna mais rápida a localização de um registro, uma vez que reduz drasticamente o número de operações. Ela fez isso transformando o dado em uma árvore: Você tem um nó raiz (que idealmente é o valor médio da sua lista). E sempre que novos valores forem adicionados, a seguinte computação é feita: É maior que o nó que estou olhando? Sim: Procuro por um novo nó à direita. Não: Procuro um novo nó à esqueda. Se encontrar um nó, repito o processo. Se não encontrar um nó, um novo é criado com o valor que estou tentando inserir.

No exemplo acima, onde ficaria armazenado o número 15? Exatamente! No nó à direita do número 12.

O processo de pesquisa é da mesma forma. Imagine que eu queria buscar o 60, quantos passos seriam executados até que eu encontre o meu registro?

Com essa árvore, eu teria apenas 3 computações até encontrar o número 60, passando por 25 (o nó raiz), por 30 e enfim, 60. Por outro lado, se esses dados não estivessem indexados, eu teria que fazer uma leitura sequencial, passando por todos os oito registros.

A diferença aqui é pequena. Mas pense em uma tabela com milhões de registros e centenas de acessos por segundo. O impacto de uma leitura sequencial (não indexada) é enorme.

Mas nós falávamos de texto, não é mesmo! Então porque mostramos números? Os textos também tem uma representação numérica que os permite ter comparação de grandeza e ordenação. Isso depende muito do collate e do conjunto de caracteres (encoding) que estamos utilizando (já ouviu falar do UTF-8?). Dessa forma, podemos dizer que a árvore binária para textos ficaria assim:

Quando eu tenho pelo menos a primeira letra de uma palavra, é possível fazer a pesquisa pelo índice. Mas se a primeira letra já é uma incógnita, então fica impossível do banco de dados utilizar o índice. Ele precisa perguntar para cada registro: “Hei! Nesse campo tem algum Francisco?”.

EVITE PESQUISAR LIKE '%QUALQUER COISA%'!

Full Text Search – Permite indexar qualquer palavra.

Uma das coisas que aprendi fazendo sistema para bibliotecas é que aquilo que geralmente chamamos de índice – a lista de capítulos e suas respectivas páginas – na verdade é o sumário. O índice mesmo é aquele, geralmente no final do livro, que possui uma lista de palavras (geralmente no seu gênero masculino e no singular) e as respectivas páginas em que elas aparecem.

E se os bancos de dados copiassem essa estratégia?

É aí que entram os Full Text Search index (FTS)!

O processo é simples: O texto passa por uma sanitização inicial, removendo todas as stop word, que são apenas palavras de baixo impacto, geralmente preposições, artigos, advérbios de negação/afirmação e por aí vai. Depois as palavras são reduzidas a um radical comum, que é muito mais do que ir para o gênero masculino e singular. O parágrafo seguinte contém esse mesmo texto, porém após passar por esse processo chamado tokenização e stemming.

process simpl text pass sanitiz inicial remov stop word
palavr baix impact geral prepos artig adverb neg afirm
palavr reduz radical comum muit ir gener masculin singular
paragraf seguint cont mesm text porem apos pass process
cham tokeniz stemming

Feito isso, o banco é capaz de construir o índice invertido: para cada palavra é atribuída a referência a todos os registros em que ela aparece. Assim, se eu pesquisar por inconstitucional, não encontrarei nada. Mas se eu pesquisar por “processador”, muito provavelmente eu encontraria o parágrafo acima na minha pesquisa.

Você vai concordar comigo que temos um problema: Processador e processo são palavras diferentes. Completamente. O que acontece, então, se eu estiver procurando por um processo jurídico? Ou por um processador de alimentos?

O que é pesquisa vetorial?

Vamos partir de um exemplo mais comum: Cores!

Você com certeza já ouviu falar do sistema RGB. Ele permite que computadores possam representar mais de 16 milhões de cores, utilizando um array de três posições, cada uma variando de 0 até 255. Assim [255,0,0] representa uma cor enquanto [10,40,200] representa outra totalmente diferente.

Agora vamos supor que você possui a cor [240,5,0] e deseja procurar essa cor no banco de dados. Contudo os únicos registros de cor que o banco de dados possui são:

  • 1- [255, 0, 0]
  • 2- [239, 1, 0]
  • 3- [240, 4, 1]
  • 4- [0, 255, 0]

Se você observou bem, nenhum dos registros é uma correspondência exata a cor que estamos pesquisando. E os cálculos de “maior, menor, antes, depois” não fazem muito sentido nesse contexto em que estamos aplicando. O que estamos buscando aqui é uma correspondência por semelhança. Provavelmente nós teríamos um resultado hipotético (eu não fiz cálculo nenhum, tá?)

  • 95% – 3 – [240, 4, 1]
  • 90% – 2 – [239, 1, 0]
  • 60% – 1 – [255, 0, 0]
  • 05% – 4 – [0, 255, 0]

E então você se pergunta: Em que cenário encontrar ocorrências “ligeiramente próximas” do que eu estou buscando pode ser eficiente? Quando temos uma busca semântica!

Pesquisa semântica

O exemplo que utilizamos pra explicar é extremamente trivial. E por convenção, você pode deduzir, por exemplo, que o registro 4 possui a cor Verde. Isso porque a posição dois (verde) está com o seu peso máximo (255) enquanto as outras posições (vermelho e azul) estão com o peso mínimo.

E se utilizássemos essa mesma ideia de pesos para palavras? Frases? Textos? É justamente assim que a pesquisa semântica funciona. Você submete um texto para a avaliação de um determinado modelo. E o modelo te devolve um array que representa semanticamente o texto enviado. Você não precisa saber o que cada posição significa. No exemplo que dei, em momento algum falei das cores (ainda que, por convenção, você possa imaginá-las) exatamente porque não importa. Se a pesquisa for feita com o mesmo modelo de pesos, a pesquisa semântica vai funcionar.

Dessa forma um texto como: “O menino dormia no colo de sua mãe, um sono tranquilo envolto por paz” seria transformado em um array de “n” posições (centenas, milhares, bilhões!) e armazenado no banco de dados. Mais tarde você pode querer pesquisar por “uma história infantil”. A frase “uma história infantil” seria transformada, também, em um array e o banco calcularia a proximidade entre o seu input e o que está no banco, podendo retornar em ordem de semelhança para você.

O potencial da pesquisa semântica para programação

Consegue imaginar o potencial disso? Toda informação passível de vetorização poderia ser localizada através desse mesmo algoritmo de pesquisa. Imagem, som, texto, código fonte!

Se você encontrar um meio de vetorizar o seu código fonte, de modo a destacar a semântica da solução implementada em detrimento do boiler plate, você poderia perguntar: “Onde está implementada a rotina de conciliação de pagamentos?” e o próprio banco de dados poderia te responder quais classes estão semanticamente próximas da pergunta que você fez. Já pensou responder dúvidas de negócio com apenas um select?

Nesse ponto quem vem seguindo à risca as boas práticas de desenvolvimento de software sai na frente com uma grande vantagem. O texto em si já possui informação com qualidade suficiente para ser vetorizada. Agora quem não seguiu as boas regras, criando métodos com nomes duvidosos e classes com N+1 responsabilidades, vai sofrer um pouco para conseguir vetorizar sua base. Esse é um assunto que eu quero aprofundar mais tarde.

Onde RAG entra nisso tudo?

Se você tem uma base de código muito extensa e utiliza I.A., já deve ter ficado assustado ao ver os seus créditos serem consumidos com uma pesquisa simples pelo sistema: “Onde está implementado o health check?”. Isso porque a LLM teria que recorrer a memória dela ou qualquer documento de arquitetura que a ajude a encontrar o que está procurando. E depois ler todos os documentos que ela achar necessário. Só aí estourou a janela de contexto e o seu uso semanal. E se a LLM tivesse acesso a uma base que contém o código vetorizado?

É aqui que entra o RAG: Retrieval-Augmented Generation (Geração Aumentada por Recuperação). O seu primeiro passo é garantir um pipeline de vetorização, armazenando em uma base de dados o conteúdo relevante. Já o segundo passo é fornecer uma interface para que usuários (LLMs ou Humanos) possam acessar esse mesmo conteúdo. Uma LLM, por exemplo, ao ser questionada sobre onde está implementada a regra x ou, enquanto tenta descobrir onde incluir a alteração solicitada, pode utilizar a RAG de Código para enriquecer o seu contexto e melhorar a qualidade das suas respostas.

Mas vetorizar o código inteiro pode ser problemático. Ele está sempre mudando e é composto por centenas de milhões de linhas de código. Sem contar os arquivos de configuração, os yamls de deploy. Como isso seria possível?

Esse será o tema do próximo artigo! Espero você lá.

Deixe um comentário

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *

Este site utiliza o Akismet para reduzir spam. Saiba como seus dados em comentários são processados.