<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0">
  <channel>
    <title><![CDATA[Martijn Laarman - Elasticsearch Labs]]></title>
    <description><![CDATA[Articles and tutorials from the Search team at Elastic]]></description>
    <copyright><![CDATA[© 2026. Elasticsearch B.V. All Rights Reserved]]></copyright>
    <image>
      <title><![CDATA[Martijn Laarman - Elasticsearch Labs]]></title>
      <url>https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt1121c0bf0e8a6e65/6a88da6340a1841030ef456f/search-labs-thumbnail.png</url>
      <link>https://www.elastic.co/es/search-labs/author/martijn-laarman</link>
    </image>
    <link>https://www.elastic.co/es/search-labs/author/martijn-laarman</link>
    <atom:link href="https://www.elastic.co/es/search-labs/rss/author/martijn-laarman.xml" rel="self" type="application/rss+xml"/>
    <language><![CDATA[es]]></language>
    <lastBuildDate>Sun, 20 Sep 2026 20:02:29 GMT</lastBuildDate>
  <item>
    <title><![CDATA[LINQ a Elasticsearch ES|QL: escribir en C#, buscar en Elasticsearch]]></title>
    <description><![CDATA[Explorar el nuevo proveedor de LINQ a Elasticsearch ES|QL en el cliente .NET de Elasticsearch, que te permite escribir código en C# que se traduce automáticamente en búsquedas ES|QL.]]></description>
    <content:encoded><![CDATA[<p>A partir de <strong>v9.3.4</strong> y <strong>v8.19.18</strong>, el cliente de Elasticsearch para .NET incluye un <a href="https://learn.microsoft.com/en-us/dotnet/csharp/linq/">proveedor de Language Integrated Query (LINQ) </a>que traduce las expresiones LINQ de C# a búsquedas del <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/esql.html">lenguaje de búsqueda de Elasticsearch (ES|QL)</a> en tiempo de ejecución. En lugar de escribir textos de ES|QL manualmente, compones búsquedas con <code>Where</code>, <code>Select</code>, <code>OrderBy</code>, <code>GroupBy</code> 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.</p><h2>Tu primera búsqueda</h2><p>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 <code>System.Text.Json</code> estándar, como <code>[JsonPropertyName]</code>, o a través de un <code>JsonNamingPolicy</code> configurado. Las mismas reglas de <a href="https://www.elastic.co/docs/reference/elasticsearch/clients/dotnet/source-serialization">serialización de origen</a> que se aplican en el resto del cliente también se aplican aquí.</p>using System.Text.Json.Serialization;

public class Product
{
    [JsonPropertyName("product_id")]
    public string Id { get; set; }

    public string Name { get; set; }

    public string Brand { get; set; }

    [JsonPropertyName("price_usd")]
    public double Price { get; set; }

    [JsonPropertyName("in_stock")]
    public bool InStock { get; set; }
}<p>Con el tipo ya definido, una consulta se ve así:</p>var minPrice = 100.0;
var brand = "TechCorp";

await foreach (var product in client.Esql.QueryAsync&lt;Product&gt;(q =&gt; q
    .From("products")
    .Where(p =&gt; p.InStock &amp;&amp; p.Price &gt;= minPrice &amp;&amp; p.Brand == brand)
    .OrderByDescending(p =&gt; p.Price)
    .Take(10)))
{
    Console.WriteLine($"{product.Name}: ${product.Price}");
}<p>El proveedor traduce esto al siguiente ES|QL:</p><p>Algunos detalles a tener en cuenta:</p><ul><li><p><strong>Resolución de nombres de propiedades:</strong> <code>p.Price</code> se vuelve <code>price_usd</code> debido al atributo <code>[JsonPropertyName]</code>, y <code>p.Brand</code> se convierte en <code>brand</code> siguiendo la política predeterminada de nombres camelCase.</p></li><li><p><strong>Captura de parámetros:</strong> Las variables C# <code>minPrice</code> y <code>brand</code> se capturan como parámetros nombrados (<code>?minPrice</code>, <code>?brand</code>). 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.</p></li><li><p><strong>Streaming:</strong> <code>QueryAsync&lt;T&gt;</code> devuelve <code>IAsyncEnumerable&lt;T&gt;</code>. Las filas se materializan una a la vez a medida que llegan desde Elasticsearch.</p></li></ul><p>También puedes inspeccionar la búsqueda generada y sus parámetros sin ejecutarla:</p>var query = client.Esql.CreateQuery&lt;Product&gt;()
    .Where(p =&gt; p.InStock &amp;&amp; p.Price &gt;= minPrice &amp;&amp; p.Brand == brand)
    .OrderByDescending(p =&gt; p.Price)
    .Take(10);

Console.WriteLine(query.ToEsqlString());
// FROM products | WHERE (in_stock == true AND price_usd &gt;= 100) | SORT price_usd DESC | LIMIT 10

Console.WriteLine(query.ToEsqlString(inlineParameters: false));
// FROM products | WHERE (in_stock == true AND price_usd &gt;= ?minPrice AND brand == ?brand) | SORT price_usd DESC | LIMIT 10

var parameters = query.GetParameters();
// { "minPrice": 100.0, "brand": "TechCorp" }<h2>¿Cómo funciona esto? Un repaso rápido de LINQ</h2><p>El mecanismo que hace posibles los proveedores LINQ es la distinción entre <code>IEnumerable&lt;T&gt;</code> y <code>IQueryable&lt;T&gt;</code>.</p><p>Cuando llamas a <code>.Where(p =&gt; p.Price &gt; 100)</code> en un <code>IEnumerable&lt;T&gt;</code>, la lambda se compila en un <code>Func&lt;Product, bool&gt;</code>, un delegado común que el runtime ejecuta en proceso. Esto es LINQ a objetos.</p><p>Cuando llamas al mismo método en un <code>IQueryable&lt;T&gt;</code>, el compilador de C# encapsula la expresión lambda en un <code>Expression&lt;Func&lt;Product, bool&gt;&gt;</code> en su lugar. Esta es una estructura de datos que representa la <em>estructura</em> 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.</p>// IEnumerable: the lambda is a compiled delegate
IEnumerable&lt;Product&gt; local = products.Where(p =&gt; p.Price &gt; 100);

// IQueryable: the lambda is an expression tree, a data structure
IQueryable&lt;Product&gt; remote = queryable.Where(p =&gt; p.Price &gt; 100);<p>La interfaz <code>IQueryProvider</code> es el punto de extensión. Cualquier proveedor puede implementar <code>CreateQuery&lt;T&gt;</code> y <code>Execute&lt;T&gt;</code> 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.</p><p>El árbol de expresión para la búsqueda anterior se ve así:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt521838e8b9c36649/6a1705b1839dfa5f40dcfdfe/f864cd18a390831f8d28503a29b5835efb1842f7-1000x720.png" alt="Árbol de expresiones para la búsqueda de ejemplo." /><p><em>Árbol de expresiones para la búsqueda de ejemplo.</em></p><p>El árbol está anidado al revés: <code>Take</code> envuelve <code>OrderByDescending</code>, que envuelve <code>Where</code>, que envuelve <code>From</code>, que envuelve la constante raíz <code>EsqlQueryable&lt;Product&gt;</code>. El predicado <code>Where</code> es en sí mismo un subárbol de <code>BinaryExpression</code> nodos para los operadores <code>&amp;&amp;</code>, <code>&gt;=</code> y <code>==</code>, con <code>MemberExpression</code> hojas para accesos a propiedades y capturas de cierre para las variables <code>minPrice</code> y <code>brand</code>. Esta es la estructura de datos que el proveedor recorre para producir el ES|QL final.</p><h2>En detalle: el pipeline de traducción</h2><p>La ruta de una expresión LINQ a los resultados de la búsqueda sigue un pipeline de seis etapas:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt930670a505dd61ea/6a1705b3b339d58a54769ecf/2a2c772b63d720f61fc9a28b2f85668fa2db8d38-1999x1036.png" alt="Visión general del pipeline de traducción." /><p><em>Visión general del pipeline de traducción.</em></p><h3>1. Captura del árbol de expresiones</h3><p>Cuando se encadenan <code>.Where()</code>, <code>.OrderBy()</code>, <code>.Take()</code> y otros operadores en un <code>IQueryable&lt;T&gt;</code>, la infraestructura LINQ estándar crea un árbol de expresiones. <code>EsqlQueryable&lt;T&gt;</code> implementa <code>IQueryable&lt;T&gt;</code> y delega a <code>EsqlQueryProvider</code>.</p><h3>2. Traducción</h3><p>Cuando se ejecuta la búsqueda (al enumerar, llamar a <code>ToList()</code> o usar <code>await foreach)</code>, <code>EsqlExpressionVisitor</code> recorre el árbol de expresiones de adentro hacia afuera. Envía cada llamada al método LINQ a un visitante especializado:</p><p>Visitante</p><p>Traduce</p><p>En</p><p>whereClauseVisitor</p><p>.Where(predicado)</p><p>Condición WHERE</p><p>SelectProjectionVisitor</p><p>.Select(selector)</p><p>EVAL + KEEP + RENAME</p><p>GroupByVisitor</p><p>.GroupBy().Select()</p><p>STATS ... BY</p><p>OrderByVisitor</p><p>.OrderBy() / .ThenBy()</p><p>Campo SORT [ASC\|DESC]</p><p>EsqlFunctionTranslator</p><p>EsqlFunctions.*, Math.*, métodos de texto</p><p>Más de 80 funciones ES|QL</p><p>Durante la traducción, las variables de C# a las que se hace referencia en las expresiones se capturan como parámetros con nombre.</p><h3>3. Modelo de búsqueda</h3><p>Los visitantes no producen textos directamente. En cambio, producen objetos <code>QueryCommand</code>, una representación intermedia inmutable. Un <code>FromCommand</code>, un <code>WhereCommand</code>, un <code>SortCommand</code> y un <code>LimitCommand</code>, cada uno representa un comando de procesamiento de ES|QL. Estos se recopilan en un modelo <code>EsqlQuery</code>.</p><p></p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt788c9936976f2f62/6a1705b50e2e4910da419ff0/2adc349b6cf655b96b7b3e826a134e8a17fe42fd-1999x1036.png" alt="Modelo de búsqueda y patrón de comandos." /><p><em>Modelo de búsqueda y patrón de comandos.</em></p><p>Este modelo intermedio está desacoplado tanto del árbol de expresiones como del formato de salida. Se puede inspeccionar, interceptar (vía <code>IEsqlQueryInterceptor</code>) o modificar antes de dar formato.</p><h3>4. Formato</h3><p><code>EsqlFormatter</code> visita cada <code>QueryCommand</code> 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.</p><h3>5. Ejecución</h3><p>El texto ES|QL formateado y los parámetros capturados se envían al endpoint <code>/_query</code> de Elasticsearch como carga útil JSON. La interfaz <code>IEsqlQueryExecutor</code> abstrae la capa de transporte, que es donde entra en juego la arquitectura de paquetes en capas.</p><h3>6. Materialización</h3><p><code>EsqlResponseReader</code> transmite la respuesta JSON sin almacenar en memoria todo el conjunto de resultados. Un árbol <code>ColumnLayout</code>, precomputado una vez por búsqueda, mapea nombres de columnas planas de ES|QL (como <code>address.street</code>, <code>address.city</code>) a propiedades anidadas de POCO. Cada fila se ensambla en una instancia <code>T</code> y se genera una a la vez a través de <code>IEnumerable&lt;T&gt;</code> o <code>IAsyncEnumerable&lt;T&gt;</code>.</p><h2>La arquitectura en capas</h2><p>La funcionalidad de LINQ a ES|QL se divide en tres paquetes:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt662bd0dd8861b6b6/6a1705b7a929cf7086ae08a2/41b8aae860ecdc2480edcb1c1d4cc9b03cfb78c9-1999x1036.png" alt="Arquitectura de paquetes." /><p><em>Arquitectura de paquetes.</em>
<a href="https://www.nuget.org/packages/Elastic.Esql"><strong><code>Elastic.Esql</code></strong></a> 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.</p>// Translation-only: no Elasticsearch connection needed
var provider = new EsqlQueryProvider();
var query = new EsqlQueryable&lt;Product&gt;(provider)
    .From("products")
    .Where(p =&gt; p.InStock)
    .OrderByDescending(p =&gt; p.Price);

Console.WriteLine(query.ToEsqlString());
// FROM products | WHERE in_stock == true | SORT price_usd DESC<p><a href="https://www.nuget.org/packages/Elastic.Clients.Esql"><strong><code>Elastic.Clients.Esql</code></strong></a> es un cliente ES|QL ligero e independiente. Añade ejecución HTTP sobre <code>Elastic.Esql</code> a través de <code>Elastic.Transport</code>. 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.</p><p><a href="https://www.nuget.org/packages/Elastic.Clients.Elasticsearch"><strong><code>Elastic.Clients.Elasticsearch</code></strong></a> es el cliente completo de Elasticsearch .NET. También se basa en <code>Elastic.Esql</code> y expone al proveedor LINQ a través del espacio de nombres <code>client.Esql</code>. Este es el punto de entrada recomendado para la mayoría de las aplicaciones.</p><p>Ambos paquetes de capa de ejecución proporcionan su propia implementación de <code>IEsqlQueryExecutor</code>, la interfaz estratégica que une la traducción y el transporte.</p><p>Los tres paquetes son compatibles con Native AOT cuando se usan con un <code>JsonSerializerContext</code> generado por el código fuente. Para el cliente completo, consulta la <a href="https://www.elastic.co/docs/reference/elasticsearch/clients/dotnet/source-serialization#native-aot">documentación de Native AOT</a>.</p><h2>Mas allá de los conceptos básicos</h2><p>El ejemplo anterior cubrió el filtrado, la clasificación y la paginación. El proveedor admite un conjunto más amplio de operaciones.</p><h3>Agregaciones</h3><p><code>GroupBy</code>, combinado con funciones agregadas en <code>Select</code>, se traduce a ES|QL <a href="https://www.elastic.co/docs/reference/query-languages/esql/commands/stats-by"><code>STATS ... BY</code></a>:</p>var stats = client.Esql.Query&lt;Product, object&gt;(q =&gt; q
    .GroupBy(p =&gt; p.Brand)
    .Select(g =&gt; new
    {
        Brand = g.Key,
        Count = g.Count(),
        AvgPrice = g.Average(p =&gt; p.Price),
        MaxPrice = g.Max(p =&gt; p.Price)
    }));

// -&gt; FROM products | STATS COUNT(*), AVG(price_usd), MAX(price_usd) BY brand<h3>Proyecciones</h3><p><code>Select</code>, con tipos anónimos genera comandos <a href="https://www.elastic.co/docs/reference/query-languages/esql/commands/eval"><code>EVAL</code></a>, <a href="https://www.elastic.co/docs/reference/query-languages/esql/commands/keep"><code>KEEP</code></a>, y <a href="https://www.elastic.co/docs/reference/query-languages/esql/commands/rename"><code>RENAME</code></a>:</p>var query = client.Esql.CreateQuery&lt;Product&gt;()
    .Select(p =&gt; new { ProductName = p.Name, p.Price, p.InStock });

// -&gt; FROM products | KEEP name, price_usd, in_stock | RENAME name AS ProductName<h3>Biblioteca de funciones enriquecida</h3><p>Hay más de 80 funciones ES|QL disponibles a través de la clase <code>EsqlFunctions</code>, que abarcan fecha/hora, texto, matemáticas, IP, coincidencia de patrones y puntuación. También se traducen los métodos estándar <code>Math.*</code> y <code>string.*</code>:</p>.Where(p =&gt; p.Name.Contains("Pro"))       // -&gt; WHERE name LIKE "*Pro*"
.Where(p =&gt; EsqlFunctions.CidrMatch(      // -&gt; WHERE CIDR_MATCH(ip, "10.0.0.0/8")
    p.IpAddress, "10.0.0.0/8"))<h3>LOOKUP JOIN</h3><p>Las consultas cruzadas de índices se traducen a ES|QL <a href="https://www.elastic.co/docs/reference/query-languages/esql/commands/lookup-join"><code>LOOKUP JOIN</code></a>:</p>var enriched = client.Esql.Query&lt;Product, object&gt;(q =&gt; q
    .LookupJoin&lt;Product, CategoryLookup, string, object&gt;(
        "category-lookup-index",
        product =&gt; product.Id,
        category =&gt; category.CategoryId,
        (product, category) =&gt; new { product.Name, category!.CategoryLabel }));<h3>Acceso directo a ES|QL sin procesar</h3><p>Para las características de ES|QL que aún no están cubiertas por el proveedor de LINQ, puedes anexar fragmentos sin procesar:</p>var results = client.Esql.Query&lt;Product&gt;(q =&gt; q
    .Where(p =&gt; p.InStock)
    .RawEsql("| EVAL discounted = price_usd * 0.9"));<h3>Búsquedas asíncronas del lado del servidor</h3><p>Para búsquedas de ejecución prolongada, envíalas para procesamiento en segundo plano en el servidor:</p>await using var asyncQuery = await client.Esql.SubmitAsyncQueryAsync&lt;Product&gt;(
    q =&gt; q.Where(p =&gt; p.InStock),
    asyncQueryOptions: new EsqlAsyncQueryOptions
    {
        WaitForCompletionTimeout = TimeSpan.FromSeconds(5),
        KeepAlive = TimeSpan.FromMinutes(10)
    });

await asyncQuery.WaitForCompletionAsync();
await foreach (var product in asyncQuery.AsAsyncEnumerable())
    Console.WriteLine(product.Name);<p>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.</p><h2>Primeros pasos</h2><p>LINQ a ES|QL está disponible a partir de:</p><ul><li><p><strong>Elastic.Clients.Elasticsearch v9.3.4</strong> (rama 9.x)</p></li><li><p><strong>Elastic.Clients.Elasticsearch v8.19.18</strong> (rama 8.x)</p></li></ul><p>Instalar desde NuGet:</p><p><code>dotnet add package Elastic.Clients.Elasticsearch</code></p><p>Los puntos de entrada están en <code>client.Esql</code>:</p><p>Método</p><p>Devuelve</p><p>Caso de uso</p><p>Query&lt;T&gt;(...)</p><p>IEnumerable&lt;T&gt;</p><p>Ejecución sincrónica</p><p>QueryAsync&lt;T&gt;(...)</p><p>IAsyncEnumerable&lt;T&gt;</p><p>Transmisión asíncrona</p><p>CreateQuery&lt;T&gt;()</p><p>IEsqlQueryable&lt;T&gt;</p><p>Composición e inspección avanzadas</p><p>SubmitAsyncQueryAsync&lt;T&gt;(...)</p><p>EsqlAsyncQuery&lt;T&gt;</p><p>Búsquedas de larga ejecución del lado del servidor</p><p>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 <a href="https://www.elastic.co/docs/reference/elasticsearch/clients/dotnet/linq-to-esql">documentación de LINQ a ES|QL</a>.</p><h2>Conclusión</h2><p>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.</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/linq-esql-c-elasticsearch-net-client</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/linq-esql-c-elasticsearch-net-client</guid>
    <category><![CDATA[ES|QL]]></category>
    <category><![CDATA[Base de datos vectorial]]></category>
    <dc:creator><![CDATA[Florian Bernd,Martijn Laarman]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltdfa35fbcbbf4959f/6a1705b9dc55de19a4e00d07/e54132e915217063e9ed0ec45059c6cfc38e31dd-1280x720.png" length="0" type="image/png"/>
    <pubDate>Wed, 01 Apr 2026 00:00:00 GMT</pubDate>
  </item>
  </channel>
</rss>