06 ‐ Index Settings ‐ Entendendo Mapping, Analyzers e Aliases - BktechBrazil/elastic-eck GitHub Wiki

6.1 - Mapping

O mapping define como os campos de um documento são armazenados e indexados no Elasticsearch. É equivalente ao schema de uma tabela em um banco de dados relacional — mas com diferenças importantes: no Elasticsearch, o mapping pode ser definido explicitamente antes da indexação ou inferido automaticamente pelo próprio Elasticsearch ao receber o primeiro documento.

Definir o mapping explicitamente é uma boa prática em ambientes de produção. Sem isso, o Elasticsearch pode inferir tipos incorretos — por exemplo, tratar um campo de CEP como long em vez de keyword — e o tipo de um campo não pode ser alterado depois que ele foi mapeado. Para mudar o tipo de um campo, é necessário reindexar os dados (veja Alterando um mapping existente).

Tipos de campo (Field Data Types)

Os tipos mais usados no dia a dia:

Tipo Uso típico Exemplo de valor
text Texto livre — será analisado (tokenizado) para full-text search "Produto de alta qualidade"
keyword Valores exatos — usado para filtros, ordenação e agregações (ex: status, UF, ID) "SP", "ativo", "CA-2024-001"
integer Números inteiros de 32 bits. Faixa: -2.147.483.648 a 2.147.483.647 42, -10, 2024 (idade, ano, quantidade)
long Números inteiros de 64 bits. Faixa muito maior que integer 1704067200000 (timestamps Unix em milissegundos, IDs grandes)
double Números decimais de 64 bits (alta precisão) 19.99, -0.5, 3.14159 (preços, coordenadas, taxas)
float Números decimais de 32 bits (menor precisão, ocupa menos espaço) 1.5, 98.6 (métricas de sistema, onde precisão absoluta não é crítica)
byte Inteiro de 8 bits. Faixa: -128 a 127 42 (flags, níveis de prioridade com poucos valores)
short Inteiro de 16 bits. Faixa: -32.768 a 32.767 1000 (portas de rede, códigos pequenos)
date Datas e timestamps "2024-05-01", "2024-05-01T10:30:00Z"
boolean Verdadeiro ou falso true, false
geo_point Coordenadas geográficas (latitude/longitude) "-23.5505, -46.6333"
nested Arrays de objetos com relacionamento interno preservado Lista de itens de um pedido, cada um com nome e preço
object Objeto JSON aninhado { "rua": "Av. Brasil", "numero": 100 }

Dica prática: na dúvida entre integer e long, prefira long — o custo de armazenamento é pequeno e evita problemas de overflow em campos que podem crescer. Entre float e double, use double para valores financeiros onde precisão importa, e float apenas em métricas de monitoramento onde uma pequena perda de precisão é aceitável.

Exemplo de mapping explícito

PUT /clientes
{
  "mappings": {
    "properties": {
      "nome":  { "type": "text" },
      "idade": { "type": "integer" },
      "email": { "type": "keyword" }
    }
  }
}

Multi-fields

É possível indexar o mesmo campo com dois tipos diferentes usando fields. Isso é útil quando você precisa tanto de full-text search quanto de filtros/ordenação no mesmo campo:

PUT /clientes
{
  "mappings": {
    "properties": {
      "nome": {
        "type": "text",
        "fields": {
          "raw": {
            "type": "keyword"
          }
        }
      }
    }
  }
}

Nesse exemplo, nome é indexado como text (para buscas por texto) e nome.raw como keyword (para ordenação e agregações exatas).

A escolha entre text e keyword decide quais consultas funcionam em cada campo:

text keyword
É analisado? Sim — quebrado em tokens Não — o valor é indexado exatamente como chegou
Consultas indicadas match e demais consultas full-text term, terms, range, agregações e ordenação
Exemplos descrição de produto, comentário, mensagem de log categoria, status, e-mail, SKU, UF

Uma consulta term em um campo text normalmente não encontra o documento: o term procura o valor exato, mas o que está no índice são os tokens gerados pelo analyzer. É por isso que o mapeamento dinâmico cria nome como text e nome.keyword como keyword: um para buscar, outro para filtrar e agregar.

Meta Fields

Todo documento no Elasticsearch possui, além dos campos definidos pelo usuário, um conjunto de campos especiais chamados meta fields. Eles são gerados e mantidos pelo próprio Elasticsearch e fornecem informações sobre o documento em si.

Os meta fields mais relevantes no dia a dia:

Meta field O que contém
_id Identificador único do documento dentro do índice. Pode ser definido na indexação ou gerado automaticamente pelo Elasticsearch.
_index Nome do índice ao qual o documento pertence.
_source O conteúdo JSON original do documento, exatamente como foi indexado. É o que aparece nos resultados de busca. Pode ser desabilitado para economizar espaço, mas aí o documento não pode ser recuperado na íntegra.
_routing Controla em qual shard o documento é armazenado. Por padrão, é calculado com base no _id. Pode ser customizado para garantir que documentos relacionados fiquem no mesmo shard.
_meta Campo livre para armazenar metadados sobre o mapping — como versão, descrição ou informações de governança. Não é usado pelo Elasticsearch na busca.
_seq_no Número de sequência do documento — incrementado a cada operação de escrita. Usado para controle de concorrência otimista.
_primary_term Junto com _seq_no, identifica unicamente uma versão do documento. Útil para operações condicionais de update e delete.

Exemplo: recuperar um documento e observar seus meta fields:

GET /clientes/_doc/1

A resposta inclui _id, _index, _seq_no, _primary_term e o _source com os dados do documento.

Exemplo: desabilitar o _source (use com cautela — o documento não poderá ser recuperado na íntegra):

PUT /metricas
{
  "mappings": {
    "_source": {
      "enabled": false
    }
  }
}

Parâmetros de mapeamento

Além do type, cada campo pode receber parâmetros adicionais que controlam como ele é indexado e armazenado:

Parâmetro Aplica a O que faz
index Todos Se false, o campo é armazenado no _source mas não é indexado — não pode ser usado em buscas ou filtros. Útil para campos grandes que só precisam ser recuperados, não filtrados.
store Todos Se true, o campo é armazenado separadamente do _source. Raramente necessário — o _source já permite recuperar qualquer campo.
doc_values keyword, numéricos, datas Estrutura de dados em disco usada para ordenação e agregações. Habilitado por padrão nesses tipos. Desabilitar economiza disco mas remove a capacidade de ordenar e agregar pelo campo.
norms text Informações de comprimento do campo usadas no cálculo de score. Desabilitar ("norms": false) economiza memória em campos que são usados apenas como filtros, não para busca por relevância.
copy_to Todos Copia o valor do campo para outro campo virtual, permitindo fazer buscas em múltiplos campos como se fossem um só.
fields Todos Define sub-campos com tipos ou analyzers diferentes — o multi-field descrito acima.
null_value keyword, numéricos Define um valor substituto para indexar quando o campo for null. Sem isso, campos nulos não são indexados e não aparecem em filtros.
eager_global_ordinals keyword Pré-carrega os ordinals globais na memória ao reabrir o índice. Melhora o desempenho de agregações terms em campos de alta cardinalidade que são consultados com muita frequência.

Exemplo combinando parâmetros:

PUT /pedidos
{
  "mappings": {
    "properties": {
      "descricao": {
        "type": "text",
        "norms": false,
        "copy_to": "busca_geral"
      },
      "status": {
        "type": "keyword",
        "null_value": "desconhecido"
      },
      "valor_bruto": {
        "type": "double",
        "index": false
      },
      "busca_geral": {
        "type": "text"
      }
    }
  }
}

Nesse exemplo: descricao não usa norms (economiza memória); status indexa o valor "desconhecido" quando o campo vier nulo; valor_bruto é armazenado mas não indexado (só para recuperação); e o campo virtual busca_geral recebe cópias de outros campos para buscas combinadas.

Mapeamento dinâmico (Dynamic Mapping)

Quando nenhum mapping é definido, o Elasticsearch infere os tipos automaticamente ao indexar o primeiro documento:

POST /clientes/_doc/1
{
  "nome":  "Maria",
  "idade": 28
}

O Elasticsearch inferirá nome como text (com subcampo keyword automático) e idade como long.

O comportamento do mapeamento dinâmico é controlado pelo parâmetro dynamic, com quatro valores possíveis:

Valor Comportamento
true (padrão) Novos campos são detectados e adicionados ao mapping automaticamente
runtime Novos campos são adicionados ao mapping como runtime fields: não são indexados e são lidos do _source no momento da consulta
false Novos campos são aceitos e armazenados no _source, mas não são indexados — não podem ser pesquisados
strict Documentos com campos não mapeados são rejeitados com erro

Exemplo com dynamic: strict:

PUT /clientes
{
  "mappings": {
    "dynamic": "strict",
    "properties": {
      "codigo": { "type": "integer" },
      "nome":   { "type": "text" }
    }
  }
}

Com essa configuração, qualquer documento que contenha campos além de codigo e nome será rejeitado pelo Elasticsearch.

Dynamic Templates

Permitem definir regras de mapeamento para campos novos com base em padrões de nome ou tipo. São aplicadas somente quando o mapeamento dinâmico está ativo e um campo novo é encontrado.

Exemplo — todos os campos do tipo string que comecem com logradouro devem ser indexados como keyword:

PUT /clientes
{
  "mappings": {
    "dynamic_templates": [
      {
        "logradouros_as_keywords": {
          "match_mapping_type": "string",
          "match_pattern": "regex",
          "match": "^logradouro.*",
          "mapping": {
            "type": "keyword"
          }
        }
      }
    ]
  }
}

Outro exemplo comum — tratar todos os campos de string como keyword por padrão:

PUT /clientes
{
  "mappings": {
    "dynamic_templates": [
      {
        "strings_as_keywords": {
          "match_mapping_type": "string",
          "mapping": {
            "type": "keyword"
          }
        }
      }
    ]
  }
}

Alterando um mapping existente

Depois que um campo foi mapeado, na maioria dos casos o mapping dele não pode mais ser alterado. A API de update de mapping (PUT /<indice>/_mapping) permite apenas:

  • adicionar campos novos ao índice;
  • adicionar multi-fields a um campo existente — os documentos indexados antes da mudança só passam a ter valor no novo sub-campo depois de atualizados ou reindexados (por exemplo, com _update_by_query);
  • alterar alguns parâmetros que a documentação indica como atualizáveis, como o ignore_above.

Mudar o tipo de um campo ou o analyzer usado na indexação exige criar um índice novo com o mapping correto e copiar os dados com a API _reindex. Para que as aplicações não percebam a troca, a leitura e a escrita devem passar por um alias (veja a seção 6.3 e o Laboratório 5).


6.2 - Analysis: Analyzers, Tokenizers e Filters

A análise de texto é o processo que o Elasticsearch aplica sobre campos do tipo text no momento da indexação e da busca. Ela transforma o texto bruto em uma lista de termos (tokens) que serão armazenados no índice invertido.

Analysis Elastic

Componentes de um Analyzer

Um analyzer é composto por três partes, aplicadas em sequência:

Analyzers Elastic

  1. Character Filters: processam o texto bruto antes da tokenização. Podem remover ou substituir caracteres — por exemplo, remover tags HTML ou converter & em and.

  2. Tokenizer: divide o texto em tokens. O tokenizer standard divide por espaços e pontuação, removendo a pontuação dos tokens resultantes. Cada analyzer tem exatamente um tokenizer.

  3. Token Filters: processam cada token gerado pelo tokenizer. Podem converter para minúsculas (lowercase), remover stopwords (stop), aplicar stemming, adicionar sinônimos, entre outros.

Análise na indexação e na busca

A análise acontece em dois momentos:

  • Na indexação: o valor do campo text é analisado e os tokens resultantes vão para o índice invertido.
  • Na busca: o texto digitado em uma consulta full-text (como match) passa pelo mesmo processo antes de ser comparado com os tokens do índice.

Na maioria dos casos, o mesmo analyzer deve ser usado nos dois momentos: assim o texto do documento e o texto da busca viram tokens no mesmo formato e se encontram. Por padrão, é isso que o Elasticsearch faz. Quando há um motivo para usar analyzers diferentes, o campo recebe um search_analyzer — o caso clássico é o autocomplete com edge_ngram, visto no Laboratório 3.

Analyzers built-in

O Elasticsearch vem com vários analyzers prontos para uso:

Analyzer O que faz
standard Tokeniza por espaços e pontuação, converte para minúsculas. É o padrão para campos text.
simple Tokeniza por qualquer caractere não-letra, converte para minúsculas
whitespace Tokeniza apenas por espaços — preserva pontuação
keyword Não tokeniza — trata o campo inteiro como um único token
portuguese Analyzer com stemming e stopwords em português
english Analyzer com stemming e stopwords em inglês

