Indexação semântica externa — vector DB como contexto persistente

TL;DR

A cada tarefa, o agente tende a redescobrir o codebase: Grep, Read, Grep, Read. Em monorepo grande, isso queima 50k–200k tokens só pra “achar onde está X”. Indexação semântica parte o codebase em chunks, gera embeddings, guarda num vector DB, e expõe via MCP uma tool search_code(query) que devolve só os trechos conceitualmente relevantes. O agente faz uma busca em vez de 30 grep+read, com 5%–10% do custo de tokens. Requer infra externa (API de embedding + vector DB) e disciplina de re-indexação incremental — mas em monorepo >100k LOC, o investimento se paga em semanas.

Por que funciona — o mecanismo

Porque grep busca por texto exato e não sabe de semântica. Para encontrar onde um JWT token é validado, você pode tentar validateToken, verifyJWT, bearerToken, authMiddleware — são quatro buscas diferentes, cada uma retornando matches parcialmente relevantes, que você lê para filtrar manualmente. Em repositórios grandes, isso se transforma em 20-30 tool calls por tarefa só de “descoberta”.

Embedding search entende intenção, não texto. A query "how is JWT token validation implemented" encontra código relevante mesmo se os nomes forem checkAuth, tokenGuard, ou auth.verify — porque o embedding captura o conceito de validação de token, não as palavras específicas. Uma busca substitui dez.

flowchart LR
    subgraph "Sem indexação"
        A1["Grep('validateToken')"]
        A2["Read('auth/middleware.ts')"]
        A3["Grep('verifyJWT')"]
        A4["Read('auth/jwt.ts')"]
        A5["Grep('bearerToken')"]
        A6["... 25 tool calls"]
        A7["~50k tokens consumidos"]
    end

    subgraph "Com indexação semântica"
        B1["search_code('JWT token validation')"]
        B2["3 chunks relevantes retornados"]
        B3["~3k tokens consumidos"]
        B4["Agente resolve a tarefa"]
    end

    style A7 fill:#fff5f5,stroke:#ff6b6b
    style B3 fill:#f0fff4,stroke:#51cf66

Indexação semântica muda o paradigma de "explorar para encontrar" para "consultar e chegar". Em repos grandes, a diferença é entre passar 30 minutos de tokens em descoberta ou 2 minutos.

O que é

Aplicação clássica de RAG ao codebase: transformar o repositório inteiro em um índice vetorial consultável, externo ao contexto do agente.

Em vez do agente fazer:

Grep("authentication")        → 47 matches
Read("auth/login.ts")         → 200 linhas
Read("auth/middleware.ts")    → 150 linhas
Grep("validateToken")         → 12 matches
Read("auth/jwt.ts")           → 180 linhas
... (20 tool calls mais)

ele faz:

search_code("how is JWT token validation implemented")
  → chunk 1: auth/jwt.ts:34-67 (score: 0.94)
  → chunk 2: auth/middleware.ts:12-41 (score: 0.87)
  → chunk 3: auth/guards/bearer.ts:1-28 (score: 0.82)

A diferença não é só volume — é qualidade de busca: semantic search entende intenção, encontra código relevante mesmo com nomes diferentes, e ranqueia por similaridade conceitual em vez de match literal de texto.

O pipeline de indexação

flowchart TD
    codebase["Codebase completo"]
    treesitter["Chunking AST-aware\n(Tree-sitter)"]
    chunks["Chunks\n50-200 linhas cada\nfronteiras de função/classe"]
    embedding["API de embedding\n(OpenAI / Voyage / Gemini)"]
    vectordb["Vector DB\n(Milvus / Qdrant / Pinecone)"]
    mcp["MCP Server\nexposição ao agente"]
    agente["Claude Code"]

    codebase -->|"parse + slice"| treesitter
    treesitter --> chunks
    chunks -->|"text → vector"| embedding
    embedding -->|"store"| vectordb
    vectordb -->|"serve"| mcp
    mcp -->|"search_code(query)"| agente

    style chunks fill:#fff3e0,stroke:#ff9800
    style vectordb fill:#e8f4f8,stroke:#339af0
    style mcp fill:#f3f0ff,stroke:#7950f2

