Por que entender um banco vetorial por dentro (e agora)
Em 2026, a busca semântica e o RAG (retrieval-augmented generation) deixaram de ser diferencial e viraram pré-requisito: assistentes, chatbots de atendimento e sistemas de busca corporativa dependem de encontrar documentos pelo significado, não por palavra-chave. Ferramentas gerenciadas (Pinecone, Weaviate, Qdrant, OpenSearch) resolvem isso, mas escondem o motor por trás da interface. Este tutorial mostra como construir um banco de dados vetorial do zero, em dez passos incrementais, usando apenas Python e NumPy — sem GPU, sem chave de API e com um modelo de embeddings que baixa na primeira execução.
O que você ganha (e o que não ganha)
✅ O que este tutorial entrega:
- Entender como documentos viram vetores de tamanho fixo e como a busca funciona por direção, não por palavra.
- Implementar similaridade por cosseno usando uma única multiplicação de matrizes.
- Adicionar filtro por metadados, validação de entrada e persistência em disco.
- Medir como a busca por força bruta escala e quando faz sentido migrar para índices aproximados (HNSW, IVF).
- Código que roda localmente, sem depender de serviço externo.
⚠️ O que ele não é:
- Não é um banco de produção: serve para aprendizado, não para milhões de documentos concorrentes.
- Não cobre índices aproximados em detalhe — apenas mostra o ponto em que eles se tornam necessários.
- O modelo de embeddings usado é o
all-MiniLM-L6-v2(384 dimensões), pequeno e em inglês; para português de alta qualidade você trocaria por um modelo multilíngue.
Requisitos
| Componente | Mínimo | Recomendado |
|---|---|---|
| Python | 3.9+ | 3.11+ |
| Dependências | numpy, sentence-transformers | as mesmas + ambiente virtual |
| Hardware | CPU comum | 8 GB de RAM (só por conforto) |
| Conhecimento prévio | Python básico | Noções de álgebra linear (produto escalar) |
| Tempo estimado | 30 minutos | 1 hora com experimentação |
A ideia central em uma frase
Todo o banco se apoia em um único truque: se cada vetor tiver comprimento 1 (normalizado), o produto escalar entre dois vetores é exatamente a similaridade por cosseno. Com isso, ranquear um corpus inteiro vira uma única multiplicação de matrizes. Todo o resto — filtros, persistência, guardas de entrada — é “contabilidade” para manter documentos, metadados e vetores em sincronia, de modo que a multiplicação continue significativa.
Passo a passo
Passo 1 — Instalação e configuração
Instale as duas dependências e inicie o script com os imports e dois helpers de exibição:
pip install numpy sentence-transformersimport time
from pathlib import Path
import numpy as np
from corpus import DOCS, META
from vector_db import VectorDB
def header(title):
print(f"\n{title}\n{'─' * len(title)}")
def show(results):
for hit in results:
print(f" {hit.score:+.3f} [{hit.meta['topic']:<7}] {hit.text}")Passo 2 — Construindo o índice
Criar o VectorDB carrega o modelo de embeddings; add() codifica cada documento em um vetor e armazena. O ponto crítico: o tamanho do índice não depende do tamanho do texto — cada documento, seja uma frase de seis palavras ou um ensaio de seis páginas, vira exatamente 384 números float32 (1.536 bytes). É isso que torna o índice previsível em tamanho e barato de varrer.
Passo 3 — A primeira busca
Com a query "what keeps a cell supplied with energy?", o topo da lista é "The mitochondria is the powerhouse of the cell" — o melhor hit compartilha apenas a palavra "cell" com a pergunta, e o segundo não compartilha nenhuma. Um índice por palavra-chave teria ranqueado de forma muito diferente, se é que encontraria algo.
Passo 4 — Buscar sem compartilhar uma palavra sequer
É o coração do exercício: as queries "why does my loaf taste sour" e "superheroes" não têm nenhuma palavra em comum com os documentos que retornam. A correspondência é por significado, não por termo literal.
Passo 5 — Lendo os scores
Uma busca vetorial sempre retorna k resultados, mesmo quando o corpus não tem nada relevante — ela simplesmente ranqueia o que existe. O score é o único sinal de qualidade: compare um +0.111 (irrelevante) com um +0.630 (relevante). Em produção você definiria um piso e retornaria nada abaixo dele.
Passo 6 e 7 — Filtro por metadados
Cada documento é adicionado com um dicionário {"topic": ...}. O argumento where mantém apenas documentos cujos metadados casam em todas as chaves. O filtro roda antes do ranqueamento: se você pedir k=5 e só houver 1 documento com aquele tópico, recebe 1 — nunca se preenche a lista com documentos que não casam só para atingir o k.
Passo 8 — Guardas de entrada
add() rejeita entradas malformadas com exceções claras: uma string solta em vez de lista, ou metadados que não batem um-para-um com os textos. Sem isso, uma string (que é iterável) seria desmembrada caractere a caractere, corrompendo o índice silenciosamente.
Passo 9 — Salvar e carregar
Vetores vão para .npy (compacto, sem parsing) e texto/metadados para .json (legível). O load() se recusa a abrir um índice criado por outro modelo — embeddings só fazem sentido em relação ao modelo que os produziu; misturá-los não seria "um pouco errado", seria um absurdo confiante.
Passo 10 — Como isso escala
Com um corpus sintético, o custo cresce linearmente: 1.000 documentos (1,5 MB) em ~0,01 ms, 100.000 (146,5 MB) em ~3,73 ms. Passado ~100 mil linhas, a ordenação começa a superar a varredura; em um milhão de documentos, o np.argpartition (top-k) fica em ~10 ms. Logo adiante está o ponto em que você adota um índice aproximado (HNSW, IVF) e troca um pouco de precisão por velocidade.
Tabela comparativa: força bruta vs. índices aproximados
| Abordagem | Precisão | Velocidade (100k+ docs) | Complexidade | Quando usar |
|---|---|---|---|---|
| Força bruta (dot product) | Exata | Lenta (linear) | Mínima | Até ~100 mil documentos |
| HNSW | Aproximada (~95%+) | Muito rápida | Média | Milhões de documentos |
| IVF (com PQ) | Aproximada | Rápida | Alta | Frotas gigantes, baixo custo de RAM |
Casos de uso reais
- Busca em base de conhecimento: encontrar a passagem certa em manuais e FAQs mesmo quando o usuário usa sinônimos.
- RAG para atendimento: recuperar os trechos relevantes antes de o LLM gerar a resposta, reduzindo alucinação.
- Deduplicação semântica: identificar documentos quase idênticos escritos com palavras diferentes.
- Recomendação: sugerir artigos "semelhantes" por proximidade vetorial em vez de por tag manual.
- Triagem automatizada: classificar tickets ou e-mails comparando-os com exemplos já rotulados.
Troubleshooting: erros comuns
- ❌
ModuleNotFoundError: sentence_transformers→ Causa: dependência não instalada. Solução:pip install numpy sentence-transformersdentro do ambiente virtual correto. - ❌ Busca retorna sempre os mesmos resultados irrelevantes → Causa: score baixo sem piso de corte. Solução: defina um limiar mínimo (ex.: 0,5) e descarte abaixo dele.
- ❌
ValueError: got 3 texts but 1 metadata entries→ Causa: listas de tamanhos diferentes. Solução: garanta um dicionário de metadados por documento. - ❌ Índice "confiante" mas errado após trocar de modelo → Causa: embeddings de modelos diferentes misturados. Solução: reindexe tudo ao mudar o modelo; o
load()deve recusar modelos distintos. - ❌ Primeira consulta muito mais lenta que as seguintes → Causa: warm-up da thread pool do NumPy. Solução: descarte a primeira medição; é artefato de benchmark, não do seu código.
FAQ
- Preciso de GPU? Não. O modelo
all-MiniLM-L6-v2é pequeno e roda em CPU. - Posso usar esse banco em produção? Para aprendizado sim; em produção, use uma solução gerenciada ou um índice aproximado com persistência e concorrência adequadas.
- Por que normalizar os vetores? Para que o produto escalar vire similaridade por cosseno e todo o ranqueamento vire uma multiplicação de matrizes.
- O que muda entre 25 e 25 milhões de documentos? O design não muda; só a estrutura de índice por baixo.
- Funciona em português? O modelo padrão é treinado majoritariamente em inglês; para melhor qualidade em português, troque por um modelo multilíngue (ex.:
paraphrase-multilingual-MiniLM).
Onde isso leva
A tendência é clara: com RAG virando padrão em produtos e agentes, entender a mecânica interna da busca semântica deixa de ser curiosidade acadêmica e vira vantagem prática — na hora de escolher uma ferramenta, dimensionar um índice ou debugar resultados estranhos. Em 2027, a fronteira deve se mover ainda mais para a combinação de busca semântica com re-ranking por modelos grandes, mas a fundação — normalizar, multiplicar, ranquear — permanece a mesma.
Descubra mais sobre noticiAI
Assine para receber nossas notícias mais recentes por e-mail.