Analyzers de idioma e stemming

Os analyzers de idioma, como o portuguese, combinam o tokenizer standard com três etapas de filtro:

  1. lowercase: converte para minúsculas;
  2. stop: remove stopwords, palavras muito frequentes que pouco ajudam na busca (o, os, de, para, estavam...);
  3. stemmer: reduz cada palavra ao seu radical, para que variações da mesma palavra se encontrem na busca.

O radical não precisa ser uma palavra real: meninos, menino e menina viram todos menin. O que importa é que todas as variações cheguem à mesma forma.

Para o português, o token filter stemmer oferece mais de um algoritmo, com intensidades diferentes:

language Comportamento Exemplo: correndo, correu, corrida, correr
light_portuguese Leve: trata principalmente plural e gênero. É o recomendado pela documentação e o usado pelo analyzer portuguese. corrend, correu, corrid, correr (quatro radicais)
portuguese Snowball: mais agressivo, também reduz formas verbais corr, corr, corr, corr (um radical)
minimal_portuguese e portuguese_rslp Outras variantes disponíveis –

Um stemmer mais agressivo aproxima mais variações e aumenta o número de documentos encontrados, mas também pode juntar palavras de sentidos diferentes no mesmo radical. A escolha depende do tipo de texto e do que o usuário espera encontrar. No Laboratório 2 você compara os dois na prática.

Analyzers customizados

Quando nenhum analyzer pronto atende, você monta o seu combinando as três etapas nos settings do índice. Exemplo de analyzer para textos em português que chegam com HTML:

"settings": {
  "analysis": {
    "analyzer": {
      "pt_custom": {
        "type": "custom",
        "char_filter": ["html_strip"],
        "tokenizer": "standard",
        "filter": ["lowercase", "portuguese_stop", "portuguese_stemmer"]
      }
    },
    "filter": {
      "portuguese_stop":    { "type": "stop", "stopwords": "_portuguese_" },
      "portuguese_stemmer": { "type": "stemmer", "language": "light_portuguese" }
    }
  }
}
  • html_strip remove as tags HTML antes da tokenização;
  • portuguese_stop usa a lista de stopwords em português que já vem com o Elasticsearch (_portuguese_);
  • portuguese_stemmer aplica o stemmer light_portuguese.

O analyzer só existe dentro do índice em que foi definido. Para usá-lo em um campo, basta informar "analyzer": "pt_custom" no mapping. O Laboratório 2 cria esse índice.

Testando um analyzer

A API _analyze permite testar como um texto será processado por um analyzer, sem precisar indexar nada:

GET /_analyze
{
  "analyzer": "standard",
  "text": "Texto para análise!"
}

Testando apenas o tokenizer:

POST /_analyze
{
  "tokenizer": "standard",
  "text": "Quick brown fox."
}

Testando um analyzer definido inline — útil para testar sem precisar de um índice criado:

POST /_analyze
{
  "tokenizer": {
    "type": "edge_ngram",
    "min_gram": 3,
    "max_gram": 10,
    "token_chars": ["letter", "digit"]
  },
  "text": "GOL Linhas Aereas"
}

Para testar um analyzer customizado de um índice específico (como my_analyzer_ngram_3), use POST /nome-do-indice/_analyze — mas o índice precisa existir e ter o analyzer configurado. Esse exemplo prático está no Laboratório 3, onde o índice fornecedor é criado com esse analyzer.

A _analyze é a principal ferramenta para investigar uma busca que não encontra o que deveria: ela mostra exatamente quais tokens foram gerados para o documento e para o texto da consulta.

Normalizers

Normalizers são semelhantes aos analyzers, mas aplicados a campos do tipo keyword. A diferença principal: um normalizer sempre produz exatamente um token por entrada — ele transforma o valor sem dividi-lo. São usados para normalizar valores antes de armazená-los, como converter para minúsculas.

PUT /meu_index
{
  "settings": {
    "analysis": {
      "normalizer": {
        "meu_normalizer": {
          "type": "custom",
          "char_filter": [],
          "filter": ["lowercase"]
        }
      }
    }
  },
  "mappings": {
    "properties": {
      "meu_campo_keyword": {
        "type": "keyword",
        "normalizer": "meu_normalizer"
      }
    }
  }
}

Com esse normalizer, o valor "BOA TARDE" será armazenado e indexado como "boa tarde", tornando as buscas case-insensitive nesse campo.

POST /meu_index/_doc/
{
  "meu_campo_keyword": "BOA TARDE"
}
GET /meu_index/_search
{
  "size": 0,
  "aggs": {
    "meus_valores": {
      "terms": {
        "field": "meu_campo_keyword"
      }
    }
  }
}

6.3 - Aliases

Um alias é um nome alternativo que aponta para um ou mais índices. Para quem consome os dados (aplicações, dashboards, queries), o alias funciona exatamente como um índice — mas por trás pode estar apontando para um, vários índices ou até para um subconjunto filtrado de um índice.

Casos de uso comuns:

  • Abstração de versão: renomear ou reindexar dados sem alterar o nome que as aplicações usam
  • Alias multi-índice: fazer uma query em vários índices ao mesmo tempo por um único nome
  • Alias filtrado: expor apenas um subconjunto de documentos de um índice (ex: apenas registros de um cliente específico)
  • Write index: controlar em qual índice as escritas são direcionadas (usado pelo ILM)

Criando um alias simples

POST /_aliases
{
  "actions": [
    {
      "add": {
        "index": "clientes",
        "alias": "clientes_atuais"
      }
    }
  ]
}

A partir desse momento, clientes_atuais pode ser usado no lugar de clientes em qualquer operação de leitura.

Alias com filtro

POST /_aliases
{
  "actions": [
    {
      "add": {
        "index": "clientes",
        "alias": "clientes_adultos",
        "filter": {
          "range": {
            "idade": { "gte": 18 }
          }
        }
      }
    }
  ]
}

O alias clientes_adultos expõe apenas documentos de clientes com 18 anos ou mais. O filtro é aplicado automaticamente — quem usa o alias não precisa saber que ele existe.

Removendo um alias

POST /_aliases
{
  "actions": [
    {
      "remove": {
        "index": "clientes",
        "alias": "clientes_atuais"
      }
    }
  ]
}