Tipos de chunking

A qualidade do retrieval depende de como o código é cortado:

EstratégiaComo funcionaPrósContras
Janela fixaBlocos de N linhasSimplesCorta funções no meio
Por ASTFunção/classe completaSemântica preservadaChunks de tamanho variável
HíbridoAST para pequenos, janela para grandesEquilíbrioImplementação mais complexa
Sliding windowBlocos com sobreposiçãoContexto nas bordasDuplicação, custo maior

Implementações sérias usam Tree-sitter para chunking AST-aware: parse da AST, detecção de boundaries (função, classe, método), corte respeitando os limites. O resultado é chunks semanticamente coerentes — um chunk de função é sempre a função inteira.

Re-indexação incremental

Indexação inicial pode demorar (minutos a horas em repo grande com 1M LOC). O que torna a abordagem sustentável é re-indexação incremental: a cada commit ou save, só os chunks cujos arquivos mudaram são re-embedados.

Implementações maduras comparam a Merkle tree do repo (hash do conteúdo de cada arquivo) com o estado anterior. Os arquivos com hash diferente são re-indexados; o restante permanece. Em repos com mudanças típicas de feature (10-30 arquivos por PR), a re-indexação incremental leva segundos.

Sem re-indexação automática, o índice fica stale e o agente recupera “código antigo” — que pode nem compilar mais.

Vídeo — construindo indexação de codebase ao vivo

Code with me — build codebase indexing for RAG and semantic search with live update (CocoIndex) mostra, na prática e ao vivo, o mesmo pipeline descrito nesta seção: chunking AST-aware com Tree-sitter, geração de embeddings e atualização incremental do índice conforme o código muda — sem precisar re-processar o repo inteiro a cada save. Útil pra quem quer ver o Merkle-tree-diff e o chunking por AST saindo do diagrama e virando código rodando.

Acesso via MCP

A camada que conecta o vector DB ao agente é um MCP server. Ele expõe tools que o agente invoca como qualquer outra ferramenta:

ToolUso
search_code(query, top_k=5)busca semântica — retorna chunks ranqueados por score
search_symbol(name)busca exata por nome de função/classe (fallback keyword)
index_status()health check do índice (quando foi a última re-indexação, cobertura)
reindex(path)força re-indexação de um path específico

Configuração:

claude mcp add claude-context \
  -e OPENAI_API_KEY=sk-... \
  -e MILVUS_TOKEN=... \
  -- npx @zilliz/claude-context-mcp@latest

Com o MCP ativo, o agente pode usar search_code como qualquer tool nativa — sem saber que há um vector DB por baixo.

Hybrid search — combinando semântico e keyword

Semantic search é forte para conceitos, fraco para símbolos exatos. grep é forte para símbolos exatos, fraco para conceitos. Implementações maduras combinam os dois:

hybrid_search(query="JWT validation", symbol="validateToken")
  → ranking combinado: vector similarity × BM25 score
  → o melhor dos dois mundos

O resultado combina relevância conceitual (embedding) com match literal (FTS5/BM25). É o padrão de busca empresarial — o mesmo que o Elasticsearch usa quando você habilita semantic search + keyword.

O achado empírico central em benchmarks de retrieval (texto e código) é que BM25 e dense retrieval têm recall complementar: os documentos que um erra, o outro frequentemente acerta. É esse complemento — não a soma bruta de scores — que faz hybrid search superar qualquer método isolado.

Casos práticos

Caso 1: onboarding de novo módulo em monorepo

Tarefa: “Preciso adicionar rate limiting no módulo de API gateway. Onde começa o código de middleware?”

# Sem indexação: engenheiro ou agente navega manualmente
Grep("middleware") → 120 matches em 40 arquivos
Read(...) → 3-4 arquivos para identificar o padrão

