<?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/cn/search-labs/author/martijn-laarman</link>
    </image>
    <link>https://www.elastic.co/cn/search-labs/author/martijn-laarman</link>
    <atom:link href="https://www.elastic.co/cn/search-labs/rss/author/martijn-laarman.xml" rel="self" type="application/rss+xml"/>
    <language><![CDATA[cn]]></language>
    <lastBuildDate>Fri, 18 Sep 2026 22:38:47 GMT</lastBuildDate>
  <item>
    <title><![CDATA[LINQ to Elasticsearch ES|QL：编写 C# 代码，查询 Elasticsearch]]></title>
    <description><![CDATA[探索 Elasticsearch .NET 客户端中全新的 LINQ to Elasticsearch ES|QL 提供程序。借助该程序，您可以编写会自动转换为 ES|QL 查询的 C# 代码。]]></description>
    <content:encoded><![CDATA[<p>从 <strong>v9.3.4</strong> 和 <strong>v8.19.18</strong> 开始，Elasticsearch .NET 客户端包含一个<a href="https://learn.microsoft.com/en-us/dotnet/csharp/linq/">语言集成查询 (LINQ) </a>提供程序，可在运行时将 C# LINQ 表达式转换为 <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/esql.html">Elasticsearch 查询语言 (ES|QL)</a> 查询。您可以使用<code>Where</code>、<code>Select</code>、<code>OrderBy</code>、<code>GroupBy</code> 和其他标准操作符来编写查询，而无需手工编写 ES|QL 字符串。提供程序负责转换、参数化和结果反序列化，包括按行流式传输，无论结果集大小如何，都能保持稳定的内存使用量。</p><h2>您的第一个查询</h2><p>首先定义一个映射到 Elasticsearch 索引的普通旧 CLR 对象 (POCO)。属性名称通过标准<code>System.Text.Json</code> 属性（如<code>[JsonPropertyName]</code>）或配置的<code>JsonNamingPolicy</code> 解析为 ES|QL 列名。适用于客户端其他部分的<a href="https://www.elastic.co/docs/reference/elasticsearch/clients/dotnet/source-serialization">源序列化</a>规则在这里也同样适用。</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>类型设置完成后，查询语句如下所示：</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>该提供程序将此转换为以下 ES|QL：</p><p>需要注意的一些细节：</p><ul><li><p><strong>属性名称解析：</strong>由于 <code>[JsonPropertyName]</code> 属性，<code>p.Price</code> 变成了 <code>price_usd</code>，根据默认 camelCase 命名策略，<code>p.Brand</code> 变成 <code>brand</code>。</p></li><li><p><strong>参数捕获：</strong>C# 变量 <code>minPrice</code> 和 <code>brand</code> 被捕获为命名参数 (<code>?minPrice</code>，<code>?brand</code>)。它们与 JSON 有效负载中的查询字符串分开发送，这样可以防止注入，并实现服务器端查询计划缓存。</p></li><li><p><strong>流式传输：</strong><code>QueryAsync&lt;T&gt;</code> 返回 <code>IAsyncEnumerable&lt;T&gt;</code>。从 Elasticsearch 返回数据时，数据会逐行具体化。</p></li></ul><p>您还可以在不执行的情况下检查生成的查询及其参数：</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>这如何运作？快速回顾一下 LINQ</h2><p>使 LINQ 提供程序成为可能的机制是 <code>IEnumerable&lt;T&gt;</code> 和 <code>IQueryable&lt;T&gt;</code> 之间的区别。</p><p>在 <code>IEnumerable&lt;T&gt;</code> 上调用 <code>.Where(p =&gt; p.Price &gt; 100)</code> 时，lambda 会编译为 <code>Func&lt;Product, bool&gt;</code>，即一个由运行时在进程内执行的常规委托。这就是 LINQ-to-Objects。</p><p>当您在<code>IQueryable&lt;T&gt;</code> 上调用相同的方法时，C# 编译器会将 lambda 封装在<code>Expression&lt;Func&lt;Product, bool&gt;&gt;</code> 中。这是一种数据结构，表示代码的<em>结构</em>，而不是代码的可执行形式。在运行时，该表达式树可被检查、分析，并转换为另一种语言。</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><code>IQueryProvider</code> 接口是扩展点。任何提供程序均可通过实现 <code>CreateQuery&lt;T&gt;</code> 和 <code>Execute&lt;T&gt;</code>，将这些表达式树转换为目标语言。实体框架就是利用此机制生成 SQL 语句。LINQ to ES|QL 提供程序使用它来生成 ES|QL 查询。</p><p>上述查询的表达式树如下所示：</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt521838e8b9c36649/6a1705b1839dfa5f40dcfdfe/f864cd18a390831f8d28503a29b5835efb1842f7-1000x720.png" alt="示例查询的表达式树。" /><p><em>示例查询的表达式树。</em></p><p>此表达式树由内而外嵌套：<code>Take</code> 包裹着 <code>OrderByDescending</code>，它又包裹着 <code>Where</code>，而后者再包裹着 <code>From</code>，而最内层是根节点 <code>EsqlQueryable&lt;Product&gt;</code> 常量。对于 <code>&amp;&amp;</code>、<code>&gt;=</code> 和 <code>==</code> 这几种操作符而言，<code>Where</code> 谓词本身是一个由 <code>BinaryExpression</code> 个节点构成的子树，其中包含 <code>MemberExpression</code> 个叶子节点，这些叶子节点用于属性访问，以及对 <code>minPrice</code> 和 <code>brand</code> 变量的闭包捕获。提供程序会遍历这一数据结构，从而生成最终的 ES|QL 查询。</p><h2>深入了解：转换管道</h2><p>从 LINQ 表达式到查询结果的路径遵循六阶段管道：</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt930670a505dd61ea/6a1705b3b339d58a54769ecf/2a2c772b63d720f61fc9a28b2f85668fa2db8d38-1999x1036.png" alt="转换管道概述。" /><p><em>转换管道概述。</em></p><h3>1. 表达式树捕获</h3><p>当在一个 <code>IQueryable&lt;T&gt;</code> 对象上串联使用 <code>.Where()</code>、<code>.OrderBy()</code>、<code>.Take()</code> 及其他操作符时，标准的 LINQ 基础架构会构建一个表达式树。<code>EsqlQueryable&lt;T&gt;</code> 实现了 <code>IQueryable&lt;T&gt;</code> 接口，并将处理委托给 <code>EsqlQueryProvider</code>。</p><h3>2. 翻译</h3><p>当查询被执行 (通过枚举、调用 <code>ToList()</code>，或使用 <code>await foreach)</code> 时)，<code>EsqlExpressionVisitor</code>自内而外遍历表达式树。它会将每个 LINQ 方法调用分派给一个专门的访问器进行处理：</p><p>访客</p><p>翻译</p><p>进入</p><p>WhereClauseVisitor</p><p>.Where(predicate)</p><p>WHERE 条件</p><p>SelectProjectionVisitor</p><p>.Select(selector)</p><p>评估 + 保留 + 重命名</p><p>GroupByVisitor</p><p>.GroupBy().Select()</p><p>统计信息 ... 依据</p><p>OrderByVisitor</p><p>.OrderBy() / .ThenBy()</p><p>SORT 字段 [ASC\|DESC]</p><p>EsqlFunctionTranslator</p><p>EsqlFunctions.*、Math.*、字符串方法</p><p>80+ ES|QL 函数</p><p>在翻译过程中，表达式中引用的 C# 变量被捕获为命名参数。</p><h3>3. 查询模型</h3><p>访问器不会直接生成字符串。相反，它们会产生 <code>QueryCommand</code> 对象，一个不可变的中间表征。一个 <code>FromCommand</code>、一个 <code>WhereCommand</code>、一个 <code>SortCommand</code> 和一个 <code>LimitCommand</code>，各代表一条 ES|QL 处理命令。这些数据被收集到<code>EsqlQuery</code> 模型中。</p><p></p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt788c9936976f2f62/6a1705b50e2e4910da419ff0/2adc349b6cf655b96b7b3e826a134e8a17fe42fd-1999x1036.png" alt="查询模型和命令模式。" /><p><em>查询模型和命令模式。</em></p><p>该中间模型与表达式树和输出格式均解耦。它可以被检查、拦截（通过 <code>IEsqlQueryInterceptor</code>）或在格式化前进行修改。</p><h3>4. 格式化</h3><p><code>EsqlFormatter</code> 依次访问每个<code>QueryCommand</code> ，并生成最终的 ES|QL 字符串。每条命令占一行，通过 ES|QL 中用于串联处理命令的管道 (|) 运算符分隔。若标识符包含特殊字符，系统会自动用反引号进行转义处理。</p><h3>5. 执行</h3><p>格式化后的 ES|QL 查询字符串及捕获的参数会以 JSON 数据载荷的形式发送至 Elasticsearch 的 <code>/_query</code> 终端。而 <code>IEsqlQueryExecutor</code> 接口则对传输层进行了抽象封装，这正是分层包架构发挥作用的关键环节。</p><h3>6. 实现</h3><p><code>EsqlResponseReader</code> 流式传输JSON响应，但不会将整个结果集缓冲到内存中。以流式方式传输 JSON 响应数据，无需将整个结果集缓存至内存。针对每次查询预先计算生成的 <code>ColumnLayout</code> 树结构，会将扁平化的 ES|QL 列名（如 <code>address.street</code>、<code>address.city</code>）映射到嵌套的 POCO 属性。每行数据会被组装为 <code>T</code> 实例，并通过 <code>IEnumerable&lt;T&gt;</code> 或 <code>IAsyncEnumerable&lt;T&gt;</code> 逐个返回。</p><h2>分层架构</h2><p>LINQ to ES|QL 功能分为三个软件包：</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt662bd0dd8861b6b6/6a1705b7a929cf7086ae08a2/41b8aae860ecdc2480edcb1c1d4cc9b03cfb78c9-1999x1036.png" alt="软件包架构。" /><p><em>软件包架构。</em><a href="https://www.nuget.org/packages/Elastic.Esql"><strong><code>Elastic.Esql</code></strong></a> 是纯转换引擎。该组件完全不依赖 HTTP 协议栈，集成了表达式访问器、查询模型、格式化器及响应解析器等核心模块。您可独立使用它来构建和检查 ES|QL 查询（无需连接 Elasticsearch），这在测试验证、查询日志记录或自定义执行层开发等场景中极具实用价值。翻译要点解析：</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|QL 客户端。该组件通过 <code>Elastic.Transport</code> 在 <code>Elastic.Esql</code> 之上扩展了 HTTP 协议执行能力。如果您的应用程序仅需使用 ES|QL 而无需其他 Elasticsearch API，此方案可实现最小化依赖集成。</p><p><a href="https://www.nuget.org/packages/Elastic.Clients.Elasticsearch"><strong><code>Elastic.Clients.Elasticsearch</code></strong></a> 是完整的 Elasticsearch.NET 客户端。它还建立在<code>Elastic.Esql</code> 的基础上，并通过<code>client.Esql</code> 命名空间公开 LINQ 提供程序接口。这是大多数应用程序的推荐入口点。</p><p>两个执行层组件包均提供了针对 <code>IEsqlQueryExecutor</code> 接口的独立实现。该策略接口作为转换与传输层的桥梁。</p><p>当与源码生成的 <code>JsonSerializerContext</code> 配合使用时，这三个组件包均支持原生 AOT 编译。如需完整客户端集成方案，请参阅<a href="https://www.elastic.co/docs/reference/elasticsearch/clients/dotnet/source-serialization#native-aot">原生 AOT 文档</a>。</p><h2>不只使用基础功能</h2><p>上面的例子涵盖了筛选、排序和分页。该提供程序支持更广泛的操作范围。</p><h3>聚合</h3><p><code>GroupBy</code>结合 <code>Select</code> 中的聚合函数，转换为 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>投影</h3><p><code>Select</code>，使用匿名类型生成 <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> 和 <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>丰富的函数库</h3><p>通过 <code>EsqlFunctions</code> 类，可以使用超过 80 个 ES|QL 函数，涵盖日期/时间、字符串、数学、IP、模式匹配和评分。标准 <code>Math.*</code> 和 <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>查找连接</h3><p>跨索引查找转换为 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>原生 ES|QL 直通接口</h3><p>对于 LINQ 提供程序尚未涵盖的 ES|QL 功能，您可以添加原始片段：</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>服务器端异步查询</h3><p>对于长时间运行的查询，可将其提交给服务器进行后台处理：</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>服务器端异步查询对于长时间运行的分析型查询/大规模数据集处理尤其有用，这类操作可能会超出常规的超时阈值；在存在负载均衡器、API 网关或代理（这些组件会强制执行严格的 HTTP 超时设置）的超时敏感环境中，异步查询同样优势显著。异步查询通过将查询提交与结果获取解耦，避免了连接中断的情况。</p><h2>开始使用</h2><p>LINQ to ES|QL 自以下版本起可用：</p><ul><li><p><strong>Elastic.Clients.Elasticsearch v9.3.4</strong>（9.x 分支）</p></li><li><p><strong>Elastic.Clients.Elasticsearch v8.19.18</strong>（8.x 分支）</p></li></ul><p>从 NuGet 安装：</p><p><code>dotnet add package Elastic.Clients.Elasticsearch</code></p><p>入口点位于<code>client.Esql</code>上：</p><p>方法</p><p>返回值</p><p>用例</p><p>查询&lt;T&gt;(...)</p><p>IEnumerable&lt;T&gt;</p><p>同步执行</p><p>QueryAsync&lt;T&gt;(...)</p><p>IAsyncEnumerable&lt;T&gt;</p><p>异步流式处理</p><p>CreateQuery&lt;T&gt;()</p><p>IEsqlQueryable&lt;T&gt;</p><p>高级结构分析和检测</p><p>SubmitAsyncQueryAsync&lt;T&gt;(...)</p><p>EsqlAsyncQuery&lt;T&gt;</p><p>长时间运行的服务器端查询</p><p>有关完整的功能参考，包括查询选项、多字段访问、嵌套对象和多值字段处理，请参阅<a href="https://www.elastic.co/docs/reference/elasticsearch/clients/dotnet/linq-to-esql">LINQ to ES|QL 文档</a>。</p><h2>结论</h2><p>LINQ 转 ES|QL 将 C# LINQ 的强大表达能力引入到 Elasticsearch 的 ES|QL 查询语言中，让您无需手工编写查询字符串，就能生成强类型、可组合的查询。它具备自动参数捕获、流式物化功能，还拥有分层式的软件包架构，既能满足独立转换需求，也能适配完整的 Elasticsearch 客户端，可自然融入任意规模的 .NET 应用程序。安装最新客户端，将 LINQ 表达式指向索引，剩下的就交给该提供程序来处理。</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[向量数据库]]></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>