Listando aliases

GET /_cat/aliases?v

Escrita por um alias

Quando o alias aponta para um único índice, as escritas feitas pelo alias vão para esse índice. Quando aponta para vários índices, uma escrita só é aceita se um deles estiver marcado com "is_write_index": true; sem isso, o Elasticsearch rejeita a escrita.

Troca atômica: reindex sem downtime

Todas as ações de um mesmo POST /_aliases são executadas em uma única operação atômica. Isso permite mover um alias de um índice para outro sem que exista um instante em que ele não aponte para nenhum:

POST /_aliases
{
  "actions": [
    { "remove": { "index": "catalogo",    "alias": "produtos" } },
    { "add":    { "index": "catalogo-v2", "alias": "produtos" } }
  ]
}

Esse é o procedimento padrão para mudar o tipo ou o analyzer de um campo com a aplicação no ar:

  1. a aplicação lê e escreve sempre pelo alias (produtos), nunca pelo nome físico do índice;
  2. cria-se o índice novo (catalogo-v2) com o mapping corrigido;
  3. a API _reindex copia os documentos do índice antigo para o novo;
  4. o alias é trocado de índice com a operação atômica acima.

A API _reindex copia apenas os documentos: settings, mappings, número de shards e réplicas do destino precisam ser definidos antes, na criação do índice novo.

Boa prática: aponte aplicações, data views e integrações para um alias, e não para o nome físico do índice. Isso deixa você livre para reindexar e evoluir o mapping sem parar o serviço.


6.4 - Preparação dos laboratórios

Os laboratórios deste capítulo usam o cluster lab-es com um node e o Kibana lab-kb, e todos os comandos são executados no Console do Kibana. Mappings, analyzers e aliases são configurações dos índices, feitas pela API do Elasticsearch: o operator ECK não participa e nenhum manifesto é alterado.

Passo 1 — Conferir o cluster

No terminal da VM:

kubectl -n elastic get elasticsearch,kibana

Resultado esperado: lab-es com HEALTH green, NODES 1 e PHASE Ready, e lab-kb com HEALTH green.

Passo 2 — Acessar o Kibana

PASSWORD=$(kubectl -n elastic get secret lab-es-es-elastic-user \
  -o go-template='{{.data.elastic | base64decode}}')
echo "Senha do elastic: $PASSWORD"

pkill -f "port-forward.*5601" || true
nohup kubectl -n elastic port-forward --address 0.0.0.0 service/lab-kb-kb-http 5601 > /tmp/pf-kibana.log 2>&1 &
sleep 2
cat /tmp/pf-kibana.log

Resultado esperado: a senha do usuário elastic e a mensagem Forwarding from 0.0.0.0:5601 -> 5601.

O túnel fica ligado ao pod do Kibana que estava rodando quando ele foi aberto. Se esse pod for reiniciado ou recriado, o túnel cai e o Kibana deixa de responder no navegador: execute este bloco novamente.

No navegador, abra https://<IP_DA_VM>:5601, faça login com o usuário elastic e a senha exibida e abra o Console (Dev Tools no menu de navegação ou Console na busca global).

Réplicas nos laboratórios: como o lab-es tem um único node, todos os índices criados neste capítulo usam "number_of_replicas": 0. Com réplica, a cópia não teria outro node onde ser alocada e o índice ficaria yellow.


6.5 - Laboratório 1: criando um índice com mapping explícito e dynamic templates

Neste laboratório, vamos primeiro ler o mapping de um índice que já existe no cluster e depois criar um índice clientes com um mapping explícito para campos conhecidos e um dynamic template para tratar automaticamente campos de endereço como keyword.

Etapa 1 — Ler o mapping de um índice existente

O índice kibana_sample_data_ecommerce, dos dados de exemplo do Kibana, é criado com um mapping explícito definido pelo próprio Kibana. Veja o mapping completo:

GET /kibana_sample_data_ecommerce/_mapping

E apenas o campo category:

GET /kibana_sample_data_ecommerce/_mapping/field/category

Repare nas escolhas feitas para cada tipo de dado:

  • category é text com o multi-field category.keyword: dá para buscar por palavra (match em category) e agregar por categoria (terms em category.keyword);
  • currency, customer_id e day_of_week são apenas keyword: são valores exatos, usados só em filtros e agregações;
  • order_date é date e taxful_total_price é half_float;
  • geoip.location é geo_point, o que permite usar o campo em mapas.

Etapa 2 — Criar o índice com mapping

PUT /clientes
{
  "settings": {
    "number_of_shards": 1,
    "number_of_replicas": 0
  },
  "mappings": {
    "dynamic_templates": [
      {
        "logradouros_as_keywords": {
          "match_mapping_type": "string",
          "match_pattern": "regex",
          "match": "^logradouro.*",
          "mapping": {
            "type": "keyword"
          }
        }
      }
    ],
    "properties": {
      "codigo": {
        "type": "integer"
      },
      "nome": {
        "type": "text",
        "fields": {
          "keyword": { "type": "keyword" }
        }
      },
      "sexo":            { "type": "keyword" },
      "estado_civil":    { "type": "keyword" },
      "data_nascimento": { "type": "date" },
      "caracteristicas": { "type": "text" }
    }
  }
}

Explicação dos campos:

  • codigo: número inteiro
  • nome: texto livre com subcampo keyword para ordenação e agregações
  • sexo e estado_civil: valores exatos, sem análise de texto
  • data_nascimento: campo de data
  • caracteristicas: texto livre para full-text search
  • dynamic_templates: qualquer campo novo que comece com logradouro será automaticamente indexado como keyword

Etapa 3 — Indexar um documento de exemplo

POST /clientes/_doc/1
{
  "codigo": 123,
  "nome": "João Silva",
  "sexo": "masculino",
  "estado_civil": "casado",
  "data_nascimento": "1980-05-15",
  "caracteristicas": "Amigável, extrovertido, atencioso",
  "logradouro_principal": "Rua das Flores",
  "logradouro_secundario": "Avenida das Arvores"
}

Os campos logradouro_principal e logradouro_secundario não estão definidos no mapping — serão detectados pelo dynamic template e indexados como keyword automaticamente.

Etapa 4 — Validar o mapping aplicado

GET /clientes/_mapping

Confirme que logradouro_principal e logradouro_secundario aparecem com o tipo keyword no mapping retornado.

Etapa 5 — Testando os valores de dynamic

