Evitando problemas com ElasticSearch - Parte 2: Mapeamento

Publicado por Thiago von Sydow no dia dev

mapa

Continuando com os nossos problemas enfrentados com o ElasticSearch, neste post irei mostrar como um mapeamento não planejado corretamente pode dar uma grande dor de cabeça. Caso você não tenha lido o primeiro post desta série leia aqui.

Vou exemplificar com um cenário que temos aqui na RD. Quando uma pessoa se inscreve em uma newsletter, landing page ou qualquer outro formuário do seu site, chamamos isso de conversão. Você pode ter vários formulários no seu site, em diferentes páginas e se quisermos tornar os dados destas conversões pesquisáveis, podemos mapeá-las no ElasticSearch da seguinte forma:

1
2
3
4
5
6
7
8
9
10
11
12
curl -XPUT 'localhost:9200/post_elasticsearch' -d '
{
  "mappings" : {
    "pessoa" : {
      "properties" : {
        "conversoes" : {
          "type" : "object"
        }
      }
    }
  }
} '

Vamos inserir dois documentos:

1
2
curl -XPOST 'localhost:9200/post_elasticsearch/pessoa' -d '{ "email": "xunda.silva@email.com", "conversoes" : [{ "nome" : "Xunda Silva", "cidade" : "Santos" }] }'
curl -XPOST 'localhost:9200/post_elasticsearch/pessoa' -d '{ "email": "xunda.santos@email.com", "conversoes" : [{ "nome" : "Xunda Santos", "cidade" : "Grande Florianópolis", "sexo": "masculino" }, { "nome" : "Xunda Santos", "cidade" : "Florianópolis", "sexo": "masculino" }] }'

Fazendo uma consulta para retornar todos os documentos (foi omitido o conteúdo irrelevante):

1
GET /pessoa/_search -d '{"query":{"match_all":{}}}'
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
"hits": [
   {
      "_source": {
         "email": "xunda.silva@email.com",
         "conversoes": [
            {
               "nome": "Xunda Silva",
               "cidade": "Santos"
            }
         ]
      }
   },
   {
      "_source": {
         "email": "xunda.santos@email.com",
         "conversoes": [
            {
               "nome": "Xunda Santos",
               "cidade": "Grande Florianópolis",
               "sexo": "masculino"
            },
            {
               "nome": "Xunda Santos",
               "cidade": "Florianópolis",
               "sexo": "masculino"
            }
         ]
      }
   }
]

Notou como mesmo sem declarar quais campos uma conversão poderia ter, eles aparecem normalmente? Isso acontece porque o ElasticSearch insere automaticamente novos campos, recurso chamado de dynamic mapping. É possível desligar este comportamento, mas por quê desligar se dá muito menos trabalho não se preocupar com declaração de campos e seus tipos? Vamos olhar o mapeamento do nosso tipo pessoa, repare que o campo sexo fica presente mesmo que não seja utilizado em todos os documentos :

1
curl -XGET 'localhost:9200/post_elasticsearch/pessoa/_mapping'
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
{
  "post_elasticsearch":  {
    "mappings":{
      "pessoa":{
        "properties":{
          "conversoes":{
            "properties":{
              "cidade":{
                "type":"string"
              },
              "nome":{
                "type":"string"
              },
              "sexo":{
                "type":"string"
              }
            }
          },
          "email":{
            "type":"string"
          }
        }
      }
    }
  }
}

Problema nº 1

Se você permite por exemplo que seus formulários tenham nomes de campos customizados, eventualmente seu mapeamento irá crescer de uma forma exagerada, afetando a performance desnecessariamente. Qual é a solução para este problema? Basta tratar o objeto conversoes como um Array de objetos chave/valor, desta forma:

1
2
curl -XPOST 'localhost:9200/post_elasticsearch/pessoa' -d '{ "email": "xunda.silva@email.com", "conversoes" : [{ "conteudo": [ {"chave": "nome", "valor": "Xunda Silva"}, {"chave": "cidade", "valor": "Santos"} ] }] }'
curl -XPOST 'localhost:9200/post_elasticsearch/pessoa' -d '{ "email": "xunda.silva@email.com", "conversoes" : [{ "conteudo": [ {"chave": "nome", "valor": "Xunda Santos"}, {"chave": "cidade", "valor": "Florianópolis"}, {"chave": "sexo", "valor": "masculino"} ] }] }'

Vamos buscar novamente todos os documentos?

1
GET /pessoa/_search -d '{"query":{"match_all":{}}}'
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
"hits": [
   {
      "_source": {
         "email": "xunda.santos@email.com",
         "conversoes": [
          {
             "conteudo": [
                {
                   "chave": "nome",
                   "valor": "Xunda Santos"
                },
                {
                   "chave": "cidade",
                   "valor": "Florianópolis"
                },
                {
                   "chave": "sexo",
                   "valor": "masculino"
                }
             ]
          }
        ]
      }
   },
   {
      "_source": {
         "email": "xunda.silva@email.com",
         "conversoes": [
          {
             "conteudo": [
                {
                   "chave": "nome",
                   "valor": "Xunda Silva"
                },
                {
                   "chave": "cidade",
                   "valor": "Santos"
                }
             ]
          }
        ]
      }
   }
]