# Com indexação:
search_code("API gateway middleware pipeline")
  → chunk 1: gateway/middleware/chain.ts:1-45 (score: 0.96)
  → chunk 2: gateway/middleware/logger.ts:1-30 (score: 0.78)
  → chunk 3: gateway/config/middleware.ts:1-25 (score: 0.71)

"O pipeline de middleware está em gateway/middleware/chain.ts.
 Vou adicionar o rate limiting após o logger (linha 32) e antes do auth (linha 38)."

O agente chegou ao local correto com 1 tool call e 3 chunks (~90 linhas) em vez de 20+ tool calls.


Caso 2: busca cross-cutting em refactoring

Tarefa: “Precisamos trocar nosso logger de Winston para Pino. Onde o logger é instanciado e usado?”

search_code("logger instantiation and initialization")
  → chunk 1: utils/logger.ts:1-35 (instanciação central)
  → chunk 2: services/orders.ts:3-8 (import e uso)
  → chunk 3: config/logging.ts:1-28 (configuração)

search_code("log function calls in business logic")
  → 5 mais chunks com padrões de uso específicos

Em vez de grep por winston (que só encontra o que já usa o nome), a busca semântica encontra onde o conceito de “logging” é aplicado — incluindo arquivos que podem usar o logger via abstração.


Caso 3: auditoria de segurança por intenção

Tarefa: “Auditoria: há queries ao banco sem prepared statements?”

search_code("database query with string concatenation")
  → chunk 1: repositories/order.ts:87-102 (SQL concatenado diretamente)
  → chunk 2: legacy/reports.ts:45-60 (string interpolation em query)
  → chunk 3: admin/search.ts:112-128 (template literal com input do usuário)

"Encontrei 3 locations suspeitas de SQL injection.
 repositories/order.ts:92 é o mais crítico — usa input do usuário diretamente."