Testando dynamic: false — campos novos são armazenados mas não indexados:

PUT /clientes_strict
{
  "settings": {
    "number_of_shards": 1,
    "number_of_replicas": 0
  },
  "mappings": {
    "dynamic": "false",
    "properties": {
      "codigo": { "type": "integer" },
      "nome":   { "type": "text" }
    }
  }
}

Indexe um documento com um campo extra:

POST /clientes_strict/_doc/1?refresh=true
{
  "codigo": 1,
  "nome": "Teste",
  "campo_novo": "valor ignorado"
}

Busque pelo campo_novo:

GET /clientes_strict/_search
{
  "query": {
    "match": { "campo_novo": "valor" }
  }
}

Nenhum resultado é retornado, pois o campo não foi indexado. Mesmo assim, o valor continua guardado no documento — confira no _source:

GET /clientes_strict/_doc/1

Testando dynamic: strict — documentos com campos não mapeados são rejeitados:

PUT /clientes_rigido
{
  "settings": {
    "number_of_shards": 1,
    "number_of_replicas": 0
  },
  "mappings": {
    "dynamic": "strict",
    "properties": {
      "codigo": { "type": "integer" },
      "nome":   { "type": "text" }
    }
  }
}
POST /clientes_rigido/_doc/1
{
  "codigo": 1,
  "nome": "Teste",
  "campo_nao_mapeado": "isso vai falhar"
}

O Elasticsearch retornará um erro de mapping (strict_dynamic_mapping_exception) informando que campo_nao_mapeado não está definido no mapping do índice.


6.6 - Laboratório 2: analyzers de idioma e analyzer customizado

Neste laboratório você vai ver os tokens gerados por analyzers diferentes, comparar os dois principais stemmers de português e criar um índice com um analyzer customizado.

Passo 1 — Comparar o standard e o portuguese

POST /_analyze
{
  "analyzer": "standard",
  "text": "O Menino Correu Rápido!"
}

O standard apenas separa as palavras, remove a pontuação e converte para minúsculas: o, menino, correu, rápido.

POST /_analyze
{
  "analyzer": "portuguese",
  "text": "Os meninos estavam correndo"
}

O portuguese remove as stopwords (os, estavam) e aplica o stemmer light_portuguese: sobram apenas menin e corrend.

Passo 2 — Comparar os stemmers light_portuguese e portuguese

A _analyze aceita um analyzer montado na própria requisição, com tokenizer e filtros. Use-a para aplicar cada stemmer às mesmas quatro palavras:

POST /_analyze
{
  "tokenizer": "standard",
  "filter": [
    "lowercase",
    { "type": "stemmer", "language": "light_portuguese" }
  ],
  "text": "correndo correu corrida correr"
}
POST /_analyze
{
  "tokenizer": "standard",
  "filter": [
    "lowercase",
    { "type": "stemmer", "language": "portuguese" }
  ],
  "text": "correndo correu corrida correr"
}

O light_portuguese gera quatro radicais diferentes (corrend, correu, corrid, correr): ele trata plural e gênero, mas não conjugações verbais. O portuguese (Snowball) reduz as quatro palavras a corr.

Passo 3 — Criar o índice catalogo com analyzer customizado

O campo nome usa o analyzer pt_custom, apresentado na seção 6.2, e cada campo recebe o tipo adequado: sku é keyword, preco é double, criado_em é date e ativo é boolean.

PUT /catalogo
{
  "settings": {
    "number_of_shards": 1,
    "number_of_replicas": 0,
    "analysis": {
      "analyzer": {
        "pt_custom": {
          "type": "custom",
          "char_filter": ["html_strip"],
          "tokenizer": "standard",
          "filter": ["lowercase", "portuguese_stop", "portuguese_stemmer"]
        }
      },
      "filter": {
        "portuguese_stop":    { "type": "stop", "stopwords": "_portuguese_" },
        "portuguese_stemmer": { "type": "stemmer", "language": "light_portuguese" }
      }
    }
  },
  "mappings": {
    "properties": {
      "nome": {
        "type": "text",
        "analyzer": "pt_custom",
        "fields": { "keyword": { "type": "keyword" } }
      },
      "sku":       { "type": "keyword" },
      "preco":     { "type": "double" },
      "criado_em": { "type": "date" },
      "ativo":     { "type": "boolean" }
    }
  }
}

Teste o analyzer do índice com um texto que contém HTML:

POST /catalogo/_analyze
{
  "analyzer": "pt_custom",
  "text": "<b>Tênis</b> de corrida masculino"
}

As tags <b> e </b> são removidas pelo html_strip, de é removida como stopword e os tokens gerados são tenil, corrid e masculin. O light_portuguese também remove os acentos, e o radical tenil parece estranho, mas não é um problema: o mesmo radical é gerado na busca, inclusive quando o usuário digita tenis sem acento.

Passo 4 — Indexar um produto e buscar

POST /catalogo/_doc?refresh=true
{
  "nome": "Tênis de corrida masculino",
  "sku": "TEN-001",
  "preco": 299.9,
  "ativo": true,
  "criado_em": "2026-01-10"
}

Busque por plural e gênero diferentes dos que estão no documento:

GET /catalogo/_search
{
  "query": {
    "match": { "nome": "corridas masculinas" }
  }
}

O produto é encontrado: corridas vira corrid e masculinas vira masculin, os mesmos radicais do documento.

Agora busque pelo verbo:

GET /catalogo/_search
{
  "query": {
    "match": { "nome": "correr" }
  }
}

Nenhum resultado: com o light_portuguese, correr continua correr e não encontra corrid. No Laboratório 5 você troca o stemmer desse campo sem parar a aplicação.

Passo 5 — term em keyword e em text

A busca exata funciona no campo keyword:

GET /catalogo/_search
{
  "query": {
    "term": { "sku": "TEN-001" }
  }
}

No campo text, o term com o valor original não encontra nada, porque no índice só existem os tokens analisados (tenil, corrid, masculin):

GET /catalogo/_search
{
  "query": {
    "term": { "nome": "Tênis de corrida masculino" }
  }
}

Para buscar o valor exato do nome, use o multi-field nome.keyword:

GET /catalogo/_search
{
  "query": {
    "term": { "nome.keyword": "Tênis de corrida masculino" }
  }
}

6.7 - Laboratório 3: customizando analyzers com edge_ngram

