Do LINQ para ES|QL: escreva C# e consulte o Elasticsearch

Explorando o novo provedor LINQ para ES|QL no cliente Elasticsearch .NET, que permite escrever código C# que é automaticamente traduzido para consultas ES|QL.

A partir das versões 9.3.4 e 8.19.18, o cliente Elasticsearch .NET inclui um provedor de Consulta Integrada em Linguagem (LINQ) que traduz expressões LINQ em C# para a Linguagem de Consulta Elasticsearch (ES|QL) em tempo de execução. Em vez de escrever manualmente as strings ES|QL, você compõe consultas usando Where, Select, OrderBy, GroupBy e outros operadores padrão. O provedor cuida da tradução, parametrização e desserialização dos resultados, inclusive o streaming por linha que mantém o uso da memória constante, independentemente do tamanho do conjunto de resultados.

Sua primeira consulta

Comece definindo um objeto CLR simples (POCO) que mapeia para o seu índice Elasticsearch. Os nomes das propriedades são resolvidos para nomes de coluna ES|QL via atributos System.Text.Json padrão, como [JsonPropertyName], ou via JsonNamingPolicy configurado. As mesmas regras de serialização de origem que se aplicam ao restante do cliente também se aplicam aqui.

Com o tipo definido, uma consulta fica assim:

O provedor traduz isso para o seguinte ES|QL:

Há alguns detalhes a serem observados:

  • Resolução do nome da propriedade: p.Price se torna price_usd por causa do atributo [JsonPropertyName], e p.Brand se torna brand seguindo a política de nomenclatura padrão camelCase.
  • Captura de parâmetros: As variáveis C# minPrice e brand são capturadas como parâmetros nomeados (?minPrice, ?brand). Eles são enviados separadamente da string de consulta na carga JSON, o que evita injeções e permite o armazenamento em cache do plano de consulta no lado do servidor.
  • Streaming: QueryAsync<T> retorna IAsyncEnumerable<T>. As linhas são materializadas uma de cada vez à medida que chegam do Elasticsearch.

Você também pode inspecionar a consulta gerada e seus parâmetros sem executá-la:

Como funciona? Uma breve revisão sobre o LINQ

O mecanismo que torna possíveis os provedores LINQ é a distinção entre IEnumerable<T> e IQueryable<T>.

Quando você chama .Where(p => p.Price > 100) em um IEnumerable<T>, o lambda compila para um Func<Product, bool>, um delegado regular que o runtime executa em processo. Isto é LINQ-to-Objects.

Quando você chama o mesmo método em um IQueryable<T>, o compilador C# envolve o lambda em um Expression<Func<Product, bool>> em vez disso. Essa é uma estrutura de dados que representa a estrutura do código em vez de sua forma executável. A árvore de expressões pode ser inspecionada, analisada e traduzida para outra linguagem em tempo de execução.

A interface IQueryProvider é o ponto de extensão. Qualquer provedor pode implementar CreateQuery<T> e Execute<T> para traduzir essas árvores de expressão para um idioma de destino. O Entity Framework usa isso para emitir SQL. O provedor LINQ to ES|QL o usa para emitir ES|QL.

A árvore de expressões para a consulta acima fica assim:

Árvore de expressões para a consulta de exemplo.

A árvore é aninhada do avesso: Take envolve OrderByDescending, que envolve Where, que envolve From, que envolve a raiz EsqlQueryable<Product> constante. O predicado Where é ele próprio uma subárvore de BinaryExpression nós para os operadores &&, >= e ==, com folhas MemberExpression para acessos a propriedades e capturas de fechamento para as variáveis minPrice e brand. Essa é a estrutura de dados que o provedor percorre para produzir o ES|QL final.

Nos bastidores: O pipeline de tradução

O caminho de uma expressão LINQ até os resultados da consulta segue um pipeline de seis estágios:

Visão geral do pipeline de tradução.

1. Captura da árvore de expressão

Quando você encadeia .Where(), .OrderBy(), .Take() e outros operadores em um IQueryable<T>, a infraestrutura padrão do LINQ constrói uma árvore de expressões. EsqlQueryable<T> implementa IQueryable<T> e delega para EsqlQueryProvider.

2. Tradução

Quando a consulta é executada (enumerando, chamando ToList() ou usando await foreach)), o EsqlExpressionVisitor percorre a árvore de expressões de dentro para fora. Ele despacha cada chamada de método LINQ para um visitante especializado:

VisitanteTraduzPara
WhereClauseVisitor.Where(predicate)Condição ONDE
SelectProjectionVisitor.Select(selector)EVAL + KEEP + RENOMEAR
GroupByVisitor.GroupBy().Select()ESTATÍSTICAS ... POR
OrderByVisitor.OrderBy() / .ThenBy()Campo SORT [ASC\|DESC]
EsqlFunctionTranslatorEsqlFunctions.*, Math.*, métodos de string80+ funções ES|QL

Durante a tradução, as variáveis C# referenciadas em expressões são capturadas como parâmetros nomeados.

3. Modelo de consulta

Os visitantes não produzem diretamente as strings. Em vez disso, produzem QueryCommand objetos, uma representação intermediária imutável. Um FromCommand, um WhereCommand, um SortCommand, e um LimitCommand, cada um representando um comando de processamento ES|QL. Eles são coletados para um modelo EsqlQuery.

Modelo de consulta e padrão de comando.

Esse modelo intermediário é desacoplado tanto da árvore de expressão quanto do formato de saída. Ele pode ser inspecionado, interceptado (via IEsqlQueryInterceptor) ou modificado antes da formatação.

4. Formatação

EsqlFormatter visita cada QueryCommand em ordem e gera a string final do ES|QL. Cada comando se transforma em uma linha, separada pelo operador pipe (|), que o ES|QL utiliza para encadear comandos de processamento. Identificadores que contêm caracteres especiais são automaticamente escapados com backticks.

