MÓDULO 3.1 GA

📚 Indexação: chunking, embeddings e BM25 híbrido

Como transformar um corpus em um índice consultável: estratégias de chunking, embeddings densos, BM25 sparse, e por que híbrido bate ambos.

6
Tópicos
60
Minutos
Intermediário
Nível
Prático
Tipo

RAG bom é resultado de indexação boa. Cada decisão — tamanho do chunk, modelo de embedding, índice sparse vs. denso — tem trade-offs concretos de recall, latência e custo. Aqui você aprende a navegar essas decisões.

🏗️ Pipeline canônico de indexação

Cada documento entra, é processado por uma pipeline determinística e sai como N chunks indexados. Reproducibilidade vem de fixar cada passo.

💡 Comece com 500 tokens + 50 overlap

Para a maioria dos corpora técnicos em PT-BR, chunks de ~500 tokens com 10% overlap dão recall razoável. Ajuste depois com base em eval real, não em achismo.

1

✂️ Chunking: dividir o corpus

Tamanho, overlap, fronteiras semânticas

Chunking é a primeira e mais consequente decisão. Chunk muito grande (>2000 tokens) dilui relevância — embedding fica genérico. Chunk muito pequeno (<100 tokens) perde contexto. Default robusto: 500 tokens com overlap de 10-20%, cortando em fronteira semântica (parágrafo, fim de sentença). Recursive splitter (LangChain) ou SemanticChunker (LlamaIndex) implementam isso bem.

chunking em fronteira de parágrafo
def chunk_by_paragraph(text: str, target=500, overlap=50) -> list[str]:
    paras = text.split('\n\n')
    chunks, current = [], ''
    for p in paras:
        if len(current) + len(p) > target:
            chunks.append(current)
            current = current[-overlap:] + '\n\n' + p  # overlap
        else:
            current += '\n\n' + p
    chunks.append(current)
    return chunks
📑 Resumo navegável
O que é: Quebrar documentos em pedaços (chunks) de 200-1000 tokens com overlap de 10-20%. Pode ser por caracteres, sentenças ou parágrafos.
Por que aprender: Chunks muito grandes diluem relevância; muito pequenos perdem contexto. Overlap evita corte abrupto entre fronteiras.
Conceitos-chave: Sliding window, semantic chunking, fronteira de seção, recursive splitter.
2

🧮 Embeddings densos: vetores semânticos

Modelos sentence-transformers

Embeddings densos mapeiam texto em vetor (768-3072 dimensões) onde proximidade vetorial ≈ similaridade semântica. Modelos canônicos: BGE-large (open), OpenAI text-embedding-3-large, Cohere embed-v3. Capturam paráfrase ('automóvel' ≈ 'carro') que BM25 sozinho perde.

embedding com sentence-transformers
from sentence_transformers import SentenceTransformer
model = SentenceTransformer('BAAI/bge-large-en-v1.5')

chunks = ['o carro é vermelho', 'o automóvel é vermelho', 'o gato dorme']
vectors = model.encode(chunks, normalize_embeddings=True)

# vectors[0] @ vectors[1] ~ 0.92 (similar)
# vectors[0] @ vectors[2] ~ 0.15 (não relacionado)
📑 Resumo navegável
O que é: Converte texto em vetor (768-3072 dim) onde proximidade vetorial ≈ similaridade semântica. Modelos: bge, mpnet, OpenAI ada/text-3.
Por que aprender: Captura sinônimos e paráfrase que BM25 perde ('automóvel' vs 'carro'). Base do retrieval moderno.
Conceitos-chave: Cosine similarity, dual encoder, MTEB benchmark, dimensionalidade.
3

🔤 BM25: o sparse clássico que ainda manda

TF-IDF refinado

BM25 é probabilístico, baseado em TF-IDF. Não usa ML, mas é excelente em match exato: números, IDs, nomes próprios, acrônimos — coisas que embeddings densos costumam errar. Em queries factuais ('quem é Liu et al. 2023?'), BM25 frequentemente bate dense retrieval sozinho.

BM25 com rank_bm25
from rank_bm25 import BM25Okapi
tokenized = [doc.lower().split() for doc in corpus]
bm25 = BM25Okapi(tokenized)

query = 'Liu 2023 lost in middle'.lower().split()
scores = bm25.get_scores(query)
top10 = sorted(zip(corpus, scores), key=lambda x: -x[1])[:10]
📑 Resumo navegável
O que é: Algoritmo probabilístico de relevância baseado em frequência de termo (TF) e raridade (IDF). Não usa ML.
Por que aprender: Robusto, rápido, captura match exato (números, IDs, nomes próprios) que embeddings densas erram.
Conceitos-chave: TF-IDF, BM25, sparse vector, lexical match, rare term boost.
4

