LINQ a ES|QL: escribe código C#, busca en Elasticsearch

Explorar el nuevo proveedor LINQ a ES|QL en el cliente .NET de Elasticsearch, que te permite escribir código C# que se traduce automáticamente a búsquedas de ES|QL.

A partir de v9.3.4 y v8.19.18, el cliente de Elasticsearch para .NET incluye un proveedor de Language Integrated Query (LINQ) que traduce las expresiones LINQ de C# a búsquedas del lenguaje de búsqueda de Elasticsearch (ES|QL) en tiempo de ejecución. En lugar de escribir textos de ES|QL manualmente, compones búsquedas con Where, Select, OrderBy, GroupBy y otros operadores estándar. El proveedor se encarga de la traducción, la parametrización y la deserialización de los resultados, incluido el streaming fila por fila, lo que mantiene el uso de memoria constante independientemente del tamaño del conjunto de resultados.

Tu primera búsqueda

Comienza por definir un objeto CLR (POCO) simple que se mapea a tu índice de Elasticsearch. Los nombres de las propiedades se resuelven a nombres de columnas ES|QL a través de atributos System.Text.Json estándar, como [JsonPropertyName], o a través de un JsonNamingPolicy configurado. Las mismas reglas de serialización de origen que se aplican en el resto del cliente también se aplican aquí.

Con el tipo ya definido, una consulta se ve así:

El proveedor traduce esto al siguiente ES|QL:

Algunos detalles a tener en cuenta:

  • Resolución de nombres de propiedades: p.Price se vuelve price_usd debido al atributo [JsonPropertyName], y p.Brand se convierte en brand siguiendo la política predeterminada de nombres camelCase.
  • Captura de parámetros: Las variables C# minPrice y brand se capturan como parámetros nombrados (?minPrice, ?brand). Se envían por separado del texto de búsqueda en la carga útil JSON, lo que previene la inyección y habilita el almacenamiento en caché del plan de búsqueda del lado del servidor.
  • Streaming: QueryAsync<T> devuelve IAsyncEnumerable<T>. Las filas se materializan una a la vez a medida que llegan desde Elasticsearch.

También puedes inspeccionar la búsqueda generada y sus parámetros sin ejecutarla:

¿Cómo funciona esto? Un repaso rápido de LINQ

El mecanismo que hace posibles los proveedores LINQ es la distinción entre IEnumerable<T> y IQueryable<T>.

Cuando llamas a .Where(p => p.Price > 100) en un IEnumerable<T>, la lambda se compila en un Func<Product, bool>, un delegado común que el runtime ejecuta en proceso. Esto es LINQ a objetos.

Cuando llamas al mismo método en un IQueryable<T>, el compilador de C# encapsula la expresión lambda en un Expression<Func<Product, bool>> en su lugar. Esta es una estructura de datos que representa la estructura del código en lugar de su forma ejecutable. El árbol de expresión puede inspeccionarse, analizarse y traducirse a otro idioma en tiempo de ejecución.

La interfaz IQueryProvider es el punto de extensión. Cualquier proveedor puede implementar CreateQuery<T> y Execute<T> para traducir estos árboles de expresiones a un idioma destino. Entity Framework usa esto para emitir SQL. El proveedor de LINQ a ES|QL lo usa para emitir ES|QL.

El árbol de expresión para la búsqueda anterior se ve así:

Árbol de expresiones para la búsqueda de ejemplo.

El árbol está anidado al revés: Take envuelve OrderByDescending, que envuelve Where, que envuelve From, que envuelve la constante raíz EsqlQueryable<Product>. El predicado Where es en sí mismo un subárbol de BinaryExpression nodos para los operadores &&, >= y ==, con MemberExpression hojas para accesos a propiedades y capturas de cierre para las variables minPrice y brand. Esta es la estructura de datos que el proveedor recorre para producir el ES|QL final.

En detalle: el pipeline de traducción

