Otimizando Consultas Lentas no Elasticsearch: Melhores Práticas para Ajuste de Desempenho

Diagnostique e melhore consultas lentas no Elasticsearch com melhor formato de consulta, paginação, cache, mapeamentos e a API de Perfil.

Otimizando Consultas Lentas no Elasticsearch: Melhores Práticas para Ajuste de Desempenho

Consultas lentas no Elasticsearch geralmente vêm de um de quatro lugares: a consulta pede demais, o mapeamento torna a consulta cara, o cluster está com recursos escassos ou a aplicação repete buscas custosas que deveriam ser armazenadas em cache ou redesenhadas. A correção depende de qual delas é verdadeira.

Antes de reescrever tudo, capture uma requisição lenta real com seu índice, filtros, ordenação, agregações, profundidade de página, tamanho da resposta e tempo. Uma agregação de dashboard, uma consulta de autocomplete e um trabalho de exportação estressam o Elasticsearch de maneiras diferentes.

Entendendo os Gargalos de Desempenho de Consultas

Antes de mergulhar nas soluções, é útil entender as razões comuns por trás de consultas lentas no Elasticsearch. Elas geralmente incluem:

  • Consultas Complexas: Consultas com múltiplas cláusulas bool, consultas aninhadas ou operações caras como wildcard ou regexp em grandes conjuntos de dados.
  • Recuperação Ineficiente de Dados: Buscar _source desnecessariamente ou recuperar grandes números de documentos para paginação.
  • Restrições de Recursos: CPU, memória ou I/O de disco insuficientes nos nós de dados.
  • Mapeamentos Subótimos: Usar tipos de dados incorretos ou não aproveitar doc_values para agregações.
  • Desequilíbrio ou Sobrecarga de Shards: Muitos shards, poucos shards ou distribuição desigual de shards/dados.
  • Perda de Cache ou Ajuste Ruim de Cache: Repetir buscas caras sem usar cache de requisição, contexto de filtro ou cache em nível de aplicação quando apropriado.

Otimizando a Estrutura da Consulta

A maneira como você constrói suas consultas tem um impacto profundo em seu desempenho. Pequenas mudanças podem levar a melhorias significativas.

1. Recupere Apenas os Campos Necessários (Filtragem _source e stored_fields)

Por padrão, o Elasticsearch retorna todo o campo _source para cada documento correspondente. Se seus documentos são grandes e a interface precisa apenas de um título, ID e timestamp, buscar o documento inteiro desperdiça largura de banda de rede e tempo de análise.

  • Filtragem _source: Use o parâmetro _source para especificar um array de campos a incluir ou excluir.

    GET /meu-indice/_search
    {
      "_source": ["titulo", "autor", "data_publicacao"],
      "query": {
        "match": {
          "conteudo": "desempenho Elasticsearch"
        }
      }
    }
    
  • stored_fields: Se você armazenou explicitamente campos específicos em seu mapeamento ("store": true), pode recuperá-los com stored_fields. A maioria das implantações não armazena muitos campos dessa forma, então a filtragem _source é a correção mais comum.

    GET /meu-indice/_search
    {
      "stored_fields": ["titulo", "autor"],
      "query": {
        "match": {
          "conteudo": "desempenho Elasticsearch"
        }
      }
    }
    

2. Prefira Tipos de Consulta Eficientes