🤝 Híbrido: BM25 + denso, fusão por RRF

O melhor dos dois

Híbrido roda BM25 + dense em paralelo, depois funde via Reciprocal Rank Fusion (RRF). RRF combina rankings sem precisar normalizar scores: score = Σ 1/(k + rank_i) com k=60. Em benchmark BEIR, híbrido bate cada método sozinho consistentemente.

RRF combinando dois rankings
def rrf(rankings: list[list[int]], k: int = 60) -> list[int]:
    scores = {}
    for ranking in rankings:
        for rank, doc_id in enumerate(ranking):
            scores[doc_id] = scores.get(doc_id, 0) + 1 / (k + rank)
    return [d for d, _ in sorted(scores.items(), key=lambda x: -x[1])]

top_bm25  = bm25_search(query, top_k=50)
top_dense = vector_search(query, top_k=50)
top_hybrid = rrf([top_bm25, top_dense])[:20]
📑 Resumo navegável
O que é: Roda ambos em paralelo, funde rankings via Reciprocal Rank Fusion (RRF) ou peso linear (alpha).
Por que aprender: Ganhos consistentes em benchmarks (BEIR). Cada método cobre falhas do outro: BM25 pega match exato, denso pega paráfrase.
Conceitos-chave: RRF, alpha-fusion, hybrid search, ColBERT (alternativa late-interaction).
5

🗂️ Vector stores: o que escolher

FAISS, pgvector, Qdrant, Pinecone

Vector stores fazem ANN (approximate nearest neighbor) com algoritmos como HNSW (hierarchical navigable small world) ou IVF. Em 100k documentos, cosine similarity exato é O(n) — inviável em produção. ANN entrega ~99% de recall em <5ms. Para começar: FAISS em memória ou pgvector em Postgres existente. Cloud: Qdrant, Pinecone, Weaviate.

comparação de vector stores (2026-Q2)
StoreHostingQuando usar
FAISSbiblioteca local<1M docs, dev/research
pgvectorPostgres existenteJá tem Postgres, simplicidade
Qdrantself-hosted ou cloudFiltros complexos, hybrid search nativo
Pineconecloud onlyNão quer operar infra
📑 Resumo navegável
O que é: Bancos de dados otimizados para nearest-neighbor search em vetores. Local (FAISS, pgvector) ou hosted (Qdrant Cloud, Pinecone).
Por que aprender: Sem isso, busca em 100k embeddings vira O(n) inviável. Vector stores fazem ANN (HNSW, IVF) em ms.
Conceitos-chave: ANN (approximate nearest neighbor), HNSW, IVF, recall@k, índice em memória vs. disco.
6

🏷️ Metadata e filtros: além do match semântico

Filtragem antes/depois do retrieval

Retrieval só por similaridade semântica é ingênuo. Em corpus heterogêneo, você quer filtros: 'eventos de 2024' não deve trazer chunks de 2019, mesmo que semanticamente similares. Vector stores modernos suportam metadata filters (pre-filter ou post-filter). Pre-filter (Qdrant, pgvector) é mais eficiente; post-filter (FAISS) força recuperar mais e descartar.

filtro por metadata em Qdrant
from qdrant_client import QdrantClient
from qdrant_client.models import Filter, FieldCondition, MatchValue

client.search(
    collection_name='docs',
    query_vector=q,
    query_filter=Filter(must=[
        FieldCondition(key='year', range={'gte': 2024}),
        FieldCondition(key='lang', match=MatchValue(value='pt')),
    ]),
    limit=10,
)
📑 Resumo navegável
O que é: Anexar metadados a cada chunk (data, autor, categoria, idioma) e filtrar por eles antes ou depois da busca vetorial.
Por que aprender: Pergunta 'eventos de 2024' não deve trazer chunks de 2019, mesmo que semanticamente similares. Filtros resolvem.
Conceitos-chave: Metadata filtering, pre-filter, post-filter, namespace.

📑 Resumo navegável dos tópicos

