Dominando o Elasticsearch Query DSL: Comandos Essenciais para Recuperação de Dados

Desbloqueie o poder da recuperação do Elasticsearch dominando o Query DSL. Este guia detalha estruturas essenciais de consulta JSON, focando no uso prático das consultas `match`, `term` e range. Aprenda a diferença crítica entre as cláusulas `must` (pontuação) e `filter` (cache) dentro da consulta fundamental `bool`, permitindo construir pesquisas de dados complexas e de alto desempenho de forma eficiente.

Dominando o Elasticsearch Query DSL: Comandos Essenciais para Recuperação de Dados

O Elasticsearch Query DSL é a linguagem JSON que você usa quando uma caixa de pesquisa simples não é suficiente. Ele permite misturar pesquisa de texto completo, filtros exatos, intervalos de datas, ordenação, paginação e agregações em uma única requisição. Essa flexibilidade é útil, mas também torna fácil escrever uma consulta que retorna os documentos errados ou que funciona bem em testes e fica lenta em produção.

A melhor maneira de aprender o Query DSL é manter duas perguntas em mente: "Estou pesquisando texto por relevância?" e "Estou filtrando valores exatos?" A maioria das escolhas de consulta segue a partir dessa divisão.

A Anatomia de uma Requisição de Pesquisa no Elasticsearch

Todas as pesquisas do Elasticsearch são realizadas no endpoint _search de um índice específico (ou índices). Uma requisição de pesquisa básica é uma requisição POST contendo um corpo JSON que define os parâmetros da consulta. A parte mais crítica deste corpo é o objeto query.

Estrutura Básica:

POST /your_index_name/_search
{
  "query": { ... Defina sua estrutura de consulta aqui ... },
  "size": 10, 
  "from": 0
}

Tipos de Consulta Principais: Precisão e Relevância

O Query DSL oferece uma vasta gama de consultas adaptadas para diferentes tipos de dados e necessidades de correspondência. A escolha da consulta impacta significativamente tanto a pontuação de relevância quanto o desempenho.

1. Pesquisa de Texto Completo: A Consulta match

A consulta match é o padrão para pesquisa de texto completo em campos analisados. Ela tokeniza o termo de pesquisa e verifica tokens correspondentes no(s) campo(s) especificado(s).

Caso de Uso: Pesquisar texto em linguagem natural onde a pontuação de relevância é importante.

Exemplo: Encontrar documentos onde o campo 'description' contém a palavra 'cloud' ou 'computing'.

GET /products/_search
{
  "query": {
    "match": {
      "description": "cloud computing"
    }
  }
}

2. Correspondência de Valor Exato: A Consulta term

A consulta term pesquisa documentos que contêm o termo exato especificado. Ao contrário de match, ela não realiza análise na string de pesquisa, tornando-a ideal para correspondências exatas em palavras-chave, IDs ou campos indexados numericamente.

Caso de Uso: Filtrar por valores exatos em campos não analisados (como campos keyword ou números).

Exemplo: Recuperar um produto com o ID exato SKU10021.

GET /products/_search
{
  "query": {
    "term": {
      "product_id": "SKU10021"
    }
  }
}

3. Consultas de Intervalo (Range)

As consultas de intervalo permitem filtrar documentos onde o valor de um campo está dentro de um intervalo especificado (numérico, data ou string).

Sintaxe: Usa gt (maior que), gte (maior ou igual a), lt (menor que) e lte (menor ou igual a).

Exemplo: Encontrar pedidos feitos após 1º de janeiro de 2024.

GET /orders/_search
{
  "query": {
    "range": {
      "order_date": {
        "gte": "2024-01-01",
        "lt": "2025-01-01"
      }
    }
  }
}

4. Filtrando por Presença: A Consulta exists

A consulta exists identifica documentos onde um campo específico está presente (ou seja, não é nulo ou ausente).

Exemplo: Encontrar todos os usuários que forneceram um endereço de e-mail.

GET /users/_search
{
  "query": {
    "exists": {
      "field": "email_address"
    }
  }
}

Construindo Lógica Complexa com a Consulta bool

Para praticamente todas as aplicações de pesquisa do mundo real, você precisa combinar múltiplos critérios. A consulta bool é a ferramenta essencial para isso, permitindo combinar outras cláusulas de consulta usando lógica booleana.

Cláusulas dentro de bool

A consulta bool aceita quatro cláusulas principais:

  1. must: Todas as cláusulas dentro deste array devem corresponder. Cláusulas em must contribuem para a pontuação de relevância.
  2. filter: Todas as cláusulas dentro deste array devem corresponder, mas são executadas em um contexto sem pontuação. Isso as torna muito mais rápidas para critérios estritos de inclusão/exclusão.
  3. should: Pelo menos uma cláusula neste array deveria corresponder. Essas cláusulas influenciam a pontuação de relevância, mas são opcionais para a correspondência.
  4. must_not: Nenhuma das cláusulas neste array deve corresponder (o equivalente a um NOT lógico).