Alguns tipos de consulta são inerentemente mais intensivos em recursos do que outros.

  • Evite Wildcards à Esquerda e Regexps Amplos: Consultas wildcard e regexp podem ser caras, especialmente com wildcards à esquerda como *teste. Consultas de prefixo são geralmente mais gerenciáveis do que buscas com wildcard à esquerda, mas ainda precisam de mapeamentos sensatos e entrada limitada.

    # Ineficiente - evite wildcard à esquerda
    {
      "query": {
        "wildcard": {
          "nome.keyword": {
            "value": "*busca"
          }
        }
      }
    }
    
    # Melhor - se você conhece o prefixo
    {
      "query": {
        "prefix": {
          "nome.keyword": {
            "value": "Elastic"
          }
        }
      }
    }
    
  • Use match_phrase para intenção de frase: Se o usuário está buscando uma frase exata, match_phrase expressa essa intenção melhor do que várias cláusulas match não relacionadas. Nem sempre é mais barato, mas evita retornar documentos que contêm as palavras muito distantes.

  • Contexto de filtro para condições sim/não: Quando você só se importa se um documento corresponde a uma condição, coloque essa condição no contexto filter ou use constant_score. Isso evita trabalho de pontuação desnecessário e é mais amigável ao cache.

    GET /meu-indice/_search
    {
      "query": {
        "constant_score": {
          "filter": {
            "term": {
              "status": "ativo"
            }
          }
        }
      }
    }
    

3. Otimize Consultas Booleanas

  • Use filtros para restrições estruturadas: Coloque IDs de tenant, valores de status, intervalos de data e tags exatas em filter, não em must, a menos que precisem de pontuação. O Elasticsearch pode reordenar e otimizar cláusulas internamente, então não confie na ordem JSON como sua principal ferramenta de desempenho.
  • Use minimum_should_match intencionalmente: Pode melhorar a relevância e reduzir correspondências amplas, mas configurá-lo muito alto pode ocultar resultados válidos.

4. Paginação Eficiente (search_after e scroll)

A paginação tradicional from/size torna-se muito ineficiente para páginas profundas (por exemplo, from: 10000, size: 10). O Elasticsearch tem que recuperar e ordenar todos os documentos até from + size em cada shard, depois descartar from documentos.

  • search_after: Para paginação profunda em tempo real, search_after é recomendado. Ele usa a ordem de ordenação do último documento da página anterior para encontrar o próximo conjunto de resultados, semelhante a cursores em bancos de dados tradicionais. É sem estado e escala melhor.

    # Primeira requisição
    GET /meu-indice/_search
    {
      "size": 10,
      "query": {"match_all": {}},
      "sort": [{"timestamp": "asc"}, {"_id": "asc"}]
    }
    
    # Requisição subsequente usando os valores de ordenação do último documento da primeira requisição
    GET /meu-indice/_search
    {
      "size": 10,
      "query": {"match_all": {}},
      "search_after": [1678886400000, "doc_id_XYZ"],
      "sort": [{"timestamp": "asc"}, {"_id": "asc"}]
    }
    
  • API scroll: Para recuperação em massa de grandes conjuntos de dados, como reindexação ou exportações, scroll ainda pode ser útil. Para versões mais recentes do Elasticsearch e varreduras completas de índice de longa duração, considere também point-in-time mais search_after. Scroll não é adequado para paginação em tempo real voltada ao usuário.

5. Otimizando Agregações

Agregações podem ser intensivas em recursos, especialmente em campos de alta cardinalidade.

  • Pré-computando Agregações: Considere executar agregações complexas e não em tempo real durante a indexação ou em um cronograma para pré-computar resultados e armazená-los em um índice separado.
  • doc_values: Garanta que campos usados em agregações tenham doc_values habilitados (que é o padrão para a maioria dos campos não-texto). Isso permite que o Elasticsearch carregue dados para agregações de forma eficiente sem carregar _source.
  • eager_global_ordinals: Para campos keyword frequentemente usados em agregações terms, configurar eager_global_ordinals: true no mapeamento pode melhorar o desempenho pré-construindo ordinais globais. Isso incorre em um custo no momento da atualização do índice, mas acelera as agregações em tempo de consulta.

Aproveitando Técnicas de Cache

O Elasticsearch oferece várias camadas de cache que podem acelerar significativamente consultas repetidas.