1 ✂️ Chunking: dividir o corpus — Tamanho, overlap, fronteiras semânticas
O que é: Quebrar documentos em pedaços (chunks) de 200-1000 tokens com overlap de 10-20%. Pode ser por caracteres, sentenças ou parágrafos.
Por que aprender: Chunks muito grandes diluem relevância; muito pequenos perdem contexto. Overlap evita corte abrupto entre fronteiras.
Conceitos-chave: Sliding window, semantic chunking, fronteira de seção, recursive splitter.
2 🧮 Embeddings densos: vetores semânticos — Modelos sentence-transformers
O que é: Converte texto em vetor (768-3072 dim) onde proximidade vetorial ≈ similaridade semântica. Modelos: bge, mpnet, OpenAI ada/text-3.
Por que aprender: Captura sinônimos e paráfrase que BM25 perde ('automóvel' vs 'carro'). Base do retrieval moderno.
Conceitos-chave: Cosine similarity, dual encoder, MTEB benchmark, dimensionalidade.
3 🔤 BM25: o sparse clássico que ainda manda — TF-IDF refinado
O que é: Algoritmo probabilístico de relevância baseado em frequência de termo (TF) e raridade (IDF). Não usa ML.
Por que aprender: Robusto, rápido, captura match exato (números, IDs, nomes próprios) que embeddings densas erram.
Conceitos-chave: TF-IDF, BM25, sparse vector, lexical match, rare term boost.
4 🤝 Híbrido: BM25 + denso, fusão por RRF — O melhor dos dois
O que é: Roda ambos em paralelo, funde rankings via Reciprocal Rank Fusion (RRF) ou peso linear (alpha).
Por que aprender: Ganhos consistentes em benchmarks (BEIR). Cada método cobre falhas do outro: BM25 pega match exato, denso pega paráfrase.
Conceitos-chave: RRF, alpha-fusion, hybrid search, ColBERT (alternativa late-interaction).
5 🗂️ Vector stores: o que escolher — FAISS, pgvector, Qdrant, Pinecone
O que é: Bancos de dados otimizados para nearest-neighbor search em vetores. Local (FAISS, pgvector) ou hosted (Qdrant Cloud, Pinecone).
Por que aprender: Sem isso, busca em 100k embeddings vira O(n) inviável. Vector stores fazem ANN (HNSW, IVF) em ms.
Conceitos-chave: ANN (approximate nearest neighbor), HNSW, IVF, recall@k, índice em memória vs. disco.
6 🏷️ Metadata e filtros: além do match semântico — Filtragem antes/depois do retrieval
O que é: Anexar metadados a cada chunk (data, autor, categoria, idioma) e filtrar por eles antes ou depois da busca vetorial.
Por que aprender: Pergunta 'eventos de 2024' não deve trazer chunks de 2019, mesmo que semanticamente similares. Filtros resolvem.
Conceitos-chave: Metadata filtering, pre-filter, post-filter, namespace.

✓ O que FAZER

  • Hash do corpus + versão da pipeline em metadata
  • Híbrido (BM25 + denso) com RRF como default
  • Filtros de metadata (data, idioma, fonte)
  • Chunks com fronteira semântica (parágrafo)

✗ O que NÃO fazer

  • Re-indexar e perder reproducibilidade
  • Confiar só em embeddings densas
  • Match puramente semântico em corpus heterogêneo
  • Cortar no meio de sentença ou fórmula

🚫 Quando NÃO usar

💻 Exemplo de código

from fec_sdk import check_compat
check_compat("modulo-3-1")

# Pseudo-código provider-neutral
from fec_sdk.indexing import Pipeline, Chunker, EmbedderHTTP, BM25

pipeline = Pipeline([
    Chunker(size=500, overlap=50, strategy="paragraph"),
    EmbedderHTTP(model="bge-large-en-v1.5"),
])

# Indexa todos os docs
for doc in corpus:
    chunks = pipeline.run(doc)
    vector_store.add(chunks, metadata={"doc_id": doc.id, "date": doc.date})

# BM25 em paralelo
bm25 = BM25(chunks)
# Híbrido: query → top-50 BM25 ∪ top-50 denso → RRF → top-10

🏋️ Exercício hands-on

Indexe um corpus de 100 abstracts ArXiv (em fixtures/arxiv-cs-100/) com pipeline determinística. Eval de recall@10 deve atingir ≥0.7 no golden FEC-GS-RAG-v1. Em exercicios/modulo-3-1/.

📚 Bibliografia

🎯 Resumo do Módulo

  • Chunking: 500 tokens + overlap, fronteira semântica.
  • Embeddings densas capturam paráfrase; BM25 pega match exato.
  • Híbrido com RRF bate ambos consistentemente.
  • Vector stores usam ANN (HNSW, IVF) para escalar.
  • Metadata + filtros são tão importantes quanto similaridade.

Próximo Módulo:

Recuperação e reranking