La ruta de una expresión LINQ a los resultados de la búsqueda sigue un pipeline de seis etapas:

Visión general del pipeline de traducción.

1. Captura del árbol de expresiones

Cuando se encadenan .Where(), .OrderBy(), .Take() y otros operadores en un IQueryable<T>, la infraestructura LINQ estándar crea un árbol de expresiones. EsqlQueryable<T> implementa IQueryable<T> y delega a EsqlQueryProvider.

2. Traducción

Cuando se ejecuta la búsqueda (al enumerar, llamar a ToList() o usar await foreach), EsqlExpressionVisitor recorre el árbol de expresiones de adentro hacia afuera. Envía cada llamada al método LINQ a un visitante especializado:

VisitanteTraduceEn
whereClauseVisitor.Where(predicado)Condición WHERE
SelectProjectionVisitor.Select(selector)EVAL + KEEP + RENAME
GroupByVisitor.GroupBy().Select()STATS ... BY
OrderByVisitor.OrderBy() / .ThenBy()Campo SORT [ASC\|DESC]
EsqlFunctionTranslatorEsqlFunctions.*, Math.*, métodos de textoMás de 80 funciones ES|QL

Durante la traducción, las variables de C# a las que se hace referencia en las expresiones se capturan como parámetros con nombre.

3. Modelo de búsqueda

Los visitantes no producen textos directamente. En cambio, producen objetos QueryCommand, una representación intermedia inmutable. Un FromCommand, un WhereCommand, un SortCommand y un LimitCommand, cada uno representa un comando de procesamiento de ES|QL. Estos se recopilan en un modelo EsqlQuery.

Modelo de búsqueda y patrón de comandos.

Este modelo intermedio está desacoplado tanto del árbol de expresiones como del formato de salida. Se puede inspeccionar, interceptar (vía IEsqlQueryInterceptor) o modificar antes de dar formato.

4. Formato

EsqlFormatter visita cada QueryCommand en orden y produce el texto final de ES|QL. Cada comando se convierte en una línea, separada por el operador de barra vertical (|) que ES|QL usa para encadenar comandos de procesamiento. Los identificadores que contienen caracteres especiales se escapan automáticamente con comillas invertidas.

5. Ejecución

El texto ES|QL formateado y los parámetros capturados se envían al endpoint /_query de Elasticsearch como carga útil JSON. La interfaz IEsqlQueryExecutor abstrae la capa de transporte, que es donde entra en juego la arquitectura de paquetes en capas.

6. Materialización

EsqlResponseReader transmite la respuesta JSON sin almacenar en memoria todo el conjunto de resultados. Un árbol ColumnLayout, precomputado una vez por búsqueda, mapea nombres de columnas planas de ES|QL (como address.street, address.city) a propiedades anidadas de POCO. Cada fila se ensambla en una instancia T y se genera una a la vez a través de IEnumerable<T> o IAsyncEnumerable<T>.

La arquitectura en capas

La funcionalidad de LINQ a ES|QL se divide en tres paquetes:

Arquitectura de paquetes.
Elastic.Esql es el motor puro de traducción. No tiene dependencias HTTP y contiene los visitantes de expresiones, el modelo de búsqueda, el formateador y el lector de respuestas. Puedes usarlo de forma independiente para crear e inspeccionar búsquedas de ES|QL sin una conexión de Elasticsearch, lo que es útil para pruebas, logging de búsquedas o para crear tu propia capa de ejecución.

Elastic.Clients.Esql es un cliente ES|QL ligero e independiente. Añade ejecución HTTP sobre Elastic.Esql a través de Elastic.Transport. Si tu aplicación solo necesita ES|QL y ninguna de las otras API de Elasticsearch, esta es la opción de dependencia mínima.

Elastic.Clients.Elasticsearch es el cliente completo de Elasticsearch .NET. También se basa en Elastic.Esql y expone al proveedor LINQ a través del espacio de nombres client.Esql. Este es el punto de entrada recomendado para la mayoría de las aplicaciones.

