Semantic search em produção: indexing, sharding, freshness
- ⬜🧬 Embeddings de busca: BGE-M3, e5, Voyage, Cohere v3(Search & Information Retrieval Profundo)
Recomendamos completar os pré-requisitos antes de seguir, mas nada te impede de continuar.
Do POC ao billion-scale
Em demo é fácil: 1k docs, embed com OpenAI, push no pgvector, query. Em produção sério, é outro mundo: bilhões de docs, freshness em minutos, latência sub-100ms p99, custo controlado, reindex sem downtime, golden set evoluindo.
Este módulo cobre o que separa demo de produção: pipelines de ingest (Airflow/Prefect), sharding strategy, hot/cold tiers, freshness lag, blue-green reindex, e o custo real de embedding 1B docs.
Referências: Pinecone, Qdrant, Weaviate engineering blogs (todos têm posts excelentes sobre escala); "Designing Data-Intensive Applications" (Kleppmann) cap. 11 (stream processing) e 12 (future of data systems).
Arquitetura completa de produção
- → mesmo modelo
- Conceito de arquitetura
- Fora da AWS
- Segurança e identidade
O desenho separa dois regimes: ingestão tolera atraso e trabalha em lote; consulta não tolera nada. Quase todo problema de busca em produção nasce de tratar os dois com o mesmo caminho de código.
- 1 · A consulta e o documento passam pelo mesmo modelo. Espaços vetoriais de modelos diferentes não são comparáveis. Trocar o modelo obriga a reindexar tudo — é decisão com custo, não configuração.
- 2 · O índice aproximado é uma troca deliberada. Ele não garante os vizinhos exatos: entrega quase sempre os certos, muito mais rápido. Em escala, exato é inviável — e essa perda precisa ser medida, não ignorada.
- 3 · Permissão filtra ANTES de ordenar. Ordenar e depois esconder o que a pessoa não pode ver devolve menos resultados do que o pedido e, pior, vaza a existência do documento. O filtro pertence à consulta.
- 4 · Ingestão é assíncrona; consulta não pode ser. Gerar vetores em lote é barato e tolera atraso. A consulta precisa responder em milissegundos. Misturar os dois caminhos degrada o que importa.
- 5 · Reindexar precisa acontecer sem derrubar a busca. Trocar modelo ou estratégia de fatiamento exige recriar tudo. Índice versionado com troca no fim permite fazer isso sem janela de indisponibilidade.
- 6 · Remoção é tão importante quanto inserção. Documento apagado que continua no índice aparece em resposta — e isso é incidente, não defeito de qualidade.
Ingestion: stream vs batch
| Critério | Stream (Kafka + workers) | Batch (Airflow / Spark) |
|---|---|---|
| Freshness | Minutos | Horas a dias |
| Throughput | Limitado por workers ativos | Pode escalar massivamente |
| Complexidade | Alta (state, retries, exactly-once) | Média (DAGs) |
| Custo | GPUs sempre ligadas (caro) | GPUs sob demanda (mais barato) |
| Falha tolerance | Kafka retém eventos, retry trivial | Idempotente, reprocessa partições |
| Quando usar | Hot data (últimas 24-48h), updates críticos | Backfill, cold data, reindex periódico |
Padrão híbrido: stream para dados quentes (últimos 7 dias) + batch noturno para cold tier (corpus completo, reembed se modelo mudou). Equilibra freshness, custo e ops.
Chunking: a decisão mais subestimada
# Estratégia 1: Fixed-size com overlap (default razoável)
from langchain.text_splitter import RecursiveCharacterTextSplitter
splitter = RecursiveCharacterTextSplitter(
chunk_size=500, # tokens, não chars (usar tiktoken)
chunk_overlap=100, # 20% típico
separators=["\n\n", "\n", ". ", " "], # tenta quebrar em fronteiras naturais
)
chunks = splitter.split_text(doc_text)
# Estratégia 2: Semantic (sentence + semantic similarity)
from llama_index.core.node_parser import SemanticSplitterNodeParser
splitter = SemanticSplitterNodeParser(
buffer_size=1,
breakpoint_percentile_threshold=95,
embed_model=OpenAIEmbedding(),
)
# Quebra onde a similaridade entre sentenças vizinhas cai abruptamente
# Estratégia 3: Hierarchical (chunk + parent context)
from llama_index.core.node_parser import HierarchicalNodeParser
parser = HierarchicalNodeParser.from_defaults(
chunk_sizes=[2048, 512, 128], # 3 níveis
)
# Retrieval no chunk de 128t (preciso)
# Resposta inclui parent de 512t ou 2048t (contexto)
# AutoMergingRetriever junta automaticamenteFreshness lag: o trade-off escondido
Lag típico em pipelines streaming bem feitos: 30s a 5min. Em batch noturno: até 24h. Decida pelo SLA: chat de suporte aguenta 5min? Probably. Catálogo e-commerce aguenta 24h? Probably not.
Blue-green reindex (mudança de modelo)
# Exemplo: troca de BGE-large-v1.5 para BGE-M3 sem downtime
# Vector DB: Qdrant
from qdrant_client import QdrantClient
client = QdrantClient(url="https://qdrant.internal")
# Step 1: criar coleção "green" com mesmo schema
client.create_collection(
collection_name="docs_v2_green",
vectors_config={"size": 1024, "distance": "Cosine"},
)
# Step 2: reindex corpus em "green" com novo modelo (job batch separado)
# Pode levar horas-dias. Roda em paralelo com tráfego em "blue" (atual).
# - Worker pool de N GPUs lendo do source
# - Embeddings com BGE-M3
# - Upsert em docs_v2_green
# Step 3: golden set validation (CI step antes do switch)
# Roda N=100 queries no green e no blue, compara NDCG@10 e MRR.
# Bloqueio: green precisa estar >= blue em pelo menos 3/4 métricas.
# Step 4: switch atomic via alias
client.update_collection_aliases(
change_aliases_operations=[
{"delete_alias": {"alias_name": "docs"}},
{"create_alias": {"collection_name": "docs_v2_green", "alias_name": "docs"}},
]
)
# A partir daqui, todas as queries para "docs" atingem v2.
# Step 5: manter blue por N dias para rollback
# Se algo der errado: reverter o alias é uma chamada API.
# Após N dias estáveis, drop blue: client.delete_collection("docs_v1_blue")Este padrão é aplicável a qualquer mudança não-trivial: troca de modelo, troca de chunking, mudança de schema, re-fine-tune do encoder. Zero downtime, rollback em 1 chamada API, validação rigorosa antes do switch.
Qual decisão o módulo chama de mais subestimada num sistema de busca semântica?
Custo real: embedding 1B documentos
Cenário: 1B documentos, doc médio 200 tokens
=============================================
OPÇÃO A: Voyage AI v3 ($0.12 / 1M tokens)
Total tokens: 1B × 200 = 200B
Custo: 200,000 × $0.12 = $24,000
Tempo: paralelo de chamadas API, ~1-2 dias com 100 workers concorrentes
OPÇÃO B: Cohere embed v3 ($0.10 / 1M tokens)
Total tokens: 200B
Custo: $20,000
Tempo: similar a Voyage
OPÇÃO C: OpenAI text-embedding-3-large ($0.13 / 1M tokens)
Custo: $26,000
OPÇÃO D: Self-hosted BGE-M3 em GPU AWS g5.xlarge ($1.006/h)
Throughput BGE-M3 fp16 batch 32: ~500 docs/s
Tempo: 1B / 500 = 2M segundos = 23 dias single-GPU
Custo: 23 × 24 × $1.006 = $555
COM 10 GPUs em paralelo (autoscale): 2.3 dias, ~$555 total
OPÇÃO E: Self-hosted BGE-M3 em A100 (g5 não é o ideal)
Throughput: ~2000 docs/s em A100 (4× mais)
10× A100 em paralelo: 1B em ~14 horas
Custo (Lambda Labs A100 $1.10/h): 14 × 10 × $1.10 = $154
CONCLUSÃO:
- < 100M docs: API é simples e custo aceitável
- 100M - 1B: avaliar caso a caso (DX vs custo)
- > 1B: self-hosted GPU vence por ~100x em custoHot/cold tier strategy
Em corpora com distribuição temporal (notícias, logs, tickets), 95% das queries atingem 5% dos docs (mais recentes). Não faz sentido manter tudo em SSD/RAM.
Observabilidade essencial
Métricas que TODO pipeline de semantic search deve emitir:
INGESTION
ingest_lag_seconds (T_doc_chegou_at_index - T_doc_criado_no_source)
embedding_queue_depth (backlog do worker pool)
embedding_throughput_dps (docs/segundo embarcados)
embedding_error_rate (% de docs que falharam)
INDEX
index_size_bytes (por shard)
index_doc_count
index_segments_count (Lucene: alerta se > 100 por shard)
hnsw_recall_proxy (recall vs força bruta em sample)
QUERY
query_latency_p50/p95/p99 (separar BM25 / dense / rerank / total)
query_qps
query_error_rate
cache_hit_rate
golden_set_ndcg_at_10 (rolling, calculado por CI)
golden_set_mrr (rolling)
ALERTAS CRÍTICOS
ingest_lag > 5min → page (freshness SLA quebrado)
golden_set_ndcg drop > 10% → page (regressão de qualidade)
query_latency_p99 > 500ms → page (UX comprometida)Quando NÃO usar vector search
📋 Busca em produção
Vector sozinho falha em out-of-vocabulary; BM25 sozinho falha em paráfrase; Hybrid + rerank é estado da arte sem trade-offs
Alt: Só BM25: ok para busca em logs, código fonte, dados estruturados onde queries são literais
Alt: Sem busca, só filtros: às vezes basta filtrar por metadados (tag, data, autor)
Alt: Vector puro: ok para corpora puramente conversacionais, mas raramente competitivo
Perguntas frequentes
❓ Quanto tempo leva um reindex em produção?
❓ Vector DB ou Postgres pgvector?
❓ Como sei se meu chunking está bom?
❓ Reranker antes ou depois do hybrid?
Resumo executivo
Produção em semantic search exige pipeline robusto: stream + batch híbrido, chunking validado, sharding estratégico, blue-green para mudanças não-triviais, hot/cold tiers para corpora temporais, observabilidade que detecta regressões antes do user reclamar.
Próximo: comparativo dos principais vector DBs — Qdrant, Weaviate, Pinecone, pgvector, LanceDB — e quando escolher cada um.
Fixando
Por que ingestão contínua e ingestão em lote coexistem em produção?
Qual métrica operacional precisa ser acompanhada além da qualidade da busca?
Terminou de ler?
Marcar como concluído registra o XP, mantém sua sequência e coloca 3 cartas deste módulo na fila de revisão espaçada.
Próximos passos sugeridos
Temas deste módulo
Discussão
Carregando comentários…