E agora nosso mapeamento fica constante:

1
curl -XGET 'localhost:9200/post_elasticsearch/pessoa/_mapping'
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
{
  "post_elasticsearch":  {
    "mappings":{
      "pessoa":{
        "properties":{
          "conversoes":{
            "properties":{
              "chave":{
                "type":"string"
              },
              "valor":{
                "type":"string"
              }
            }
          },
          "email":{
            "type":"string"
          }
        }
      }
    }
  }
}

Pronto! Tô manjando muito desse tal de ElasticSearch, já sei o que eu quero agora. Vou fazer uma query para buscar todas as pessoas que moram em Santos, basta buscar por chave igual a cidade e valor igual a Santos, né? O resultado irá trazer apenas a pessoa Xunda Silva, fácil demais.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
  GET /pessoa/_search -d '{
    "query": {
      "bool": {
        "must": [
          {
           "match": {
             "conversoes.conteudo.chave": "cidade"
            }
          },
          {
           "match": {
             "conversoes.conteudo.valor": "Santos"
            }
          }
        ]
      }
    }
  }'
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
"hits": [
   {
      "_source": {
         "email": "xunda.santos@email.com",
         "conversoes": [
          {
             "conteudo": [
                {
                   "chave": "nome",
                   "valor": "Xunda Santos"
                },
                {
                   "chave": "cidade",
                   "valor": "Florianópolis"
                },
                {
                   "chave": "sexo",
                   "valor": "masculino"
                }
             ]
          }
        ]
      }
   },
   {
      "_source": {
         "email": "xunda.silva@email.com",
         "conversoes": [
          {
             "conteudo": [
                {
                   "chave": "nome",
                   "valor": "Xunda Silva"
                },
                {
                   "chave": "cidade",
                   "valor": "Santos"
                }
             ]
          }
        ]
      }
   }
]

Not this time

Dor nº 2

Como assim? Está tudo certo, será que a nossa busca está montada errada? Não, nossa busca está correta, acontece que este comportamento é conhecido, e por isso um type: object não deve ser utilizado se você irá ter vários campos com nomes iguais, mas qual o motivo?

Vamos ver como fica o nosso campo conversoes.conteudo quando indexado:

1
2
3
4
{
  "conversoes.conteudo.chave": ["nome", "cidade"," sexo"],
  "conversoes.conteudo.valor": ["xunda", "silva", "santos", "florianópolis", "masculino"]
}

Os valores de um object ficam todos juntos em um Array, logo nossa busca na verdade está correta, mas o que esperávamos do mapeamento não. Ambas pessoas contém a chave cidade, e ambos contém o valor Santos. Para resolvermos este comportamento, devemos utilizar Nested Objects. A alteração do mapeamento original é simplesmente trocar object por nested :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
curl -XPUT 'localhost:9200/post_elasticsearch' -d '
{
  "mappings" : {
    "pessoa" : {
      "properties" : {
        "conversoes" : {
          "type" : "object",
          "properties" : {
            "conteudo" : {
              "type": "nested"
            }
          }
        }
      }
    }
  }
}'

Os documentos são inseridos da mesma forma de antes, mas a consulta muda ligeiramente:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
GET /pessoa/_search -d '{
  "query": {
    "nested": {
       "path": "conversoes.conteudo",
       "query": {
         "bool": {
           "must": [
             {
               "match": {
                 "conversoes.conteudo.chave": "cidade"
               }
             },
             {
               "match": {
                 "conversoes.conteudo.valor": "Santos"
               }
             }
          ]
        }
      }
    }
  }
}'
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
"hits": [
  {
     "_source": {
        "email": "xunda.silva@email.com",
        "conversoes": [
         {
            "conteudo": [
               {
                  "chave": "nome",
                  "valor": "Xunda Silva"
               },
               {
                  "chave": "cidade",
                  "valor": "Santos"
               }
            ]
         }
       ]
     }
  }
]

Nailed

Agora que conseguimos realizar o mapeamento corretamente e os resultados retornam de acordo com nossas expectativas, vale ressaltar que dependendo da quantidade de Nested Objects que você colocar dentro de um documento, a performance será afetada, pois cada um deles se torna um documento interno, aumentando o número de documentos no seu index rapidamente. Talvez para seu caso seja melhor utilizar um dos modelos de relacionamento existentes no ElasticSearch, assunto que irei explicar no próximo post da série.

Quer saber mais sobre os problemas que tivemos e como estamos resolvendo do ElasticSearch? Deixe seu comentário. Até nosso próximo post ;)

Thiago von Sydow

Thiago von Sydow

Full Stack Developer

Comentários