Busca Semântica na Prática com Embeddings
No artigo anterior vimos que a busca semântica tenta encontrar resultados com base no significado de uma consulta, e não apenas nas palavras exatas utilizadas.
Por exemplo, imagine que temos estes documentos:
1. "Como melhorar o desempenho do seu computador"
2. "Como preparar um bolo de chocolate"
3. "Como configurar um servidor Linux"
O usuário pesquisa:
"Meu PC está muito lento"
Uma busca lexical pode ter dificuldade porque o documento fala:
computador
enquanto o usuário escreveu:
PC
Já uma busca semântica consegue perceber que:
"Meu PC está muito lento"
e:
"Como melhorar o desempenho do seu computador"
possuem significados relacionados.
Para fazer isso, podemos utilizar Embeddings.
1. O que são Embeddings?
Um embedding transforma um texto em uma representação numérica.
Por exemplo:
"Meu PC está muito lento"
pode ser convertido conceitualmente em:
[0.12, -0.34, 0.87, 0.22, ...]
Outro texto:
"Como melhorar o desempenho do seu computador"
também recebe um vetor:
[0.10, -0.31, 0.84, 0.25, ...]
Como os vetores são parecidos, podemos considerar que os textos possuem uma relação semântica.
O fluxo é:
Texto
↓
Embedding Model
↓
Vetor
↓
Comparação
↓
Documentos mais semelhantes
2. Instalando o SDK
Vamos utilizar o SDK da OpenAI:
npm install openai
Também precisamos configurar nossa API Key:
OPENAI_API_KEY=sua_api_key
3. Gerando Embeddings
Vamos criar uma função para transformar um texto em embedding.
import OpenAI from "openai";
const openai = new OpenAI({
apiKey: process.env.OPENAI_API_KEY,
});
async function createEmbedding(text: string) {
const response = await openai.embeddings.create({
model: "text-embedding-3-small",
input: text,
});
return response.data[0].embedding;
}
Agora podemos fazer:
const embedding = await createEmbedding(
"Meu PC está muito lento"
);
console.log(embedding);
O resultado será um vetor numérico.
4. Criando nossos documentos
Vamos imaginar que temos uma pequena base de conhecimento:
const documents = [
{
id: 1,
text: "Como melhorar o desempenho do seu computador",
},
{
id: 2,
text: "Como preparar um bolo de chocolate",
},
{
id: 3,
text: "Como configurar um servidor Linux",
},
];
Precisamos gerar um embedding para cada documento.
for (const document of documents) {
document.embedding = await createEmbedding(
document.text
);
}
Agora cada documento possui:
Document
├── id
├── text
└── embedding
Por exemplo:
{
id: 1,
text: "Como melhorar o desempenho do seu computador",
embedding: [0.12, -0.31, 0.84, ...]
}
5. Comparando Embeddings
Uma maneira comum de comparar embeddings é utilizando Cosine Similarity.
A fórmula é:
similarity(A, B) =
A · B
───────
||A|| ||B||
Em TypeScript:
function cosineSimilarity(
a: number[],
b: number[]
): number {
let dotProduct = 0;
let magnitudeA = 0;
let magnitudeB = 0;
for (let i = 0; i < a.length; i++) {
dotProduct += a[i] * b[i];
magnitudeA += a[i] ** 2;
magnitudeB += b[i] ** 2;
}
return (
dotProduct /
(Math.sqrt(magnitudeA) *
Math.sqrt(magnitudeB))
);
}
Quanto maior o resultado, maior a similaridade entre os textos.
6. Realizando uma busca
Agora podemos criar nossa função de busca:
async function semanticSearch(
query: string
) {
const queryEmbedding =
await createEmbedding(query);
const results = documents.map((document) => ({
...document,
score: cosineSimilarity(
queryEmbedding,
document.embedding
),
}));
return results.sort(
(a, b) => b.score - a.score
);
}
Podemos pesquisar:
const results = await semanticSearch(
"Meu PC está muito lento"
);
console.log(results);
O resultado poderia ser:
[
{
id: 1,
text: "Como melhorar o desempenho do seu computador",
score: 0.89
},
{
id: 3,
text: "Como configurar um servidor Linux",
score: 0.42
},
{
id: 2,
text: "Como preparar um bolo de chocolate",
score: 0.08
}
]
Observe que o usuário pesquisou:
"Meu PC está muito lento"
Mas o resultado mais relevante foi:
"Como melhorar o desempenho do seu computador"
Mesmo que as palavras não sejam exatamente iguais.
Isso é o conceito fundamental da busca semântica.
7. O problema dessa implementação
Nosso exemplo funciona para demonstrar o conceito, mas não seria uma boa solução para produção.
Imagine:
10 documentos
Podemos comparar a query com todos eles.
Mas imagine:
10 milhões de documentos
Não queremos fazer:
Query
↓
Comparar com 10 milhões de vetores
↓
Ordenar tudo
Isso seria muito lento.
Em sistemas reais, normalmente utilizamos um:
Vector Database
ou um mecanismo de busca que suporte pesquisa vetorial.
Algumas opções são:
PostgreSQL + pgvector
Qdrant
Pinecone
Weaviate
Milvus
Elasticsearch
OpenSearch
O fluxo passa a ser:
DOCUMENTOS
↓
Embeddings
↓
Vector Database
↑
│
Usuário → Query → Embedding
│
↓
Vector Search
↓
Top K resultados
8. Usando PostgreSQL + pgvector
Se já utilizamos PostgreSQL, podemos armazenar embeddings utilizando a extensão:
pgvector
A tabela poderia ser:
CREATE TABLE documents (
id SERIAL PRIMARY KEY,
content TEXT NOT NULL,
embedding VECTOR(1536)
);
O tamanho do vetor depende do modelo utilizado.
Podemos armazenar:
Documento
↓
Embedding
↓
PostgreSQL
E depois procurar os vetores mais próximos.
Conceitualmente:
SELECT
id,
content
FROM documents
ORDER BY embedding <=> $1
LIMIT 10;
O operador:
<=>
pode ser utilizado para calcular distância de cosseno no pgvector.
O banco retorna os documentos mais próximos semanticamente da consulta.
9. Arquitetura completa
Uma aplicação real poderia funcionar assim:
Usuário
│
│ "Meu computador está lento"
▼
Backend
│
▼
OpenAI Embeddings
│
▼
Query Vector
│
▼
PostgreSQL + pgvector
│
▼
Top 10 documentos
│
▼
Backend
│
▼
Frontend
Se estivermos construindo um chatbot com RAG, podemos adicionar um LLM:
Usuário
│
▼
Pergunta
│
▼
Embedding
│
▼
Busca Semântica
│
▼
Documentos relevantes
│
▼
LLM
│
▼
Resposta
Nesse caso, a busca semântica funciona como a etapa responsável por encontrar o contexto correto para que o modelo de IA possa gerar uma resposta.
Conclusão
Uma implementação básica de busca semântica pode ser resumida em:
1. Transformar documentos em embeddings
2. Armazenar os embeddings
3. Transformar a query em embedding
4. Comparar os vetores
5. Ordenar pela similaridade
6. Retornar os documentos mais relevantes
O exemplo mais simples seria:
Documentos
↓
Embeddings
↓
Vector Database
↓
Query
↓
Embedding
↓
Busca por similaridade
↓
Resultados relevantes
A grande vantagem é que podemos encontrar documentos pelo significado, mesmo quando as palavras utilizadas na consulta são diferentes das palavras presentes no conteúdo.
Em aplicações reais, entretanto, a busca semântica geralmente é combinada com busca lexical, filtros e técnicas de reranking, formando uma busca híbrida mais precisa.