Grep por SELECT ou query( encontraria 150 resultados. A busca semântica encontrou os 3 que têm o padrão de risco.


Caso 4: onboarding de dev júnior num monorepo desconhecido

Tarefa: dev novo pergunta “como funciona o fluxo de checkout deste sistema? Preciso adicionar um cupom de desconto.”

# Sem indexação: dev pede ajuda no Slack, ou passa 1h navegando manualmente
Grep("checkout") → 80 matches espalhados em controllers, services, tests, mocks
Read(...) → 5-6 arquivos até montar o fluxo mental

# Com indexação:
search_code("checkout flow order total calculation")
  → chunk 1: checkout/service.ts:20-68 (orquestra o fluxo: cart → pricing → payment)
  → chunk 2: checkout/pricing.ts:1-40 (cálculo de total, onde descontos entram)
  → chunk 3: checkout/coupon.ts:1-25 (stub existente — só valida cupom, não aplica)

"O fluxo está em checkout/service.ts. O cálculo de total é em pricing.ts,
 e já existe um stub de validação de cupom em coupon.ts que não está
 conectado ao pricing ainda — é aí que a feature entra."

Esse é o caso onde indexação semântica paga dividendo além de tokens: reduz a dependência de conhecimento tribal. Um dev júnior (humano ou agente) chega à mesma resposta que um sênior chegaria perguntando no Slack — mas em segundos, sem interromper ninguém. O ganho não é só de custo, é de autonomia de onboarding: novos membros do time (ou uma nova sessão de agente sem memória do repo) reconstroem contexto sozinhos.

Quando usar

CenárioVale?Por quê
Monorepo >100k LOCSimGrep direto vira gargalo
>20 grep+read por sessãoSimCusto de descoberta domina
Repo pequeno (<20k LOC)NãoGrep resolve mais simples
Codebase instável (refactors diários)CuidadoÍndice fica stale entre sessões
Sem budget pra embedding APINãoUse grep + sandboxing
Busca por símbolo exatoNãoGrep/LSP são mais precisos

Custo real da abordagem

ItemCusto estimado
Embedding API (indexação inicial 1M LOC)~$5 (OpenAI text-embedding-3-small)
Vector DB (free tier)$0/mês até ~500k chunks
Vector DB (pago)$20-100/mês a partir de alguns GB
Re-indexação incremental diária<$0.10/dia em repos típicos
Setup e manutenção10-20h iniciais + ~1h/mês

Em conta Claude Code de $500/mês com sessões longas em monorepo, redução de 30% no custo de tokens paga a infra em menos de 10 dias.

Comparativo de modelos de embedding para código

Nem todo embedding é igual para código. Modelos treinados em linguagem natural pura têm desempenho inferior na recuperação de código:

ModeloDimensõesCusto/1M tokensNota para código
OpenAI text-embedding-3-small1536$0.020Boa escolha geral, equilibrada
OpenAI text-embedding-3-large3072$0.130Melhor qualidade, 6× mais caro
Voyage code-21536$0.120Treinado especificamente para código
Cohere embed-english-v3.01024$0.100Boa para código em inglês
nomic-embed-code (local)768$0 (GPU)Self-hosted, sem envio de dados

Para a maioria dos casos, text-embedding-3-small é o ponto de partida correto: custo baixo, qualidade adequada, fácil de trocar depois. Se você precisa de maior precisão (repo crítico, auditoria de segurança), voyage-code-2 — treinado especificamente para código — costuma superar os modelos gerais por 10-15% em benchmarks de code retrieval.

Benchmarks de retrieval em código

Retrieval de código tem sua própria linhagem de benchmarks, distinta de retrieval de texto genérico — porque código tem estrutura (assinatura de função, hierarquia de classe) que texto em linguagem natural não tem.

CodeSearchNet foi o benchmark fundador (2019): pares de query-em-linguagem-natural → função-em-código, em seis linguagens. As baselines originais já comparavam IR clássico (ElasticSearch, BM25, TF-IDF) contra bi-encoders neurais treinados com objetivo contrastivo — o mesmo desenho conceitual usado hoje em embeddings de produção. Modelos mais recentes como CodeBERT e GraphCodeBERT elevaram a régua ao incorporar estrutura de AST diretamente no treinamento, não só o texto do código.

O padrão que a pesquisa mais recente confirma — e que a seção de hybrid search acima antecipou — é o desenho em duas etapas recall-then-rerank: um bi-encoder leve (opcionalmente com BM25 fundido) recupera um pool amplo de candidatos com baixo custo computacional; um cross-encoder mais caro reordena só esse pool reduzido para o ranking final. É o mesmo motivo pelo qual search_code retorna top_k=5 em vez de escanear o repo inteiro toda vez — a etapa cara (reranking semântico completo) só roda sobre um conjunto pequeno e pré-filtrado.

flowchart LR
    query["Query"] --> recall["Recall: bi-encoder<br/>+ BM25 (barato)"]
    recall --> pool["Pool de candidatos<br/>(ex: top-50)"]
    pool --> rerank["Rerank: cross-encoder<br/>(caro, mas só 50 docs)"]
    rerank --> final["Top-5 final"]

    style recall fill:#e8f4f8,stroke:#339af0
    style rerank fill:#fff3e0,stroke:#ff9800

Um exemplo concreto dessa estratégia de fusão é o sistema TOSS, que combina múltiplos sinais de retrieval (incluindo BM25) e atinge MRR (Mean Reciprocal Rank) de 0.763 com latência bem menor do que reranking puro sobre o corpus inteiro — evidência empírica de que hybrid search não é só “mais robusto”, é também mais barato em produção, porque a etapa cara opera sobre um pool pequeno.

O benchmark confirma na prática o que a seção de hybrid search argumenta em teoria: BM25 e embeddings erram em pontos diferentes, e a arquitetura recall-then-rerank é o jeito padrão de capturar esse complemento sem pagar o custo de reranking sobre o corpus inteiro.

Integração com o fluxo de desenvolvimento

A re-indexação precisa acontecer automaticamente. As três abordagens comuns:

Via git hook

# .git/hooks/post-commit
#!/bin/bash
echo "Re-indexando arquivos modificados..."
git diff --name-only HEAD~1 | xargs claude-context reindex
echo "Indexação concluída."

Roda após cada commit. Indexa só os arquivos que mudaram. Latência: 1-5s para commits típicos.

Via file watcher (em desenvolvimento ativo)

# Configurado no CLAUDE.md ou como processo separado
npx chokidar-cli 'src/**/*.ts' -c 'claude-context reindex {path}'

Re-indexa ao salvar. Mais responsivo, mas pode sobrecarregar se você salva frequentemente sem completar a lógica.

Via CI/CD (para pipelines)

# .github/workflows/reindex.yml
on:
  push:
    branches: [main]
 
jobs:
  reindex:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npx @zilliz/claude-context-mcp reindex-full
        env:
          OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
          MILVUS_TOKEN: ${{ secrets.MILVUS_TOKEN }}

Para pipelines multi-branch, re-indexa em merge para main. Sub-agents sempre recebem o índice atualizado do trunk.

Como avaliar a qualidade do retrieval

Antes de confiar no índice, valide:

Teste de sanidade: para 5 funções que você conhece bem no codebase, faça search_code com descrição do que elas fazem (sem mencionar o nome). O chunk correto deve aparecer no top-3.

search_code("rate limit requests per user per minute")
→ Esperado: chunk de rate_limiter.ts no top-3

search_code("send email notification on order creation")
→ Esperado: chunk de notifications/order.ts no top-3

Se menos de 3 dos 5 testes retornam o chunk correto no top-3, o chunking ou o modelo de embedding precisam de ajuste.

Cobertura: index_status() deve mostrar cobertura próxima de 100% dos arquivos .ts, .py, .java, etc. Se está em 60%, o indexer está falhando silenciosamente em alguns arquivos.

Drift detection: compare timestamps da última re-indexação com o último commit. Se a diferença for >1h em projeto ativo, a automação de re-indexação não está funcionando.

Teoria subjacente — por que embeddings funcionam para código

Embeddings são representações vetoriais de alta dimensão (~1536 dimensões para text-embedding-3-small) onde distância no espaço vetorial corresponde a similaridade semântica. Dois trechos de código semanticamente semelhantes (mesma intenção, lógica similar) ficam próximos no espaço — mesmo que os tokens de texto sejam completamente diferentes.

O modelo de embedding aprendeu, durante o treinamento, que validateToken(jwt, secret) e checkAuth(bearer, key) implementam o mesmo conceito. Essa representação latente é o que torna semantic search possível — o que grep nunca consegue, porque ele trabalha com tokens de texto, não com significado.

A limitação fundamental: embeddings capturam intenção e conceito, não estrutura. Para saber quais funções chamam X, você precisa de análise de AST (knowledge graph). Para saber onde X é discutido conceitualmente, você precisa de embeddings. As duas abordagens são ortogonais.

Armadilhas comuns

Índice stale é pior do que não ter índice

Um agente que confia num índice stale vai propor patches baseados em código que já foi refatorado. Se a re-indexação incremental não está automatizada (hook no git commit, watch em save), o índice é uma fonte de erro. Ou automatize completamente ou não use — não há meio-termo seguro.

Chunking ruim invalida a busca

Chunking que corta funções no meio produz chunks semanticamente quebrados: a função começa num chunk, termina em outro, e nenhum dos dois é semanticamente coerente. A busca retorna “pedaços de função” que não fazem sentido fora do contexto. Vale o investimento em chunking AST-aware desde o início — implementar depois exige re-indexar tudo.

Dados sensíveis na API de embedding

Embeddings de OpenAI, Cohere, Voyage etc. implicam enviar o código para o provedor. Em codebase corporativo com IP sensível ou dados regulados (HIPAA, PCI), isso é risco. Avalie embeddings locais (sentence-transformers, nomic-embed) ou provedores com cláusulas explícitas de não-retenção de dados.

Não indexar o .indexignore

node_modules/, dist/, vendor/, geração automática de código — tudo isso entra no índice e polui retrievals. Configure um .indexignore tão agressivo quanto seu .gitignore. Chunks de bibliotecas de terceiros ranqueados acima do código próprio é o sintoma mais comum de índice mal configurado.

Confiar na busca semântica para tudo

Semantic search é ótimo para “onde tá a lógica de X” e ruim para “todas as chamadas exatas de função Y”. Para busca de símbolo precisa, grep e LSP são mais confiáveis. Implementações maduras usam hybrid search (vetor + BM25) — mas mesmo assim, para navegação estrutural de dependências, o knowledge graph (04 - Knowledge graph local com AST) é mais adequado.

Contextual retrieval — melhorando a qualidade dos chunks

Uma técnica da Anthropic que melhora a qualidade do retrieval em 30-50% em benchmarks: antes de embedar cada chunk, adiciona contexto do documento completo ao chunk.

Em vez de embedar:

// chunk original — sem contexto
export function processPayment(amount: number, currency: string): Promise<Receipt> {
  return stripe.charge({ amount, currency });
}

Você embeda:

Contexto: Este arquivo (src/services/payment.ts) implementa a integração
com Stripe para processamento de pagamentos. A função processPayment é o
ponto central de cobrança, chamada pelo checkout controller.

export function processPayment(amount: number, currency: string): Promise<Receipt> {
  return stripe.charge({ amount, currency });
}

O embedding do chunk+contexto captura melhor a intenção do código. Quando alguém busca “Stripe payment processing”, o chunk com contexto explícito de “integração com Stripe” ranqueia mais alto do que o chunk com só o código.

O custo é ~2× mais tokens de embedding (porque você embeda chunk+contexto em vez de só o chunk). Para projetos onde a qualidade de retrieval é crítica, o investimento compensa.

Como explicar em inglês

Semantic search indexing applies RAG (Retrieval-Augmented Generation) to codebases: instead of grepping and reading files to “rediscover” the codebase on every session, you pre-index all code into a vector database and expose it via an MCP server tool (search_code(query)). The agent retrieves only the semantically relevant chunks, typically reducing discovery token cost by 90%+ compared to grep-and-read exploration.

The key differentiator from keyword search is that embedding models represent semantic intent, not literal text. A query like “JWT validation implementation” retrieves code implementing auth token checks even if the actual function names are checkAuth, tokenGuard, or bearerValidate — because the embedding captures what the code does, not what it’s called.

In a technical interview, you might say:

“For large monorepos I deploy semantic search via MCP: Tree-sitter chunking preserves function boundaries, embeddings capture semantic intent, and the agent gets search results instead of grep results. The practical shift is from ‘explore to find’ to ‘query and arrive’ — a query like ‘database connection pool implementation’ returns three relevant chunks directly, instead of the agent spending 30 tool calls exploring the repo. I combine it with keyword fallback (FTS5) for symbol-exact search, since embeddings aren’t always more precise than grep for exact symbol names.”

Tabela PT ↔ EN

PortuguêsEnglishContexto
Indexação semânticaSemantic indexingtécnica de indexar código com embeddings
Busca semânticaSemantic searchbusca por intenção, não texto exato
EmbeddingEmbedding (sem tradução)representação vetorial de alta dimensão
Banco de vetoresVector databasebanco que armazena e busca embeddings
ChunkingChunking (sem tradução)dividir código em pedaços indexáveis
Busca por ASTAST-aware chunkingchunking que respeita fronteiras da árvore sintática
Recuperação aumentadaRetrieval-Augmented Generation (RAG)padrão de buscar contexto externo antes de gerar
Índice staleStale indexíndice desatualizado após mudanças no código
Busca híbridaHybrid searchcombinação de vetor + keyword (BM25)

O que vem a seguir

Indexação semântica responde “onde está o código que faz X?” A próxima camada responde “o que é afetado se eu mudar Y?” — que requer análise estrutural, não semântica.

Veja também

Fontes