O edge_ngram é um tokenizer que gera tokens progressivos a partir do início do texto. Por exemplo, a palavra LINHAS com min_gram: 3 e max_gram: 5 gera os tokens LIN, LINH e LINHA. Isso é especialmente útil para implementar busca progressiva enquanto o usuário digita — semelhante ao autocomplete.

Passo 1 — Criar o índice com analyzer customizado

PUT /fornecedor
{
  "settings": {
    "number_of_shards": 1,
    "number_of_replicas": 0,
    "analysis": {
      "analyzer": {
        "my_analyzer_ngram_3": {
          "tokenizer": "split_3_ngram",
          "filter": ["lowercase"]
        }
      },
      "tokenizer": {
        "split_3_ngram": {
          "type": "edge_ngram",
          "min_gram": 3,
          "max_gram": 15,
          "token_chars": ["letter", "digit", "whitespace"]
        }
      }
    }
  },
  "mappings": {
    "properties": {
      "codigo":          { "type": "integer" },
      "nome_fornecedor": {
        "type": "text",
        "fields": { "keyword": { "type": "keyword" } }
      },
      "nome_fornecedor_engram": {
        "type": "text",
        "analyzer": "my_analyzer_ngram_3",
        "search_analyzer": "standard",
        "fields": { "keyword": { "type": "keyword" } }
      },
      "cnpj":     { "type": "keyword" },
      "endereco": { "type": "keyword" },
      "bairro":   { "type": "keyword" },
      "cidade":   { "type": "keyword" },
      "uf":       { "type": "keyword" }
    }
  }
}

Nota importante sobre search_analyzer: no campo nome_fornecedor_engram, o analyzer de indexação é o my_analyzer_ngram_3 (que gera os tokens em ngram), mas o analyzer de busca é o standard. Isso é intencional — na indexação queremos gerar todos os tokens progressivos, mas na busca queremos tratar o termo digitado pelo usuário como um token inteiro, não tokenizá-lo em ngrams. Usar o mesmo analyzer de indexação na busca produziria resultados incorretos.

Sobre o token_chars: ele lista as classes de caracteres que podem fazer parte de um token; o tokenizer divide o texto nos caracteres que ficaram de fora. Como whitespace está na lista, os espaços não separam palavras: GOL Linhas Aereas é tratado como um bloco único e os tokens são sempre prefixos do nome inteiro (gol, gol , gol l, gol li...), até 15 caracteres.

Passo 2 — Testar o tokenizer

Veja como o texto será dividido em tokens durante a indexação:

POST /fornecedor/_analyze
{
  "tokenizer": "split_3_ngram",
  "text": "GOLLINHASAEREAS"
}

Observe que serão gerados tokens progressivos: GOL, GOLL, GOLLI, GOLLIN, GOLLINH... até GOLLINHASAEREAS, com 15 caracteres (o max_gram). Como aqui foi testado apenas o tokenizer, sem o filtro lowercase do analyzer, os tokens continuam em maiúsculas.

Passo 3 — Inserir documentos

POST /fornecedor/_doc/1
{
  "codigo": 123,
  "nome_fornecedor": "GOL Linhas Aereas",
  "nome_fornecedor_engram": "GOL Linhas Aereas",
  "cnpj": "12222334000101",
  "endereco": "Rua das Flores, 100",
  "bairro": "Centro",
  "cidade": "Caxias",
  "uf": "RJ"
}
POST /fornecedor/_doc/2?refresh=true
{
  "codigo": 124,
  "nome_fornecedor": "TAM Linhas Aereas",
  "nome_fornecedor_engram": "TAM Linhas Aereas",
  "cnpj": "12222334000102",
  "endereco": "Avenida Brasil, 200",
  "bairro": "Fulano",
  "cidade": "Santos",
  "uf": "SP"
}

Passo 4 — Realizar buscas com o campo ngram

GET /fornecedor/_search
{
  "query": {
    "match": {
      "nome_fornecedor_engram": "TAM"
    }
  }
}

Graças ao edge_ngram, mesmo digitando apenas TAM a busca encontrará TAM Linhas Aereas. Teste também com GOL e compare os resultados.

Agora busque por uma palavra do meio do nome, nos dois campos:

GET /fornecedor/_search
{
  "query": {
    "match": {
      "nome_fornecedor_engram": "Linhas"
    }
  }
}
GET /fornecedor/_search
{
  "query": {
    "match": {
      "nome_fornecedor": "Linhas"
    }
  }
}

No campo nome_fornecedor_engram, Linhas não encontra nada: os tokens são apenas prefixos do nome inteiro, e nenhum deles é linhas. No campo nome_fornecedor, analisado pelo standard, os dois fornecedores são encontrados. Por isso é comum manter os dois campos lado a lado: um para o autocomplete e outro para a busca por palavra.


6.8 - Laboratório 4: criando aliases de índice

Neste laboratório, vamos criar dois índices representando a matriz e a filial de uma empresa, unificá-los com um alias e criar uma visão filtrada por estado.

Passo 1 — Criar os índices

PUT /fornecedor_matriz
{
  "settings": {
    "number_of_shards": 1,
    "number_of_replicas": 0
  },
  "mappings": {
    "properties": {
      "codigo":          { "type": "integer" },
      "nome_fornecedor": {
        "type": "text",
        "fields": { "keyword": { "type": "keyword" } }
      },
      "cnpj":     { "type": "keyword" },
      "endereco": { "type": "keyword" },
      "bairro":   { "type": "keyword" },
      "cidade":   { "type": "keyword" },
      "uf":       { "type": "keyword" }
    }
  }
}
PUT /fornecedor_filial
{
  "settings": {
    "number_of_shards": 1,
    "number_of_replicas": 0
  },
  "mappings": {
    "properties": {
      "codigo":          { "type": "integer" },
      "nome_fornecedor": {
        "type": "text",
        "fields": { "keyword": { "type": "keyword" } }
      },
      "cnpj":     { "type": "keyword" },
      "endereco": { "type": "keyword" },
      "bairro":   { "type": "keyword" },
      "cidade":   { "type": "keyword" },
      "uf":       { "type": "keyword" }
    }
  }
}

Passo 2 — Inserir documentos

