Elasticsearch internals: Lucene, segments, shards, refresh
- ⬜📐 BM25 e TF-IDF: a math da busca clássica(Search & Information Retrieval Profundo)
Recomendamos completar os pré-requisitos antes de seguir, mas nada te impede de continuar.
Por dentro do Elasticsearch (e Lucene)
Elasticsearch é frequentemente descrito como "banco de busca", mas isso esconde a verdade: ele é uma camada distribuída em cima do Apache Lucene, um engine de IR escrito por Doug Cutting (mesmo criador do Hadoop) que já existe desde 1999. Toda performance, garantias de durabilidade e modelo de dados vem do Lucene. Elasticsearch adiciona: REST API, sharding, replicação, cluster coordination, ingest pipelines, ILM, mapping, query DSL.
Entender Lucene é entender Elasticsearch (e OpenSearch, e Solr). Este módulo desce ao nível do inverted index, segments imutáveis, refresh vs flush, translog, e como tudo isso se compõe num cluster distribuído.
Referências: Lucene wiki oficial (apache.org), "Elasticsearch: The Definitive Guide" (Clinton Gormley & Zachary Tong, O'Reilly, ainda relevante apesar de antigo), e o blog Elastic.co (especialmente posts de Adrien Grand sobre segments e merging).
Stack: do disco ao cluster
Inverted index: a estrutura fundamental
O coração do Lucene é o inverted index: um mapeamento de term → lista de doc IDs que contêm o termo. Para cada termo, há uma posting list com (doc_id, term_freq, positions). Buscar "postgres mvcc" vira:
Inverted index (esquema simplificado)
====================================
Term Dictionary (ordenado, com FST — Finite State Transducer)
─────────────────────────────────────────────────────────────
"mvcc" → ptr posting list A
"postgres" → ptr posting list B
"vacuum" → ptr posting list C
...
Posting List "postgres" (comprimida com PFOR-Delta / FOR / VInt)
────────────────────────────────────────────────────────────────
[ (doc=3, tf=2, positions=[12, 47]),
(doc=17, tf=1, positions=[8]),
(doc=42, tf=3, positions=[2, 19, 91]),
...
]
Skip list overlay (para acelerar AND queries com docs grandes)
──────────────────────────────────────────────────────────────
Skip a cada N postings (N=128 default), permitindo "advance"
direto para um doc_id próximo sem decodificar tudo.
Query "postgres AND mvcc":
1. Recupera posting list de "postgres" e "mvcc"
2. Caminha as duas listas em paralelo (merge sort)
3. Para cada doc comum, calcula score BM25
4. Mantém heap top-K
5. Retorna ordenado por scoreSegments imutáveis: a escolha arquitetural
Lucene não atualiza segments. Nunca. Cada commit cria um novo segment. Deletes não removem nada — marcam num bitset .liv dentro do segment. Updates são delete + insert (em segment novo). Periodicamente, um merge consolida segments pequenos em um maior, descartando docs deletados.
Imutabilidade é o que habilita: leitura sem locks, mmap eficiente, paralelismo por segment, page cache do OS. O custo é: deletes não liberam espaço até merge, write amplification em merges grandes.
Refresh vs flush vs commit (todo mundo confunde)
| Operação | O que faz | Custo | Default |
|---|---|---|---|
| Refresh | Cria segment in-memory + abre IndexReader. Docs viram buscáveis. | Baixo (não fsync) | A cada 1s (refresh_interval) |
| Flush | fsync de segments + truncate translog + checkpoint. Durabilidade. | Alto (IO) | A cada 512 MB de translog ou 30 min |
| Commit | Termo Lucene equivalente a flush — escreve segments e metadata no disco. | Alto | Implícito no flush |
| Merge | Consolida segments pequenos em maiores, expunge deletes. | Alto (CPU + IO) | Background, tiered merge policy |
Bulk ingest pesado? Aumente para 30s ou desative (-1) durante o load, depois retorne a 1s. Pode acelerar ingestão em 5-10× — porque cada refresh cria segment e dispara merge eventual. Menos refreshes = segments maiores desde o início.
Translog: durabilidade entre commits
Segments só vão para disco em flushes (a cada ~512 MB de translog ou 30 min). Entre flushes, docs já indexados estão num segment in-memory. Se o nó cai, perderíamos tudo desde o último flush. Solução: translog, o write-ahead log do ES.
Durabilidade ajustável: (default, fsync por request, ~ms) ou (fsync a cada 5s, perda potencial de 5s de writes em crash, mas throughput maior). É a mesma equação de fsync que você encara no Postgres.
Por que os segmentos do índice são imutáveis?
Query path: o que acontece num search
Client → POST /logs-*/_search { query: ... }
│
▼
Coordinator node
│
┌──────────┼──────────┐
▼ ▼ ▼
Shard 0 Shard 1 Shard 2 (em paralelo)
│ │ │
│ Lucene index search por shard:
│ - Para cada termo da query, recupera posting list
│ - AND/OR/phrase merge
│ - Calcula BM25 (default similarity)
│ - Mantém top-K em heap
▼ ▼ ▼
(doc_ids + scores top-K de cada shard)
│
▼
Coordinator merge:
- Junta top-K de cada shard
- Re-sort por score
- Top-N global
- Fetch phase: busca _source dos N docs finais
│
▼
Response → client
Latência típica:
- query phase: ~5-30 ms (CPU bound)
- fetch phase: ~5-15 ms (IO bound, _source no disco)
- network: ~1-5 ms intra-cluster- → fase 1
- → fase 1
- → fase 1
- → ids + notas
- → fase 2
- Fora da AWS
- Conceito de arquitetura
As duas fases explicam de uma vez dois comportamentos que parecem arbitrários: por que paginação profunda fica cara e por que aumentar o número de shards nem sempre acelera. A busca sempre toca todos os shards; o que se economiza na primeira fase é leitura de documento, não trabalho de busca.
- 1 · Qualquer nó coordena. Não há nó especial de leitura: quem recebe a requisição vira coordenador daquela busca. É o que permite colocar o cluster inteiro atrás de um balanceador simples.
- 2 · A busca vai a todos os shards. O coordenador não sabe onde estão os melhores resultados, então pergunta a todos. É por isso que shards demais numa consulta custam caro: cada um é uma ida e volta e um pedaço de trabalho.
- 3 · Dentro do shard, segmentos imutáveis. Um segmento nunca é editado. Atualizar um documento marca o antigo como apagado e escreve um novo — é o que permite ler sem trava, e o que exige mesclagem periódica para o número de segmentos não explodir.
- 4 · A primeira fase devolve só identificador e nota. Trazer o documento inteiro de cada shard seria desperdício: a maior parte não entra no topo global. Primeiro se decide quem entra, depois se busca o conteúdo.
- 5 · A segunda fase lê poucos documentos. Só os que sobreviveram à ordenação global. É por isso que pedir a página mil é caro: cada shard precisa devolver mil candidatos para que o coordenador possa ordenar.
- 6 · O log de transação cobre a janela de risco. Entre a escrita e a confirmação em disco, o que garante durabilidade é o log. Atualização visível para busca e dado gravado com segurança são dois eventos distintos — confundi-los é a origem da confusão clássica entre os comandos.
Tiered merge policy: por que merges acontecem
Lucene usa TieredMergePolicy (default) para decidir quando merge segments. A heurística: agrupar segments de tamanho similar em "tiers" e mergear tiers que ultrapassem um limite. Parâmetros chave:
- — tamanho máximo de segment pós-merge (default 5 GB)
- — quantos segments por tier antes de mergear (default 10)
- — segments menores que isso são agrupados (default 2 MB)
Em produção: nunca force em índices ativos. Isso cria 1 mega-segment que tira eficiência de merges futuros e bloqueia até completar. Force-merge só faz sentido em índices read-only (logs antigos, finalizados via ILM).
Elastic License: o drama de 2021
Quando usar Elasticsearch (e quando não)
📋 Busca full-text + agregações + analytics em escala
Inverted index Lucene maduro, BM25 default, hybrid search nativo (8.x+); Agregações (terms, date_histogram, percentiles) extremamente otimizadas; Sharding + replicação out-of-the-box, scaling horizontal real; Ecossistema (Logstash, Kibana, Beats) para logs/observability
Alt: Postgres tsvector / FTS: ok para apps pequenos a médios sem necessidade de cluster. Limite ~1M docs com performance digna.
Alt: Meilisearch / Typesense: developer experience superior, typo tolerance built-in, ideal para "instant search" em e-commerce
Alt: Vespa: superior em large-scale ML ranking, mas curva de aprendizado brutal
Alt: Vector DB puro (Qdrant, Weaviate): se busca é 100% semântica e você não precisa de keyword search
Perguntas frequentes
❓ ES e OpenSearch são compatíveis?
❓ Vale rodar ES self-hosted ou ir em managed?
❓ ES suporta vector search?
❓ Por que meu cluster fica 'yellow'?
Resumo executivo
Elasticsearch = Lucene distribuído. Domine os conceitos do Lucene (inverted index, segments imutáveis, refresh vs flush, merges, translog) e o resto vira aplicação de tuning sobre essa base. Em 2026, ES 8.x e OpenSearch 2.x oferecem hybrid search nativo (BM25 + vector + RRF) — o que torna ES um candidato sério mesmo para aplicações modernas de RAG.
Próximo módulo: comparativo prático entre OpenSearch, Meilisearch e Typesense — quando escolher qual.
Fixando
O que significa dizer que a busca é quase em tempo real?
Qual erro de modelagem mais compromete um índice em produção?
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…