Exemplo Prático de Consulta bool

Vamos combinar vários conceitos para encontrar documentos de alta prioridade que mencionem 'security', mas excluam rascunhos e estejam disponíveis na região 'US'.

GET /logs/_search
{
  "query": {
    "bool": {
      "must": [
        {
          "match": {
            "content": "security breach"
          }
        }
      ],
      "filter": [
        {
          "term": {
            "region.keyword": "US"
          }
        }
      ],
      "should": [
        {
          "term": {
            "priority": 5
          }
        }
      ],
      "must_not": [
        {
          "term": {
            "status.keyword": "DRAFT"
          }
        }
      ]
    }
  }
}

Explicação do Exemplo:

  • Must: O documento deve conter a frase "security breach" no campo de conteúdo analisado.
  • Filter: O documento deve ser marcado para a região 'US' (uma correspondência exata e rápida).
  • Should: Documentos correspondentes a priority: 5 receberão um aumento em sua pontuação de relevância, mas documentos com prioridades mais baixas que atendam às cláusulas must e filter ainda serão retornados.
  • Must Not: Documentos marcados como 'DRAFT' são estritamente excluídos.

Melhores Práticas para Construção de Consultas

Para garantir que suas pesquisas sejam precisas e eficientes, siga estas diretrizes:

  • Prefira filter em vez de must para critérios sem pontuação. Se você está apenas verificando inclusão/exclusão (por exemplo, filtrando por ID, data exata ou status), sempre use a cláusula filter dentro de uma consulta bool. Isso aproveita o cache e evita cálculos de pontuação caros.
  • Use Consultas Exatas com Sabedoria: Para campos mapeados como text (analisados), use match. Para campos mapeados como keyword (não analisados), use term ou consultas de intervalo.
  • Evite Aninhamento Profundo: Embora possível, consultas bool profundamente aninhadas podem se tornar difíceis de ler e depurar, e às vezes podem levar à degradação do desempenho.
  • Aproveite minimum_should_match: Para cláusulas should, definir minimum_should_match (por exemplo, para 1 ou 2) força um certo número desses critérios opcionais a serem atendidos, transformando-os efetivamente em critérios obrigatórios, enquanto ainda permite que contribuam para a pontuação.

O Mapeamento Decide Qual Consulta Faz Sentido

A maioria dos erros no Query DSL começa com o mapeamento. Uma consulta pode parecer correta e ainda assim retornar resultados confusos se o campo estiver mapeado de forma diferente do que você pensa.

Um padrão comum é um campo de texto com um subcampo de palavra-chave:

{
  "mappings": {
    "properties": {
      "title": {
        "type": "text",
        "fields": {
          "keyword": {
            "type": "keyword"
          }
        }
      },
      "status": { "type": "keyword" },
      "created_at": { "type": "date" },
      "price": { "type": "double" }
    }
  }
}

Use match em title quando quiser o comportamento de texto completo analisado. Use term em title.keyword quando precisar do valor exato do título. Use term em status porque já é uma palavra-chave. Use range em created_at ou price porque esses campos são valores de data e numéricos.

Se uma consulta term em um campo de texto não funcionar como esperado, o problema geralmente é a análise. Os tokens armazenados podem estar em minúsculas, divididos, com radical ou alterados de outra forma. Verifique o mapeamento antes de alterar a consulta.

GET /products/_mapping

Para problemas de análise de texto, _analyze é útil:

GET /products/_analyze
{
  "field": "description",
  "text": "Cloud Computing"
}

Isso mostra quais tokens o Elasticsearch irá pesquisar.

match, match_phrase e multi_match

match é a consulta de texto completo do dia a dia, mas não é a única que você usará.

Use match_phrase quando a ordem das palavras for importante:

GET /products/_search
{
  "query": {
    "match_phrase": {
      "description": "wireless charging stand"
    }
  }
}

Isso é útil para nomes de produtos, mensagens de log, títulos de documentos e frases onde a sequência exata carrega significado. É mais restritivo que match, então pode retornar menos documentos.

Use multi_match quando a mesma entrada do usuário deve pesquisar vários campos:

GET /products/_search
{
  "query": {
    "multi_match": {
      "query": "noise cancelling headphones",
      "fields": ["title^3", "description", "brand^2"]
    }
  }
}

Os boosts ^3 e ^2 dizem ao Elasticsearch que correspondências em title e brand devem contar mais do que correspondências em description. O boosting não é uma garantia de que um documento será classificado em primeiro lugar; é uma dica de pontuação. Teste com consultas reais antes de ajustar os boosts de forma muito agressiva.

Paginação Sem Prejudicar o Cluster

Os parâmetros básicos from e size são adequados para paginação rasa:

GET /products/_search
{
  "from": 20,
  "size": 10,
  "query": {
    "match": {
      "description": "laptop sleeve"
    }
  }
}