1. Cache de Consulta do Nó

  • Mecanismo: Armazena em cache os resultados de cláusulas de filtro dentro de consultas bool que são usadas com frequência. É um cache em memória no nível do nó.
  • Eficácia: Mais eficaz para cláusulas de filtro repetidas. Não conte com ele para todas as consultas; o Elasticsearch decide o que vale a pena armazenar em cache.
  • Configuração: Habilitado por padrão. Você pode controlar seu tamanho com indices.queries.cache.size (padrão 10% do heap).

2. Cache de Requisição do Shard

  • Mecanismo: Armazena em cache resultados de busca em nível de shard, mais comumente para requisições pesadas em agregações com size=0. É um ajuste forte para consultas de dashboard repetidas sobre dados que não estão mudando a cada segundo.

  • Eficácia: Excelente para consultas de dashboard ou aplicações analíticas onde a mesma requisição (incluindo agregações) é executada repetidamente com parâmetros idênticos.

  • Como usar: Habilite-o explicitamente em sua consulta usando "request_cache": true.

    GET /meu-indice/_search?request_cache=true
    {
      "size": 0,
      "query": {
        "bool": {
          "filter": [
            {"term": {"status.keyword": "ativo"}},
            {"range": {"timestamp": {"gte": "now-1h"}}}
          ]
        }
      },
      "aggs": {
        "mensagens_por_minuto": {
          "date_histogram": {
            "field": "timestamp",
            "fixed_interval": "1m"
          }
        }
      }
    }
    
  • Ressalvas: O cache é invalidado sempre que um shard é atualizado (novos documentos são indexados ou existentes são atualizados). Útil apenas para consultas que retornam resultados idênticos com frequência.

3. Cache do Sistema de Arquivos (Nível do SO)

  • Mecanismo: O cache do sistema de arquivos do sistema operacional desempenha um papel crítico. O Elasticsearch depende fortemente dele para armazenar em cache segmentos de índice acessados com frequência.
  • Eficácia: Crucial para o desempenho de consultas. Se os segmentos de índice estão na RAM, o I/O de disco é completamente evitado, levando a uma execução de consulta muito mais rápida.
  • Melhor Prática: Deixe RAM substancial para o cache do sistema de arquivos. Um ponto de partida comum é manter o heap JVM em torno da metade da memória do sistema, com os limites usuais de heap do Elasticsearch em mente, depois valide com sua carga de trabalho.

4. Cache em Nível de Aplicação

  • Mecanismo: Implementar um cache na camada da sua aplicação (por exemplo, usando Redis, Memcached ou um cache em memória) para resultados de busca frequentemente solicitados.
  • Eficácia: Pode fornecer os tempos de resposta mais rápidos ao evitar completamente o Elasticsearch para requisições repetidas. Melhor para resultados de busca estáticos ou que mudam lentamente.
  • Considerações: A estratégia de invalidação de cache é chave. Requer design cuidadoso para garantir consistência de dados.

Usando a API de Perfil para Identificação de Gargalos

A API de Perfil é uma ferramenta inestimável para entender exatamente como o Elasticsearch executa uma consulta e onde o tempo é gasto. Ela detalha o tempo de execução para cada componente de sua consulta e agregação.

Como Usar a API de Perfil

Simplesmente adicione "profile": true ao corpo da sua requisição de busca.

GET /meu-indice/_search
{
  "profile": true,
  "query": {
    "bool": {
      "must": [
        {"match": {"titulo": "Elasticsearch"}},
        {"term": {"status.keyword": "publicado"}}
      ],
      "filter": [
        {"range": {"data_publicacao": {"gte": "2023-01-01"}}}
      ]
    }
  },
  "aggs": {
    "principais_autores": {
      "terms": {
        "field": "autor.keyword",
        "size": 10
      }
    }
  }
}

Interpretando Resultados da API de Perfil

A resposta incluirá uma seção profile detalhando a execução da consulta e agregação em cada shard. Métricas chave a procurar incluem:

  • description: O componente específico da consulta ou agregação.
  • time_in_nanos: O tempo gasto executando este componente.
  • breakdown: Sub-métricas detalhadas como build_scorer_time, collect_time, set_weight_time para consultas e reduce_time para agregações.
  • children: Componentes aninhados, mostrando como o tempo é distribuído dentro de consultas complexas.

