05 - Search (Elasticsearch)
video: https://youtu.be/... no frontmatter da nota.Search (Elasticsearch)
Elasticsearch é um motor de busca distribuído construído sobre o Apache Lucene, projetado para indexar grandes volumes de dados semiestruturados e responder consultas textuais em dezenas de milissegundos. Ele ocupa três papéis centrais em arquiteturas de produção: busca de produto e conteúdo para o usuário final, agregação e exploração de logs (a base do stack ELK) e autocomplete com relevância ajustável. Diferentemente do banco relacional, que responde com precisão por igualdade e faixa, o motor de busca responde com ordenação por relevância, tolerância a erro de grafia, análise morfológica e facets, e é essa família de capacidades que justifica sua existência ao lado do banco principal. O cluster é distribuído por natureza, com shards particionando índices e réplicas sustentando leitura e disponibilidade. Em 2024 e 2025 a categoria também mudou de rumo comercial: após a adoção de licenças restritivas, o Elasticsearch 8.16 reintroduziu uma opção open source sob AGPLv3, enquanto o OpenSearch, fork da AWS sob Apache 2.0, consolidou sua própria linha de evolução.
Conceito
O coração de um motor de busca é o índice invertido: em vez de mapear documento para conteúdo, ele mapeia cada termo para a lista de documentos que o contêm, com posições e frequências. Assim como o índice remissivo de um livro aponta em que páginas cada palavra aparece, o índice invertido permite responder "quais documentos falam de 'postgresql'?" sem varrer a coleção. Antes de entrar no índice, o texto passa pelo pipeline de análise: tokenização quebra o texto em termos, filtros normalizam (minúsculas, remoção de stopwords) e stemmers reduzem variações à raiz ("correndo" e "correu" convergem para "corre"). Campos numéricos e de data recebem estruturas próprias para faixas, e o campo keyword preserva o termo exato para agregação e filtro. O documento indexado é, portanto, uma representação transformada do original, e a qualidade da busca depende primeiro da qualidade dessa análise.
Que problema resolve
O motor de busca resolve o conjunto de problemas que bancos generalistas atendem mal, e cada problema tem nome e sintoma em produção:
- Busca textual com relevância: o usuário digita "fone bluetooth" e espera os resultados mais prováveis primeiro, ordenados por pontuação de relevância e não por data de inserção.
- Tolerância a variação linguística: plural, acento, inflexão e erro de digitação são absorvidos por análise, fuzzy matching e correção sugerida.
- Agregação e facets sobre texto: contagens por categoria, faixa de preço e marca em tempo subsegundo sobre milhões de documentos.
- Exploração de logs: ingestão contínua, retenção por idade e consultas ad hoc combinadas a agregações por janela, com ciclo de vida gerido pelo próprio índice.
- Autocomplete e busca-as-you-type: prefixos e completions servidos em milissegundos, algo inviável com
LIKE '%texto%'no banco relacional.
Sem o search engine, esses requisitos terminam em varreduras caras no banco primário, latência crescente e acoplamento entre a saúde da aplicação e consultas exploratórias.
Funcionamento interno
O Elasticsearch organiza dados em índices, e cada índice é dividido em shards, que são instâncias Lucene completas distribuídas entre nós do cluster. Cada shard é replicado em cópias (primária e réplicas) que servem leitura e sobrevivem à perda do nó primário. A escrita passa primeiro por um translog para durabilidade e por um buffer em memória; periodicamente o buffer é descarregado em segmentos imutáveis, que são mesclados em segundo plano (merge) para controlar o número de segmentos e remover documentos deletados; deleção em Lucene é marcação, e o espaço só volta após o merge. Consultas distribuídas seguem o modelo scatter-gather: o nó coordenador distribui a consulta para um shard de cada grupo (primária ou réplica), cada um responde com seus melhores candidatos, e o coordenador mescla e ordena o resultado final. O índice por shard tem custo de memória e file handles, e shards muito grandes ou numerosos demais são a fonte mais comum de problemas operacionais da categoria.
// Mapeamento e settings de um índice de produtos
PUT produtos
{
"settings": {
"number_of_shards": 3, // particionamento horizontal
"number_of_replicas": 1, // cópias para leitura e HA
"refresh_interval": "1s" // custo de visibilidade x throughput
},
"mappings": {
"properties": {
"nome": { "type": "text", "analyzer": "portuguese" },
"marca": { "type": "keyword" },
"preco": { "type": "scaled_float", "scaling_factor": 100 },
"tags": { "type": "keyword" },
"criado_em": { "type": "date" }
}
}
}
// Consulta com relevância combinada e facets
POST produtos/_search
{
"query": {
"bool": {
"must": [{ "match": { "nome": { "query": "fone bluetooth", "fuzziness": "AUTO" } } }],
"filter": [{ "term": { "marca": "audiox" } },
{ "range": { "preco": { "lte": 500 } } }]
}
},
"aggs": {
"por_marca": { "terms": { "field": "marca" } }
},
"size": 20
}
Relevância e scoring
A pontuação padrão usa BM25, evolução do TF-IDF que pondera frequência do termo no documento, raridade do termo na coleção e normalização pelo tamanho do documento. Em produção, a relevância percebida é mais engenharia do que sorteio: filtros baratos antes de matches textuais, boosts por campo (título pesa mais que descrição), boosts funcionais por popularidade, vendas ou margem, e regras de negócio para itens patrocinados. A avaliação de relevância é um processo contínuo com conjuntos de consultas de referência e métricas como nDCG, corridas A/B e análise de consultas sem clique, que indicam onde a análise de texto ou o ranking falham. Três armadilhas recorrentes merecem atenção: deep pagination com from alto é proibitiva (use search_after com cursor), o filtro e a query têm semânticas de scoring distintas, e boost por script executa por documento, custando caro em coleções grandes.
Autocomplete e busca por prefixo
Autocomplete é o caso de uso que expõe o custo de fazer busca mal feita no banco relacional, e o search engine o resolve com estruturas dedicadas. Para sugestão enquanto o usuário digita, o padrão é indexar um campo auxiliar com analisador de edge ngram, que gera prefixos incrementais de cada termo, e consultar com match sobre esse campo; a alternativa search_as_you_type embute essa estrutura pronta. Para completions de alta velocidade, o tipo completion usa uma estrutura de prefixo (FST) em memória, com respostas em microssegundos, porém sem scoring textual completo. Três detalhes de produção fazem diferença: cache no frontend para teclas digitadas em sequência, limite de resultados por requisição e ordenação por popularidade para as sugestões, já que o usuário espera o item mais provável primeiro, não o alfabético.
// Campo de autocomplete com edge ngram no mapeamento
PUT produtos
{
"settings": {
"analysis": {
"filter": {
"edge_2_15": { "type": "edge_ngram", "min_gram": 2, "max_gram": 15 }
},
"analyzer": {
"auto_complete": {
"tokenizer": "standard",
"filter": ["lowercase", "asciifolding", "edge_2_15"]
}
}
}
},
"mappings": {
"properties": {
"nome_autocomplete": {
"type": "text",
"analyzer": "auto_complete",
"search_analyzer": "standard" // consulta sem ngram
}
}
}
}
Cluster, shards e capacidade
O desenho de capacidade do cluster é decisão de arquitetura com consequências duradouras. Shards têm tamanho saudável na faixa das dezenas de gigabytes, e o número de shards por nó deve manter o heap sob controle; a regra prática antiga de shards de até 50 GB ainda orienta, mas o que importa é o custo de rebalanceamento e de merges. Réplicas aumentam throughput de leitura e protegem contra falha de nó, ao custo de armazenamento dobrado; em clusters pequenos, number_of_replicas: 1 é o piso usual, e escalar réplicas para 2 ou 3 só sob demanda de leitura. O gerenciamento de ciclo de vida (ILM no Elasticsearch, políticas equivalentes no OpenSearch) move índices entre fases: hot para ingestão e busca intensa, warm para leitura ocasional, cold para arquivo consultável e delete para expiração. Log management sem ILM é o caminho mais rápido para cluster saturado, e a regra é dimensionar ingestão diária, retenção por fase e tamanho de shard juntos, nunca em separado.
Elasticsearch versus OpenSearch
O histórico comercial da categoria importa para decisão de longo prazo. A Elastic adotou licenças restritivas em 2021 (SSPL e Elastic License), o que motivou a AWS a criar o OpenSearch como fork do Elasticsearch 7.10.2 sob Apache 2.0. Em 2024, com o Elasticsearch 8.16, a Elastic reintroduziu uma opção open source sob AGPLv3, somada às licenças anteriores, reabrindo o caminho de distribuições comunitárias; o OpenSearch seguiu evolução independente sob a Linux Foundation, com recursos próprios como o OpenSearch Flow Framework e melhorias de escala. Na prática, os dois produtos divergem em versões de API e recursos, e a compatibilidade de clientes deixou de ser total. A escolha hoje é de governança e plataforma: OpenSearch para quem prioriza licença Apache e ofertas gerenciadas AWS, Elasticsearch para quem quer o ecossistema Elastic (observabilidade, segurança unificada) e aceita AGPLv3 ou licença comercial.
| Critério | Elasticsearch | OpenSearch |
|---|---|---|
| Licença atual | AGPLv3 (opcional, desde 8.16), SSPL, Elastic License | Apache 2.0 |
| Governança | Elastic NV | Linux Foundation |
| Linha de origem | Lucene, versões 7/8/9 | Fork do 7.10.2, linha própria |
| Gestão de índices | ILM | ISM (Index State Management) |
| Segurança básica | Incluída na distribuição básica | Incluída (security plugin integrado) |
| Ofertas gerenciadas | Elastic Cloud, Elastic Cloud on EKS | Amazon OpenSearch Service |
| Ecossistema | Stack unificado de observabilidade e segurança | Comunidade AWS e integrações OpenSearch |
Erros comuns
- Shards pequenos e numerosos (milhares por nó), inflando o heap com metadados e degradando merges e recuperação; consolide índices por janela de tempo.
- Consultas com wildcard no início do termo ou
script_scorepor documento, custos lineares que derrubam o cluster no primeiro pico de tráfego. - Deep pagination com
from: 10000, memória e CPU por documento pulado; pagine comsearch_aftere limite o usuário a poucas páginas. - Campos text usados como filtro exato ou keyword indexado com texto longo, invertendo o propósito dos tipos e inflando o índice.
- Falta de ILM/ISM em índices de logs, com retenção infinita e cluster de busca servindo de arquivo morto.
- Refresh interval padrão assumido como real time, quando a visibilidade de novos documentos tem atraso de cerca de um segundo por desenho.
- Mapeamento dinâmico sem controle, onde um campo novo de alta cardinalidade por documento explode o índice e os termos no dicionário.
Boas práticas
- Modele o mapeamento explicitamente (templates e componentes reutilizáveis) e trate mudança de mapeamento como reindexação planejada.
- Dimensione shards por janela de ingestão diária e retenção, revisitando o plano quando o volume diário dobrar.
- Separe papéis no cluster (master dedicado, data hot, data warm) quando a escala justificar, e mantenha o heap abaixo de 75 por cento com folga para merges.
- Versione consultas críticas como código, com testes de relevância sobre consultas de referência executadas em pipeline.
- Use aliases para abstrair o índice da aplicação, permitindo reindexação e rotação sem mudança no código cliente.
- Monitore latência de query por shard, taxa de rejeição da fila de busca, heap e merge pressure, alertando antes do degrau de saturação.
- Para logs, defina fases de ciclo de vida no primeiro dia de ingestão, com base no volume diário e no custo de armazenamento aceito.