作者:来自 Elastic Florian Bernd 及 Martijn Laarman

探索 Elasticsearch .NET 客户端中新推出的 LINQ 到 ES|QL 提供 程序 ,它允许你编写 C# 代码并自动转换为 ES|QL 查询。
动手体验 Elasticsearch:深入探索 Elasticsearch Labs repo 中的示例 notebooks,开始免费的 cloud 试用,或立即在本地机器上试用 Elastic。
从 v9.3.4 和 v8.19.18 开始,Elasticsearch 的 .NET 客户端包含一个 Language Integrated Query(LINQ)提供程序,可在运行时将 C# LINQ 表达式 转换 为 Elasticsearch Query Language(ES|QL)查询。你无需手动编写 ES|QL 字符串,而是使用 Where、Select、OrderBy、GroupBy 等标准操作符来构建查询。该提供程序负责处理转换、参数化以及结果反序列化,包括逐行流式处理,从而在结果集规模变化时仍保持内存使用恒定。
你的第一个查询
首先定义一个普通的 CLR 对象(POCO),用于映射到你的 Elasticsearch 索引。属性名称通过标准的 System.Text.Json 特性(如 [JsonPropertyName])或配置的 JsonNamingPolicy 映射为 ES|QL 列名。与客户端其他部分一致的序列化规则在这里同样适用。
csharp
`
1. using System.Text.Json.Serialization;
3. public class Product
4. {
5. [JsonPropertyName("product_id")]
6. public string Id { get; set; }
8. public string Name { get; set; }
10. public string Brand { get; set; }
12. [JsonPropertyName("price_usd")]
13. public double Price { get; set; }
15. [JsonPropertyName("in_stock")]
16. public bool InStock { get; set; }
17. }
`AI写代码
在定义好类型之后,一个查询如下所示:
ini
`
1. var minPrice = 100.0;
2. var brand = "TechCorp";
4. await foreach (var product in client.Esql.QueryAsync<Product>(q => q
5. .From("products")
6. .Where(p => p.InStock && p.Price >= minPrice && p.Brand == brand)
7. .OrderByDescending(p => p.Price)
8. .Take(10)))
9. {
10. Console.WriteLine($"{product.Name}: ${product.Price}");
11. }
`AI写代码
该提供程序会将其转换为以下 ES|QL:
sql
`
1. FROM products
2. | WHERE (in_stock == true AND price_usd >= ?minPrice AND brand == ?brand)
3. | SORT price_usd DESC
4. | LIMIT 10
`AI写代码
需要注意的几点:
- 属性名称解析:p.Price 由于 [JsonPropertyName] 属性会被转换为 price_usd,而 p.Brand 则根据默认的 camelCase 命名策略转换为 brand。
- 参数捕获:C# 变量 minPrice 和 brand 会被捕获为命名参数(?minPrice、?brand)。它们会作为 JSON 负载的一部分与查询字符串分开发送,从而防止注入并支持服务器端查询计划缓存。
- 流式处理:QueryAsync 返回 IAsyncEnumerable。当数据从 Elasticsearch 返回时,结果会逐行被实例化。
你还可以在不执行查询的情况下检查生成的查询及其参数:
ini
`
1. var query = client.Esql.CreateQuery<Product>()
2. .Where(p => p.InStock && p.Price >= minPrice && p.Brand == brand)
3. .OrderByDescending(p => p.Price)
4. .Take(10);
6. Console.WriteLine(query.ToEsqlString());
7. // FROM products | WHERE (in_stock == true AND price_usd >= 100) | SORT price_usd DESC | LIMIT 10
9. Console.WriteLine(query.ToEsqlString(inlineParameters: false));
10. // FROM products | WHERE (in_stock == true AND price_usd >= ?minPrice AND brand == ?brand) | SORT price_usd DESC | LIMIT 10
12. var parameters = query.GetParameters();
13. // { "minPrice": 100.0, "brand": "TechCorp" }
`AI写代码
这是如何工作的?LINQ 快速回顾
使 LINQ 提供程序成为可能的机制在于 IEnumerable 和 IQueryable 之间的区别。
当你在 IEnumerable 上调用 .Where(p => p.Price > 100) 时,该 lambda 会被编译为 Func<Product, bool>,这是一个普通的委托,由运行时在进程内执行。这就是 LINQ-to-Objects。
当你在 IQueryable 上调用相同方法时,C# 编译器会将该 lambda 包装为 Expression<Func<Product, bool>>。这是一种数据结构,用于表示代码的结构,而不是其可执行形式。表达式树可以在运行时被检查、分析并转换为另一种语言。
ini
`
1. // IEnumerable: the lambda is a compiled delegate
2. IEnumerable<Product> local = products.Where(p => p.Price > 100);
4. // IQueryable: the lambda is an expression tree, a data structure
5. IQueryable<Product> remote = queryable.Where(p => p.Price > 100);
`AI写代码
IQueryProvider 接口是扩展点。任何提供程序都可以实现 CreateQuery 和 Execute,将这些表达式树转换为目标语言。Entity Framework 使用它来生成 SQL,而 LINQ 到 ES|QL 提供程序则使用它来生成 ES|QL。
上述查询的表达式树如下所示:

示例查询的表达式树。
该树是由内向外嵌套的:Take 包裹 OrderByDescending,后者包裹 Where,再包裹 From,最终包裹根节点 EsqlQueryable 常量。Where 谓词本身也是一个子树,由表示 &&、>= 和 == 运算符的 BinaryExpression 节点组成,其叶子节点为用于属性访问的 MemberExpression,以及用于捕获 minPrice 和 brand 变量的闭包。这正是提供程序遍历以生成最终 ES|QL 的数据结构。
底层机制:转换管道
从 LINQ 表达式到查询结果的路径遵循一个六阶段管道:
转换管道概览。
1)表达式树捕获
当你在 IQueryable 上链式调用 .Where()、.OrderBy()、.Take() 和其他操作符时,标准 LINQ 基础设施会构建一个表达式树。EsqlQueryable 实现了 IQueryable 并将操作委托给 EsqlQueryProvider。
2)转换
当查询被执行(通过枚举、调用 ToList() 或使用 await foreach)时,EsqlExpressionVisitor 会从内向外遍历表达式树。它将每个 LINQ 方法调用分派给一个专门的访问器:
| Visitor | 翻译 | 成 |
|---|---|---|
| WhereClauseVisitor | .Where(predicate) | WHERE condition |
| SelectProjectionVisitor | .Select(selector) | EVAL + KEEP + RENAME |
| GroupByVisitor | .GroupBy().Select() | STATS ... BY |
| OrderByVisitor | .OrderBy() / .ThenBy() | SORT field [ASC\|DESC] |
| EsqlFunctionTranslator | EsqlFunctions.*, Math.*, string methods | 80+ ES|QL functions |
在转换过程中,表达式中引用的 C# 变量会被捕获为命名参数。
3)查询模型
访问器不会直接生成字符串。相反,它们会生成 QueryCommand 对象,这是一个不可变的中间表示。包括 FromCommand、WhereCommand、SortCommand 和 LimitCommand,每个代表一个 ES|QL 处理命令。这些命令被收集到一个 EsqlQuery 模型中。
查询模型与命令模式。
这个中间模型与表达式树和最终输出格式解耦。它可以在格式化之前进行检查、拦截(通过 IEsqlQueryInterceptor)或修改。
4. 格式化
EsqlFormatter 按顺序访问每个 QueryCommand 并生成最终的 ES|QL 字符串。每条命令成为一行,并用 ES|QL 用于链式处理的管道符号 (|) 分隔。包含特殊字符的标识符会自动用反引号转义。
5. 执行
格式化后的 ES|QL 字符串以及捕获的参数被作为 JSON payload 发送到 Elasticsearch 的 /_query 端点。IEsqlQueryExecutor 接口抽象了传输层,这也是分层包架构发挥作用的地方。
6. 物化
EsqlResponseReader 以流式方式读取 JSON 响应,而不会将整个结果集缓存在内存中。ColumnLayout 树在每次查询时预先计算,将扁平化的 ES|QL 列名(如 address.street、address.city)映射到嵌套的 POCO 属性。每一行都会被组装成 T 实例,并通过 IEnumerable<T> 或 IAsyncEnumerable<T> 一次返回一条。
分层架构
LINQ 到 ES|QL 的功能分布在三个包中:
包架构
Elastic.Esql 是纯翻译引擎。它没有任何 HTTP 依赖,并包含 expression visitors、 query model、formatter 和 response reader。你可以独立使用它来构建和检查 ES|QL 查询而无需 Elasticsearch 连接,这对于测试、查询记录或构建你自己的执行层非常有用。
ini
`
1. // Translation-only: no Elasticsearch connection needed
2. var provider = new EsqlQueryProvider();
3. var query = new EsqlQueryable<Product>(provider)
4. .From("products")
5. .Where(p => p.InStock)
6. .OrderByDescending(p => p.Price);
8. Console.WriteLine(query.ToEsqlString());
9. // FROM products | WHERE in_stock == true | SORT price_usd DESC
`AI写代码
Elastic.Clients.Esql 是轻量级独立 ES|QL 客户端。它通过 Elastic.Transport 在 Elastic.Esql 之上增加 HTTP 执行。如果你的应用只需要 ES|QL 而不需要其他 Elasticsearch API,这是最小依赖选项。
Elastic.Clients.Elasticsearch 是完整的 Elasticsearch .NET 客户端。它同样基于 Elastic.Esql 并通过 client .Esql 命名空间提供 LINQ provider。这是大多数应用推荐的入口点。
两个执行层包都提供自己的 IEsqlQueryExecutor 实现,这是连接翻译和 transport 的策略接口。
当与源生成的 JsonSerializerContext 一起使用时,所有三个包都兼容 Native AOT。完整客户端请参阅 Native AOT 文档。
基础之外
上面的示例涵盖了过滤、排序和分页。provider 支持更广泛的操作集。
聚合
GroupBy,结合 Select 中的 aggregate functions,可翻译为 ES|QL STATS ... BY:
ini
`
1. var stats = client.Esql.Query<Product, object>(q => q
2. .GroupBy(p => p.Brand)
3. .Select(g => new
4. {
5. Brand = g.Key,
6. Count = g.Count(),
7. AvgPrice = g.Average(p => p.Price),
8. MaxPrice = g.Max(p => p.Price)
9. }));
11. // -> FROM products | STATS COUNT(*), AVG(price_usd), MAX(price_usd) BY brand
`AI写代码
投影
Select,使用匿名类型会生成 EVAL、KEEP 和 RENAME 命令:
css
`
1. var query = client.Esql.CreateQuery<Product>()
2. .Select(p => new { ProductName = p.Name, p.Price, p.InStock });
4. // -> FROM products | KEEP name, price_usd, in_stock | RENAME name AS ProductName
`AI写代码
丰富的函数库
通过 EsqlFunctions 类提供超过 80 个 ES|QL 函数,涵盖 date/time、string、math、IP、pattern matching 和 scoring。标准 Math.* 和 string.* 方法也会被翻译:
sql
`
1. .Where(p => p.Name.Contains("Pro")) // -> WHERE name LIKE "*Pro*"
2. .Where(p => EsqlFunctions.CidrMatch( // -> WHERE CIDR_MATCH(ip, "10.0.0.0/8")
3. p.IpAddress, "10.0.0.0/8"))
`AI写代码
LOOKUP JOIN
跨索引查找可翻译为 ES|QL LOOKUP JOIN:
ini
`
1. var enriched = client.Esql.Query<Product, object>(q => q
2. .LookupJoin<Product, CategoryLookup, string, object>(
3. "category-lookup-index",
4. product => product.Id,
5. category => category.CategoryId,
6. (product, category) => new { product.Name, category!.CategoryLabel }));
`AI写代码
原生 ES|QL 逃生舱
对于 LINQ provider 尚未覆盖的 ES|QL 功能,你可以追加 raw fragments:
css
`
1. var results = client.Esql.Query<Product>(q => q
2. .Where(p => p.InStock)
3. .RawEsql("| EVAL discounted = price_usd * 0.9"));
`AI写代码
服务器端异步查询
对于长时间运行的查询,将它们提交到服务器进行后台处理:
ini
`
1. await using var asyncQuery = await client.Esql.SubmitAsyncQueryAsync<Product>(
2. q => q.Where(p => p.InStock),
3. asyncQueryOptions: new EsqlAsyncQueryOptions
4. {
5. WaitForCompletionTimeout = TimeSpan.FromSeconds(5),
6. KeepAlive = TimeSpan.FromMinutes(10)
7. });
9. await asyncQuery.WaitForCompletionAsync();
10. await foreach (var product in asyncQuery.AsAsyncEnumerable())
11. Console.WriteLine(product.Name);
`AI写代码
服务器端异步查询对于长时间运行的分析查询 / 大型数据集处理特别有用,这类查询可能超过典型的超时阈值,或者在带有负载均衡器、API gateways 或强制严格 HTTP 超时的代理的超时敏感环境中。异步查询通过将提交与结果检索分离,避免连接中断。
入门
LINQ to ES|QL 可从以下版本开始使用:
- Elastic.Clients.Elasticsearch v9.3.4 (9.x 分支)
- Elastic.Clients.Elasticsearch v8.19.18 (8.x 分支)
通过 NuGet 安装:
csharp
`dotnet add package Elastic.Clients.Elasticsearch`AI写代码
入口点在 client.Esql:
| Method | Returns | Use case |
|---|---|---|
| Query<T>(...) | IEnumerable<T> | Synchronous execution |
| QueryAsync<T>(...) | IAsyncEnumerable<T> | Async streaming |
| CreateQuery<T>() | IEsqlQueryable<T> | Advanced composition and inspection |
| SubmitAsyncQueryAsync<T>(...) | EsqlAsyncQuery<T> | Long-running server-side queries |
有关完整功能参考,包括查询选项、多字段访问、嵌套对象和多值字段处理,请参阅 LINQ to ES|QL 文档。
结论
LINQ to ES|QL 将 C# LINQ 的完整表达能力引入 Elasticsearch 的 ES|QL 查询语言,让你可以编写强类型、可组合的查询,而无需手工构建查询字符串。通过自动参数捕获、流式实体化以及从独立翻译到完整 Elasticsearch 客户端的分层包架构,它自然适用于任何规模的 .NET 应用。安装最新客户端,将你的 LINQ 表达式指向索引,让 provider 处理其余部分。