POST /fornecedor_matriz/_doc/1
{
  "codigo": 124,
  "nome_fornecedor": "TAM Linhas Aereas",
  "cnpj": "12222334000102",
  "endereco": "Rua tal e etc",
  "bairro": "Fulano",
  "cidade": "Caxias",
  "uf": "RJ"
}
POST /fornecedor_filial/_doc/1?refresh=true
{
  "codigo": 124,
  "nome_fornecedor": "GOL Linhas Aereas",
  "cnpj": "12222334000102",
  "endereco": "Rua tal e etc",
  "bairro": "Fulano",
  "cidade": "Santos",
  "uf": "SP"
}

Passo 3 — Criar o alias corporacao

O alias corporacao aponta para os dois índices ao mesmo tempo. Uma busca pelo alias retorna dados de ambos:

POST /_aliases
{
  "actions": [
    { "add": { "index": "fornecedor_matriz", "alias": "corporacao" } },
    { "add": { "index": "fornecedor_filial", "alias": "corporacao" } }
  ]
}
GET /corporacao/_search

Passo 4 — Criar um alias filtrado fornecedores_sp

POST /_aliases
{
  "actions": [
    {
      "add": {
        "index": "fornecedor_*",
        "alias": "fornecedores_sp",
        "filter": {
          "term": { "uf": "SP" }
        }
      }
    }
  ]
}
GET /fornecedores_sp/_search

A busca retornará apenas documentos com uf: SP, independentemente de qual índice eles estão.

Confira os aliases criados:

GET /_cat/aliases/corporacao,fornecedores_sp?v

6.9 - Laboratório 5: evoluindo o mapping com reindex e troca de alias

O Laboratório 2 terminou com um problema real: o campo nome do catalogo usa o stemmer light_portuguese e a busca por correr não encontra corrida. Neste laboratório você vai colocar o catálogo atrás de um alias, ver o que pode e o que não pode ser alterado no mapping, e trocar o stemmer com um índice novo, sem que a aplicação perceba.

Passo 1 — Criar o alias produtos

A partir daqui, a "aplicação" usa apenas o alias produtos:

POST /_aliases
{
  "actions": [
    { "add": { "index": "catalogo", "alias": "produtos" } }
  ]
}
GET /produtos/_search
{
  "query": { "match_all": {} }
}

Passo 2 — Adicionar um campo ao mapping existente

Adicionar um campo novo é uma alteração permitida:

PUT /catalogo/_mapping
{
  "properties": {
    "marca": { "type": "keyword" }
  }
}

A resposta é "acknowledged": true.

Passo 3 — Tentar trocar o analyzer de um campo existente

PUT /catalogo/_mapping
{
  "properties": {
    "nome": {
      "type": "text",
      "analyzer": "standard",
      "fields": { "keyword": { "type": "keyword" } }
    }
  }
}

A requisição é rejeitada com um erro illegal_argument_exception informando que o parâmetro analyzer do campo nome não pode ser atualizado.

Passo 4 — Criar o catalogo-v2 com o mapping corrigido

O índice novo repete o mapping do catalogo, com duas diferenças: o portuguese_stemmer passa a usar o stemmer portuguese (Snowball) e o campo marca já nasce no mapping.

PUT /catalogo-v2
{
  "settings": {
    "number_of_shards": 1,
    "number_of_replicas": 0,
    "analysis": {
      "analyzer": {
        "pt_custom": {
          "type": "custom",
          "char_filter": ["html_strip"],
          "tokenizer": "standard",
          "filter": ["lowercase", "portuguese_stop", "portuguese_stemmer"]
        }
      },
      "filter": {
        "portuguese_stop":    { "type": "stop", "stopwords": "_portuguese_" },
        "portuguese_stemmer": { "type": "stemmer", "language": "portuguese" }
      }
    }
  },
  "mappings": {
    "properties": {
      "nome": {
        "type": "text",
        "analyzer": "pt_custom",
        "fields": { "keyword": { "type": "keyword" } }
      },
      "sku":       { "type": "keyword" },
      "preco":     { "type": "double" },
      "criado_em": { "type": "date" },
      "ativo":     { "type": "boolean" },
      "marca":     { "type": "keyword" }
    }
  }
}

Passo 5 — Copiar os documentos com _reindex

POST /_reindex?refresh=true
{
  "source": { "index": "catalogo" },
  "dest":   { "index": "catalogo-v2" }
}

A resposta mostra "created": 1. Compare a busca por correr no índice novo e no alias, que ainda aponta para o antigo:

GET /catalogo-v2/_search
{
  "query": {
    "match": { "nome": "correr" }
  }
}
GET /produtos/_search
{
  "query": {
    "match": { "nome": "correr" }
  }
}

O catalogo-v2 encontra o tênis (correr e corrida viram corr); o produtos ainda não.

Passo 6 — Trocar o alias de forma atômica

POST /_aliases
{
  "actions": [
    { "remove": { "index": "catalogo",    "alias": "produtos" } },
    { "add":    { "index": "catalogo-v2", "alias": "produtos" } }
  ]
}

A mesma busca pelo alias agora encontra o produto, sem nenhuma mudança do lado de quem consulta:

GET /produtos/_search
{
  "query": {
    "match": { "nome": "correr" }
  }
}

Passo 7 — Escrever pelo alias

Como o produtos aponta para um único índice, as escritas feitas por ele vão para o catalogo-v2:

POST /produtos/_doc?refresh=true
{
  "nome": "Camiseta de corrida feminina",
  "sku": "CAM-002",
  "preco": 89.9,
  "ativo": true,
  "criado_em": "2026-02-01",
  "marca": "Acme"
}
GET /_cat/count/catalogo-v2?v
GET /_cat/count/catalogo?v

O catalogo-v2 tem 2 documentos e o catalogo continua com 1. Depois de validar o índice novo, o antigo pode ser removido (veja a seção 6.11).


6.10 - Laboratório 6: aplicando todas as configurações de índice em conjunto

Neste laboratório vamos usar component templates e index templates para aplicar mapping, settings e uma política de ILM em múltiplos índices com uma única configuração. Esse é o padrão recomendado em produção para garantir consistência entre índices que seguem a mesma estrutura.

O fluxo é:

ILM Policy → Component Template → Index Template → Índices (criados automaticamente)

Passo 1 — Criar a ILM Policy

PUT /_ilm/policy/policy_pessoas
{
  "policy": {
    "phases": {
      "hot": {
        "actions": {}
      },
      "warm": {
        "min_age": "1d",
        "actions": {
          "forcemerge": {
            "max_num_segments": 1
          }
        }
      },
      "delete": {
        "min_age": "30d",
        "actions": {
          "delete": {}
        }
      }
    }
  }
}