5. Execução

A string ES|QL formatada e os parâmetros capturados são enviados para o endpoint /_query do Elasticsearch no corpo da requisição, como JSON. A interface IEsqlQueryExecutor abstrai a camada de transporte, e é aí que a arquitetura de pacotes em camadas se aplica.

6. Materialização

EsqlResponseReader transmite a resposta JSON sem armazenar todo o conjunto de resultados na memória. Uma árvore ColumnLayout , pré-computada uma vez por consulta, mapeia ES|QL nomes de colunas (como address.street, address.city) para propriedades aninhadas do POCO. Cada linha é montada em uma instância T e gerada uma de cada vez via IEnumerable<T> ou IAsyncEnumerable<T>.

A arquitetura em camadas

A funcionalidade LINQ para ES|QL é dividida em três pacotes:

Arquitetura de pacotes.
Elastic.Esql é o motor de tradução puro. Ele não tem dependência HTTP e contém os visitantes de expressões, o modelo de consulta, o formatador e o leitor de resposta. Você pode usá-lo de forma independente para criar e inspecionar consultas ES|QL sem nenhuma conexão com o Elasticsearch. Isso é útil para testes, logging de consultas ou para criar a sua camada de execução.

Elastic.Clients.Esql é um cliente ES|QL leve e independente. Ele adiciona a execução HTTP além de Elastic.Esql via Elastic.Transport. Se sua aplicação só precisa do ES|QL e nenhuma das outras APIs do Elasticsearch, essa é a opção de dependência mínima.

Elastic.Clients.Elasticsearch é o cliente completo do Elasticsearch .NET. Também se baseia em Elastic.Esql e expõe o provedor LINQ via espaço de nome client.Esql. Esse é o ponto de entrada recomendado para a maioria das aplicações.

Ambos os pacotes da camada de execução fornecem a própria implementação do IEsqlQueryExecutor, a interface estratégica que conecta tradução e transporte.

Todos os três pacotes são compatíveis com o Native AOT quando usados com um JsonSerializerContext gerado por fonte. Para as informações completas sobre o cliente, consulte a documentação do Native AOT.

Além do básico

O exemplo acima abordou filtragem, classificação e paginação. O provedor aceita um conjunto mais amplo de operações.

Agregações

GroupBy, combinado com funções agregadas em Select, traduz-se em ES|QL STATS ... BY:

Projeções

Select, com tipos anônimos gera os comandos EVAL, KEEP e RENAME:

Biblioteca repleta de funções

Mais de 80 funções ES|QL estão disponíveis via classe EsqlFunctions, cobrindo data/hora, string, matemática, IP, correspondência de padrões e pontuação. Métodos Math.* padrão e string.* também são traduzidos:

PESQUISAR ENTRAR

Consultas cruzadas de índice traduzem-se para ES|QL LOOKUP JOIN:

Acesso direto ao ES|QL bruto

Para recursos do ES|QL que ainda não são cobertos pelo provedor LINQ, você pode adicionar fragmentos brutos:

Consultas assíncronas do lado do servidor

Para consultas de longa duração, envie-as para processamento em segundo plano no servidor:

Consultas assíncronas do lado do servidor são úteis principalmente em consultas analíticas de longa duração/processamento de grandes conjuntos de dados que podem exceder os tempos-limite típicos, ou em ambientes sensíveis a tempo-limite com balanceadores de carga, gateways de API ou proxies que impõem tempos-limite de HTTP rigorosos. Consultas assíncronas evitam quedas de conexão ao separar o envio da obtenção dos resultados.

Para começar

LINQ to ES|QL está disponível a partir de:

  • Elastic.Clients.Elasticsearch v9.3.4 (9.x branch)
  • Elastic.Clients.Elasticsearch v8.19.18 (8.x branch)

Instale do NuGet:

dotnet add package Elastic.Clients.Elasticsearch

Os pontos de entrada estão em client.Esql:

MétodoReturnsCaso de uso
Query<T>(...)IEnumerable<T>Execução síncrona
QueryAsync<T>(...)IAsyncEnumerable<T>Streaming assíncrono
CreateQuery<T>()IEsqlQueryable<T>Composição avançada e inspeção
SubmitAsyncQueryAsync<T>(...)EsqlAsyncQuery<T>Consultas de longa duração no servidor

Para obter a referência completa de recursos, incluindo opções de consulta, acesso a vários campos, objetos aninhados e tratamento de campos de vários valores, consulte a documentação do LINQ to ES|QL.

Conclusão

Do LINQ para ES|QL traz toda a expressividade do C# LINQ para a linguagem de consulta ES|QL do Elasticsearch, para que você escreva consultas componíveis e com tipagem forte sem precisar criar manualmente as strings de consulta. Com captura automática de parâmetros, materialização em streaming e arquitetura de pacotes em camadas que se adapta de traduções independentes ao cliente completo do Elasticsearch, ele se integra naturalmente a aplicações .NET de qualquer tamanho. Instale o cliente mais recente, direcione suas expressões LINQ para um índice e deixe o provedor cuidar do resto.

Quão útil foi este conteúdo?

Não útil

Um pouco útil

Muito útil

Conteúdo relacionado

Pronto para criar buscas de última geração?

Uma pesquisa suficientemente avançada não se consegue apenas com o esforço de uma só pessoa. O Elasticsearch é impulsionado por cientistas de dados, especialistas em operações de aprendizado de máquina, engenheiros e muitos outros que são tão apaixonados por buscas quanto você. Vamos nos conectar e trabalhar juntos para construir a experiência de busca mágica que lhe trará os resultados desejados.

Experimente você mesmo(a)