Ambos paquetes de capa de ejecución proporcionan su propia implementación de IEsqlQueryExecutor, la interfaz estratégica que une la traducción y el transporte.

Los tres paquetes son compatibles con Native AOT cuando se usan con un JsonSerializerContext generado por el código fuente. Para el cliente completo, consulta la documentación de Native AOT.

Mas allá de los conceptos básicos

El ejemplo anterior cubrió el filtrado, la clasificación y la paginación. El proveedor admite un conjunto más amplio de operaciones.

Agregaciones

GroupBy, combinado con funciones agregadas en Select, se traduce a ES|QL STATS ... BY:

Proyecciones

Select, con tipos anónimos genera comandos EVAL, KEEP, y RENAME:

Biblioteca de funciones enriquecida

Hay más de 80 funciones ES|QL disponibles a través de la clase EsqlFunctions, que abarcan fecha/hora, texto, matemáticas, IP, coincidencia de patrones y puntuación. También se traducen los métodos estándar Math.* y string.*:

LOOKUP JOIN

Las consultas cruzadas de índices se traducen a ES|QL LOOKUP JOIN:

Acceso directo a ES|QL sin procesar

Para las características de ES|QL que aún no están cubiertas por el proveedor de LINQ, puedes anexar fragmentos sin procesar:

Búsquedas asíncronas del lado del servidor

Para búsquedas de ejecución prolongada, envíalas para procesamiento en segundo plano en el servidor:

Las búsquedas asíncronas del lado del servidor son especialmente útiles para búsquedas analíticas de larga duración/procesamiento de grandes sets de datos que pueden superar los umbrales típicos de tiempo de espera, o en entornos sensibles al tiempo de espera con balanceadores de carga, gateways API o proxies que imponen tiempos de espera HTTP estrictos. Las búsquedas asíncronas evitan las caídas de conexión al separar el envío de la solicitud de la recuperación de los resultados.

Primeros pasos

LINQ a ES|QL está disponible a partir de:

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

Instalar desde NuGet:

dotnet add package Elastic.Clients.Elasticsearch

Los puntos de entrada están en client.Esql:

MétodoDevuelveCaso de uso
Query<T>(...)IEnumerable<T>Ejecución sincrónica
QueryAsync<T>(...)IAsyncEnumerable<T>Transmisión asíncrona
CreateQuery<T>()IEsqlQueryable<T>Composición e inspección avanzadas
SubmitAsyncQueryAsync<T>(...)EsqlAsyncQuery<T>Búsquedas de larga ejecución del lado del servidor

Para consultar la referencia completa de características, incluidas las opciones de búsqueda, el acceso a múltiples campos, los objetos anidados y el manejo de campos de valores múltiples, consulta la documentación de LINQ a ES|QL.

Conclusión

LINQ a ES|QL aporta toda la expresividad de LINQ de C# al lenguaje de búsqueda ES|QL de Elasticsearch, lo que te permite realizar búsquedas con tipado fuerte y combinables sin crear manualmente cadenas de texto. Con captura automática de parámetros, materialización de streaming y una arquitectura de paquetes en capas que escala desde el paquete de traducción autónomo hasta el cliente completo de Elasticsearch, se adapta naturalmente a aplicaciones .NET de cualquier tamaño. Instala el cliente más reciente, dirige tus expresiones LINQ a un índice y deja que el proveedor se encargue del resto.

¿Te ha sido útil este contenido?

No es útil

Algo útil

Muy útil

Contenido relacionado

¿Estás listo para crear experiencias de búsqueda de última generación?

No se logra una búsqueda suficientemente avanzada con los esfuerzos de uno. Elasticsearch está impulsado por científicos de datos, operaciones de ML, ingenieros y muchos más que son tan apasionados por la búsqueda como tú. Conectemos y trabajemos juntos para crear la experiencia mágica de búsqueda que te dará los resultados que deseas.

Pruébalo tú mismo