Os tempos da policy estão em dias: durante o laboratório os índices permanecem na fase hot. O objetivo aqui é ver a policy sendo aplicada automaticamente pelo template, não acompanhar as transições de fase.

Passo 2 — Criar o component template

Um component template é um bloco reutilizável de configurações (settings, mappings, aliases) que pode ser incluído em um ou mais index templates. Isso evita repetição quando vários templates compartilham a mesma estrutura base.

PUT /_component_template/ct_pessoas
{
  "template": {
    "settings": {
      "number_of_shards": 1,
      "number_of_replicas": 0,
      "index.lifecycle.name": "policy_pessoas"
    },
    "mappings": {
      "properties": {
        "nome":  { "type": "text" },
        "idade": { "type": "integer" },
        "sexo":  { "type": "keyword" },
        "uf":    { "type": "keyword" }
      }
    },
    "aliases": {
      "pessoas": {}
    }
  }
}

Passo 3 — Criar o index template

O index template define o padrão de nome dos índices que receberão as configurações. O campo composed_of inclui o component template criado no passo anterior:

PUT /_index_template/it_pessoas
{
  "index_patterns": ["pessoas_*"],
  "composed_of": ["ct_pessoas"]
}

A partir desse momento, qualquer índice criado com o padrão pessoas_* receberá automaticamente o mapping, os settings e a ILM policy definidos no component template.

Passo 4 — Indexar documentos de exemplo

Como os índices pessoas_rj e pessoas_sp seguem o padrão pessoas_*, serão criados automaticamente com todas as configurações aplicadas:

POST /_bulk
{ "index": { "_index": "pessoas_rj", "_id": "1" } }
{ "nome": "João da Silva",   "sexo": "masculino", "idade": 19, "uf": "RJ" }
{ "index": { "_index": "pessoas_rj", "_id": "2" } }
{ "nome": "Maria Oliveira",  "sexo": "feminino",  "idade": 28, "uf": "RJ" }
{ "index": { "_index": "pessoas_rj", "_id": "3" } }
{ "nome": "Roberto Carlos",  "sexo": "masculino", "idade": 35, "uf": "RJ" }
{ "index": { "_index": "pessoas_rj", "_id": "4" } }
{ "nome": "Ana Maria",       "sexo": "feminino",  "idade": 14, "uf": "RJ" }
{ "index": { "_index": "pessoas_rj", "_id": "5" } }
{ "nome": "Paulo Souza",     "sexo": "masculino", "idade": 15, "uf": "RJ" }
POST /_bulk?refresh=true
{ "index": { "_index": "pessoas_sp", "_id": "1" } }
{ "nome": "Bruno Garcia",     "sexo": "masculino", "idade": 22, "uf": "SP" }
{ "index": { "_index": "pessoas_sp", "_id": "2" } }
{ "nome": "Renata Santos",    "sexo": "feminino",  "idade": 30, "uf": "SP" }
{ "index": { "_index": "pessoas_sp", "_id": "3" } }
{ "nome": "Gilberto Ferreira","sexo": "masculino", "idade": 48, "uf": "SP" }
{ "index": { "_index": "pessoas_sp", "_id": "4" } }
{ "nome": "Maria Silva",      "sexo": "feminino",  "idade": 12, "uf": "SP" }
{ "index": { "_index": "pessoas_sp", "_id": "5" } }
{ "nome": "Pedro Borges",     "sexo": "masculino", "idade": 15, "uf": "SP" }

Passo 5 — Verificar as configurações aplicadas

GET /pessoas_rj
GET /pessoas_sp
GET /pessoas/_search
GET /pessoas_*/_ilm/explain

Verifique que ambos os índices têm o mapping, os settings e a ILM policy aplicados conforme o component template, que a busca pelo alias pessoas retorna documentos dos dois índices e que o _ilm/explain mostra os dois índices com a policy policy_pessoas na fase hot.

Como o alias pessoas aponta para dois índices e nenhum deles é o write index, ele serve apenas para leitura: uma escrita em pessoas é rejeitada. Os documentos são escritos diretamente em pessoas_rj e pessoas_sp.


6.11 - Limpeza do ambiente

Remova os índices, templates e a policy criados neste capítulo. A remoção de índices exige os nomes explícitos (sem curingas), e a ordem importa: o index template precisa ser removido antes do component template que ele usa.

DELETE /clientes,clientes_strict,clientes_rigido,catalogo,catalogo-v2,fornecedor,fornecedor_matriz,fornecedor_filial,pessoas_rj,pessoas_sp

Os aliases (produtos, corporacao, fornecedores_sp e pessoas) são removidos junto com os índices.

DELETE /_index_template/it_pessoas
DELETE /_component_template/ct_pessoas
DELETE /_ilm/policy/policy_pessoas

Confira que não sobrou nada:

GET /_cat/indices/clientes*,catalogo*,fornecedor*,pessoas*?v

A resposta deve vir vazia, apenas com o cabeçalho.


Resumo do capítulo

  • O mapping define o tipo de cada campo. Em produção, defina-o explicitamente: o mapeamento dinâmico pode inferir tipos errados, e o tipo de um campo não pode ser alterado depois de mapeado.
  • text é analisado e serve para busca full-text; keyword guarda o valor exato para filtros, agregações e ordenação. Os multi-fields permitem ter os dois no mesmo campo.
  • O parâmetro dynamic (true, runtime, false, strict) controla o que acontece com campos novos, e os dynamic templates definem regras para mapeá-los.
  • Um analyzer é formado por char filters, um tokenizer e token filters, e é aplicado na indexação e na busca. A API _analyze mostra os tokens gerados.
  • Para o português, o light_portuguese trata plural e gênero; o portuguese (Snowball) também reduz formas verbais. Os normalizers fazem o mesmo papel para campos keyword, gerando um único token.
  • O edge_ngram com search_analyzer diferente do de indexação é a base do autocomplete.
  • Aliases dão um nome estável para um ou mais índices, com filtros opcionais. A troca atômica de alias, junto com o _reindex, permite corrigir o mapping sem parar a aplicação.
  • Component templates e index templates aplicam mapping, settings, aliases e ILM policy a todos os índices de um padrão de nome.

Origem do conteúdo

6.1 - Mapping

6.2 - Analysis

6.3 - Aliases

6.4 a 6.11 - Laboratórios

⚠️ **GitHub.com Fallback** ⚠️