Paginação profunda é diferente. Pedir a página 1.000 força o Elasticsearch a classificar e pular muitos resultados. Para pesquisas voltadas ao usuário, evite paginação profunda ilimitada. Para exportações ou varreduras em segundo plano, use search_after com uma classificação estável:

GET /products/_search
{
  "size": 100,
  "sort": [
    { "created_at": "asc" },
    { "_id": "asc" }
  ],
  "search_after": ["2025-01-10T12:00:00Z", "abc123"],
  "query": {
    "term": {
      "status": "active"
    }
  }
}

Os valores em search_after vêm do array sort do último resultado na resposta anterior. Essa abordagem é mais estável para percorrer grandes conjuntos de resultados.

Filtragem de Origem Mantém as Respostas Úteis

O desempenho da pesquisa não é apenas a execução da consulta. Retornar documentos enormes pode tornar o cliente, a rede e o nó coordenador mais lentos. Se a interface do usuário precisar apenas de alguns campos, solicite esses campos:

GET /orders/_search
{
  "_source": ["order_id", "customer_id", "total", "created_at", "status"],
  "query": {
    "bool": {
      "filter": [
        { "term": { "status": "paid" } },
        { "range": { "created_at": { "gte": "now-7d/d" } } }
      ]
    }
  }
}

Isso torna a resposta mais fácil de ler e pode reduzir o tamanho do payload. Não substitui um bom design de índice, mas ajuda quando os documentos contêm descrições grandes, blobs de metadados ou arrays aninhados que a página atual não precisa.

Classificação e Agregações Precisam dos Campos Certos

Classificar em texto analisado geralmente é um erro. Classifique em campos de palavra-chave, numéricos ou de data:

GET /products/_search
{
  "sort": [
    { "price": "asc" },
    { "title.keyword": "asc" }
  ],
  "query": {
    "term": {
      "status": "active"
    }
  }
}

O mesmo se aplica a muitas agregações. Se você quiser contagens por status, agregue em um campo de palavra-chave:

GET /orders/_search
{
  "size": 0,
  "aggs": {
    "orders_by_status": {
      "terms": {
        "field": "status"
      }
    }
  },
  "query": {
    "range": {
      "created_at": {
        "gte": "now-30d/d"
      }
    }
  }
}

size: 0 diz ao Elasticsearch que você só quer resultados de agregação, não documentos correspondentes. É um pequeno hábito que mantém as respostas mais limpas.

Depurar Consultas Com explain e profile

Quando um resultado é classificado de forma estranha, use explain em um único documento:

GET /products/_explain/SKU10021
{
  "query": {
    "match": {
      "description": "cloud computing"
    }
  }
}

Quando uma consulta é lenta, use profile em um teste de não produção ou produção cuidadosamente controlada:

GET /products/_search
{
  "profile": true,
  "query": {
    "bool": {
      "must": [
        { "match": { "description": "cloud computing" } }
      ],
      "filter": [
        { "term": { "status": "active" } }
      ]
    }
  }
}

A saída do perfil é detalhada, mas pode mostrar se o tempo é gasto em uma consulta de texto, um filtro, um script ou outra parte da requisição. Não deixe a criação de perfil habilitada no código da aplicação; use-a como uma ferramenta de depuração.

Um Hábito Sensato de Construção de Consultas

Para a maioria das pesquisas de aplicação, construa a requisição nesta ordem:

  1. Coloque restrições exatas em filter: ID do locatário, status, região, janela de data, permissões.
  2. Coloque o texto inserido pelo usuário em must com match, match_phrase ou multi_match.
  3. Use should para preferências de classificação, não requisitos rígidos, a menos que você defina minimum_should_match.
  4. Limite _source aos campos que o chamador precisa.
  5. Adicione uma classificação estável se paginação ou exportações forem importantes.
  6. Verifique o mapeamento antes de culpar o Elasticsearch.

O Query DSL é poderoso porque separa filtragem, pontuação, classificação e modelagem de resposta. Depois que você mantém esses trabalhos separados, as consultas se tornam mais fáceis de ler, mais fáceis de ajustar e menos surpreendentes em produção.

Um Pequeno Exemplo de Solução de Problemas

Suponha que um usuário pesquise por ACME-1000 e não obtenha resultado, mesmo que o produto exista. Não adicione curingas imediatamente. Primeiro verifique o mapeamento. Se sku for uma keyword, isso deve funcionar:

GET /products/_search
{
  "query": {
    "term": {
      "sku": "ACME-1000"
    }
  }
}

Se sku foi acidentalmente mapeado como text, a análise pode ter dividido ou alterado o valor. Você ainda pode consultá-lo em alguns casos, mas a melhor correção geralmente é uma alteração de mapeamento para índices futuros. Identificadores exatos, status, regiões e IDs de locatários devem ser campos do tipo palavra-chave. Descrições e títulos escritos por humanos devem ser campos de texto. O Query DSL fica muito mais fácil quando o mapeamento corresponde à maneira como as pessoas realmente recuperam os dados.