Exemplo de Interpretação:

Se você vir um alto time_in_nanos para um WildcardQuery, confirma que esta é uma parte cara da sua consulta. Se collect_time é alto, sugere que recuperar e processar documentos após uma correspondência é um gargalo, possivelmente devido à análise de _source ou paginação profunda. Alto reduce_time em agregações pode indicar uma carga pesada durante a fase final de mesclagem.

Ao examinar essas métricas, você pode identificar cláusulas de consulta específicas ou campos de agregação que estão consumindo mais recursos e então aplicar as técnicas de otimização discutidas anteriormente.

Melhores Práticas Gerais para Desempenho

Além de otimizações específicas de consulta, várias práticas recomendadas em todo o cluster e nível de índice contribuem para o desempenho geral da busca.

1. Mapeamentos de Índice Ótimos

  • text vs. keyword: Use text para busca de texto completo e keyword para correspondência de valor exato, ordenação e agregações. Tipos incompatíveis podem levar a consultas ineficientes.
  • doc_values: Garanta que doc_values estejam habilitados para campos que você pretende ordenar ou agregar. Eles estão habilitados por padrão para a maioria dos tipos de campo que suportam ordenação e agregações, como campos keyword, numéricos, de data, booleanos e IP. Campos text simples são para busca de texto completo; use um subcampo keyword quando precisar de correspondência exata ou agregação.
  • norms: Desabilite norms ("norms": false) para campos onde você não precisa de normalização de comprimento de documento (por exemplo, campos de ID). Isso economiza espaço em disco e melhora a velocidade de indexação, com impacto mínimo no desempenho de consultas para consultas sem pontuação.
  • index_options: Para campos text, use index_options: docs se você só precisa saber se um termo existe em um documento, e index_options: positions (o padrão) se você precisa de consultas de frase e buscas de proximidade.

2. Monitore a Saúde do Cluster e Recursos

  • Status do Cluster: Verde é o objetivo. Amarelo significa que um ou mais shards de réplica não estão atribuídos; as buscas ainda podem funcionar, mas a resiliência é reduzida e o desempenho pode sofrer. Vermelho significa que shards primários estão faltando e alguns dados estão indisponíveis.
  • Monitoramento de Recursos: Monitore regularmente CPU, RAM, I/O de disco e uso de rede em seus nós de dados. Picos nessas métricas geralmente se correlacionam com consultas lentas.
  • Heap JVM: Fique de olho no uso do heap JVM. Alta utilização pode levar a pausas frequentes de coleta de lixo, tornando as consultas lentas. Otimize consultas para reduzir a pressão no heap.

3. Alocação Adequada de Shards

  • Muitos Shards: Cada shard consome recursos. Muitos shards pequenos criam sobrecarga. Shards na casa das dezenas de gigabytes são comuns, mas o tamanho certo depende do heap, padrão de consulta, metas de recuperação e hardware.
  • Poucos Shards: Limita o paralelismo. Consultas contra um índice com poucos shards não conseguirão aproveitar todos os nós de dados disponíveis de forma eficiente.

4. Estratégia de Indexação

  • Intervalo de Atualização: Um refresh_interval mais baixo (padrão 1 segundo) torna os dados visíveis mais rapidamente, mas aumenta a sobrecarga de indexação. Para cargas de trabalho pesadas em busca, considere aumentá-lo ligeiramente (por exemplo, 5-10 segundos) para reduzir a pressão de atualização.

O fluxo de trabalho prático é simples: encontre a consulta lenta real, perfile-a, reduza a quantidade de dados que ela toca e faça o mapeamento corresponder à maneira como os usuários buscam. Se a consulta já está limpa, olhe para o layout do shard, pressão no heap, cache do sistema de arquivos e I/O de disco. O Elasticsearch é rápido quando o design do índice, o formato da consulta e os recursos do cluster concordam entre si.