06 ‐ Index Settings ‐ Entendendo Mapping, Analyzers e Aliases - BktechBrazil/elastic-eck GitHub Wiki
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).
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
integerelong, prefiralong— o custo de armazenamento é pequeno e evita problemas de overflow em campos que podem crescer. Entrefloatedouble, usedoublepara valores financeiros onde precisão importa, efloatapenas em métricas de monitoramento onde uma pequena perda de precisão é aceitável.
PUT /clientes
{
"mappings": {
"properties": {
"nome": { "type": "text" },
"idade": { "type": "integer" },
"email": { "type": "keyword" }
}
}
}É 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.
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/1A 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
}
}
}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.
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.
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"
}
}
}
]
}
}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).
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.
Um analyzer é composto por três partes, aplicadas em sequência:
-
Character Filters: processam o texto bruto antes da tokenização. Podem remover ou substituir caracteres — por exemplo, remover tags HTML ou converter
&emand. -
Tokenizer: divide o texto em tokens. O tokenizer
standarddivide por espaços e pontuação, removendo a pontuação dos tokens resultantes. Cada analyzer tem exatamente um tokenizer. -
Token Filters: processam cada token gerado pelo tokenizer. Podem converter para minúsculas (
lowercase), remover stopwords (stop), aplicar stemming, adicionar sinônimos, entre outros.
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.
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 |
Os analyzers de idioma, como o portuguese, combinam o tokenizer standard com três etapas de filtro:
-
lowercase: converte para minúsculas; -
stop: remove stopwords, palavras muito frequentes que pouco ajudam na busca (o,os,de,para,estavam...); -
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.
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_stripremove as tags HTML antes da tokenização; -
portuguese_stopusa a lista de stopwords em português que já vem com o Elasticsearch (_portuguese_); -
portuguese_stemmeraplica o stemmerlight_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.
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), usePOST /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 índicefornecedoré 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 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"
}
}
}
}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)
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.
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.
POST /_aliases
{
"actions": [
{
"remove": {
"index": "clientes",
"alias": "clientes_atuais"
}
}
]
}GET /_cat/aliases?vQuando 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.
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:
- a aplicação lê e escreve sempre pelo alias (
produtos), nunca pelo nome físico do índice; - cria-se o índice novo (
catalogo-v2) com o mapping corrigido; - a API
_reindexcopia os documentos do índice antigo para o novo; - 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.
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.
No terminal da VM:
kubectl -n elastic get elasticsearch,kibanaResultado esperado: lab-es com HEALTH green, NODES 1 e PHASE Ready, e lab-kb com HEALTH green.
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.logResultado 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-estem 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 ficariayellow.
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.
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/_mappingE apenas o campo category:
GET /kibana_sample_data_ecommerce/_mapping/field/categoryRepare nas escolhas feitas para cada tipo de dado:
-
categoryétextcom o multi-fieldcategory.keyword: dá para buscar por palavra (matchemcategory) e agregar por categoria (termsemcategory.keyword); -
currency,customer_ideday_of_weeksão apenaskeyword: são valores exatos, usados só em filtros e agregações; -
order_dateédateetaxful_total_priceéhalf_float; -
geoip.locationégeo_point, o que permite usar o campo em mapas.
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 subcampokeywordpara ordenação e agregações -
sexoeestado_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 comlogradouroserá automaticamente indexado comokeyword
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.
GET /clientes/_mappingConfirme que logradouro_principal e logradouro_secundario aparecem com o tipo keyword no mapping retornado.
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/1Testando 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.
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.
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.
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.
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.
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.
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" }
}
}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.
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 camponome_fornecedor_engram, o analyzer de indexação é omy_analyzer_ngram_3(que gera os tokens em ngram), mas o analyzer de busca é ostandard. 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. Comowhitespaceestá 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.
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.
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"
}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.
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.
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" }
}
}
}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"
}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/_searchPOST /_aliases
{
"actions": [
{
"add": {
"index": "fornecedor_*",
"alias": "fornecedores_sp",
"filter": {
"term": { "uf": "SP" }
}
}
}
]
}GET /fornecedores_sp/_searchA busca retornará apenas documentos com uf: SP, independentemente de qual índice eles estão.
Confira os aliases criados:
GET /_cat/aliases/corporacao,fornecedores_sp?vO 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.
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": {} }
}Adicionar um campo novo é uma alteração permitida:
PUT /catalogo/_mapping
{
"properties": {
"marca": { "type": "keyword" }
}
}A resposta é "acknowledged": true.
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.
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" }
}
}
}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.
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" }
}
}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?vGET /_cat/count/catalogo?vO 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).
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)
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.
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": {}
}
}
}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.
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" }GET /pessoas_rjGET /pessoas_spGET /pessoas/_searchGET /pessoas_*/_ilm/explainVerifique 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
pessoasaponta para dois índices e nenhum deles é o write index, ele serve apenas para leitura: uma escrita empessoasé rejeitada. Os documentos são escritos diretamente empessoas_rjepessoas_sp.
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_spOs aliases (produtos, corporacao, fornecedores_sp e pessoas) são removidos junto com os índices.
DELETE /_index_template/it_pessoasDELETE /_component_template/ct_pessoasDELETE /_ilm/policy/policy_pessoasConfira que não sobrou nada:
GET /_cat/indices/clientes*,catalogo*,fornecedor*,pessoas*?vA resposta deve vir vazia, apenas com o cabeçalho.
- 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;keywordguarda 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
_analyzemostra os tokens gerados. - Para o português, o
light_portuguesetrata plural e gênero; oportuguese(Snowball) também reduz formas verbais. Os normalizers fazem o mesmo papel para camposkeyword, gerando um único token. - O
edge_ngramcomsearch_analyzerdiferente 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.
6.1 - Mapping
- Mapping
- Field data types
- Multi-fields
- Document metadata fields
- Optimistic concurrency control (
_seq_noe_primary_term) - Mapping parameters
- Explicit mapping
- Dynamic field mapping
- Parâmetro
dynamic - Dynamic templates
- Update mapping API examples
6.2 - Analysis
- Text analysis
- Anatomy of an analyzer
- Index and search analysis
- Built-in analyzers
- Language analyzers (analyzer
portuguese) - Stemming
- Stemmer token filter
- Create a custom analyzer
- Test an analyzer
- Normalizers
6.3 - Aliases
6.4 a 6.11 - Laboratórios
- Mapping dos dados de exemplo eCommerce (Kibana 9.4)
- Analyze API
- HTML strip character filter
- Stop token filter
- Edge n-gram tokenizer
- Search analyzer
- Update field mappings API
- Templates
- Create or update a component template API
- Create or update an index template API
- Index modules: number_of_replicas
- Delete index API (
action.destructive_requires_name)

