深入了解Elasticsearch:您可以在Elasticsearch Labs仓库中查看我们的示例笔记本,开启免费的云试用,或在本地机器上尝试Elastic。
从v9.3.4和v8.19.18版本开始,Elasticsearch的.NET客户端引入了一种语言集成查询(LINQ)提供程序,它可以在运行时将C#的LINQ表达式转换为Elasticsearch查询语言(ES|QL)查询。您无需手动编写ES|QL字符串,而是使用Where、Select、OrderBy、GroupBy等标准操作符来组合查询。提供程序负责翻译、参数化和结果反序列化,包括每行流式处理,无论结果集大小如何,都能保持内存使用恒定。
首先定义一个普通的CLR对象(POCO),它映射到您的Elasticsearch索引。属性名称通过标准的System.Text.Json属性(如[JsonPropertyName])或配置的JsonNamingPolicy解析为ES|QL列名。适用于客户端其他部分的源序列化规则在此同样适用。
1
2
3
4
5
6
7
8
9
10
11
12
13
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; }
}
定义类型后,查询如下所示:
1
2
3
4
5
6
7
8
9
10
11
var minPrice = 100.0;
var brand = "TechCorp";
await foreach (var product in client.Esql.QueryAsync<Product>(q => q
.From("products")
.Where(p => p.InStock && p.Price >= minPrice && p.Brand == brand)
.OrderByDescending(p => p.Price)
.Take(10)))
{
Console.WriteLine($"{product.Name}: ${product.Price}");
}
提供程序将其转换为以下ES|QL:
1
2
3
4
FROM products
| WHERE (in_stock == true AND price_usd >= ?minPrice AND brand == ?brand)
| SORT price_usd DESC
| LIMIT 10
几点注意事项:
[JsonPropertyName]属性,p.Price被解析为price_usd,而p.Brand则根据默认的camelCase命名策略解析为brand。minPrice和brand被捕获为命名参数(?minPrice,?brand)。它们与查询字符串分开在JSON负载中发送,这可以防止注入并启用服务器端查询计划缓存。QueryAsync<T>返回IAsyncEnumerable<T>。行在从Elasticsearch到达时逐一实例化。您还可以在不执行查询的情况下检查生成的查询及其参数:
1
2
3
4
5
6
7
8
9
10
11
12
13
var query = client.Esql.CreateQuery<Product>()
.Where(p => p.InStock && p.Price >= minPrice && p.Brand == brand)
.OrderByDescending(p => p.Price)
.Take(10);
Console.WriteLine(query.ToEsqlString());
// FROM products | WHERE (in_stock == true AND price_usd >= 100) | SORT price_usd DESC | LIMIT 10
Console.WriteLine(query.ToEsqlString(inlineParameters: false));
// FROM products | WHERE (in_stock == true AND price_usd >= ?minPrice AND brand == ?brand) | SORT price_usd DESC | LIMIT 10
var parameters = query.GetParameters();
// { "minPrice": 100.0, "brand": "TechCorp" }
使LINQ提供程序成为可能的机制是IEnumerable<T>和IQueryable<T>之间的区别。
当您在IEnumerable<T>上调用.Where(p => p.Price > 100)时,lambda编译为Func<Product, bool>,这是一个常规的委托,在运行时在进程内执行。这是LINQ-to-Objects。
当您在IQueryable<T>上调用相同的方法时,C#编译器将lambda包装在Expression<Func<Product, bool>>中。这是一个表示代码结构而不是其可执行形式的数据结构。表达式树可以在运行时被检查、分析和转换为另一种语言。
1
2
3
4
5
// IEnumerable: lambda是一个编译的委托
IEnumerable<Product> local = products.Where(p => p.Price > 100);
// IQueryable: lambda是一个表达式树,是一个数据结构
IQueryable<Product> remote = queryable.Where(p => p.Price > 100);
IQueryProvider接口是扩展点。任何提供程序都可以实现CreateQuery<T>和Execute<T>来将这些表达式树翻译成目标语言。实体框架利用这一点来生成SQL。LINQ to ES|QL提供程序使用它来生成ES|QL。
上述查询的表达式树如下所示:

示例查询的表达式树。
示例查询的表达式树。
树是从内向外嵌套的:Take包裹OrderByDescending,OrderByDescending包裹Where,Where包裹From,From包裹根EsqlQueryable<Product>常量。Where谓词本身是BinaryExpression节点的子树,用于&&、>=和==运算符,以及用于属性访问的MemberExpression叶子和用于捕获闭包的minPrice和brand变量。这是提供程序遍历以生成最终ES|QL的数据结构。
从LINQ表达式到查询结果的路径遵循六阶段管道:

翻译管道概述。
翻译管道概述。
当您在IQueryable<T>上链接.Where()、.OrderBy()、.Take()和其他操作符时,标准的LINQ基础设施会构建一个表达式树。EsqlQueryable<T>实现了IQueryable<T>并委托给EsqlQueryProvider。
当查询被执行(通过枚举、调用ToList()或使用await foreach)时,EsqlExpressionVisitor从内向外遍历表达式树。它将每个LINQ方法调用分派给一个专门的访问者:
访问者 | 翻译内容 | 翻译成 |
|---|---|---|
WhereClauseVisitor | .Where(predicate) | WHERE condition |
SelectProjectionVisitor | .Select(selector) | EVAL + KEEP + RENAME |
GroupByVisitor | .GroupBy().Select() | STATS ... BY |
OrderByVisitor | .OrderBy() / .ThenBy() | SORT field [ASC\ |
EsqlFunctionTranslator | EsqlFunctions.*, Math.*, string methods | 80+ ES |
在翻译过程中,表达式中引用的C#变量被捕获为命名参数。
访问者不会直接生成字符串。相反,它们生成QueryCommand对象,这是一个不可变的中间表示。一个FromCommand、一个WhereCommand、一个SortCommand和一个LimitCommand,每个都代表一个ES|QL处理命令。它们被收集到一个EsqlQuery模型中。

查询模型和命令模式。
查询模型和命令模式。
这个中间模型与表达式树和输出格式解耦。它可以被检查、拦截(通过IEsqlQueryInterceptor)或在格式化之前修改。
EsqlFormatter按顺序访问每个QueryCommand并生成最终的ES|QL字符串。每个命令成为一行,由ES|QL用来链接处理命令的管道(|)操作符分隔。包含特殊字符的标识符会自动用反引号转义。
格式化后的ES|QL字符串和捕获的参数作为JSON负载发送到Elasticsearch的/_query端点。IEsqlQueryExecutor接口抽象了传输层,这是分层包架构发挥作用的地方。
EsqlResponseReader在不缓冲整个结果集到内存中的情况下流式传输JSON响应。每个查询预先计算一次的ColumnLayout树,将平面ES|QL列名(如address.street,address.city)映射到嵌套的POCO属性。每行被组装成一个T实例并通过IEnumerable<T>或IAsyncEnumerable<T>逐一生成。
LINQ to ES|QL功能分布在三个包中:

包架构。
包架构。
Elastic.Esql是纯翻译引擎。它没有HTTP依赖,包含表达式访问者、查询模型、格式化器和响应读取器。您可以单独使用它来构建和检查ES|QL查询,而不需要Elasticsearch连接,这对于测试、查询日志记录或构建您自己的执行层非常有用。
1
2
3
4
5
6
7
8
9
// 仅翻译:不需要Elasticsearch连接
var provider = new EsqlQueryProvider();
var query = new EsqlQueryable<Product>(provider)
.From("products")
.Where(p => p.InStock)
.OrderByDescending(p => p.Price);
Console.WriteLine(query.ToEsqlString());
// FROM products | WHERE in_stock == true | SORT price_usd DESC
Elastic.Clients.Esql是一个轻量级的独立ES|QL客户端。它通过Elastic.Transport在Elastic.Esql之上添加HTTP执行。如果您的应用程序只需要ES|QL而不需要其他Elasticsearch API,这是最小依赖选项。
Elastic.Clients.Elasticsearch是完整的Elasticsearch .NET客户端。它也建立在Elastic.Esql之上,并通过client.Esql命名空间暴露LINQ提供程序。这是大多数应用程序的推荐入口。
这两个执行层包提供了自己的IEsqlQueryExecutor实现,这是连接翻译和传输的策略接口。
当与源生成的JsonSerializerContext一起使用时,所有三个包都兼容原生AOT。有关完整客户端的信息,请参阅原生AOT文档。
上面的示例涵盖了过滤、排序和分页。该提供程序支持更广泛的操作集。
GroupBy与Select中的聚合函数结合,转换为ES|QL STATS ... BY:
1
2
3
4
5
6
7
8
9
10
var stats = client.Esql.Query<Product, object>(q => q
.GroupBy(p => p.Brand)
.Select(g => new
{
Brand = g.Key,
Count = g.Count(),
AvgPrice = g.Average(p => p.Price),
MaxPrice = g.Max(p => p.Price)
}));
// -> FROM products | STATS COUNT(*), AVG(price_usd), MAX(price_usd) BY brand
Select与匿名类型生成EVAL、KEEP和RENAME命令:
1
2
3
var query = client.Esql.CreateQuery<Product>()
.Select(p => new { ProductName = p.Name, p.Price, p.InStock });
// -> FROM products | KEEP name, price_usd, in_stock | RENAME name AS ProductName
通过EsqlFunctions类,可以使用超过80种ES|QL函数,涵盖日期/时间、字符串、数学、IP、模式匹配和评分。标准的Math.*和string.*方法也被转换:
1
2
3
4
5
.Where(p => p.Name.Contains("Pro"))
// -> WHERE name LIKE "*Pro*"
.Where(p => EsqlFunctions.CidrMatch(
// -> WHERE CIDR_MATCH(ip, "10.0.0.0/8")
p.IpAddress, "10.0.0.0/8"))
跨索引查找转换为ES|QL LOOKUP JOIN:
1
2
3
4
5
6
var enriched = client.Esql.Query<Product, object>(q => q
.LookupJoin<Product, CategoryLookup, string, object>(
"category-lookup-index",
product => product.Id,
category => category.CategoryId,
(product, category) => new { product.Name, category!.CategoryLabel }));
对于LINQ提供程序尚未覆盖的ES|QL功能,您可以附加原始片段:
1
2
3
var results = client.Esql.Query<Product>(q => q
.Where(p => p.InStock)
.RawEsql("| EVAL discounted = price_usd * 0.9"));
对于长时间运行的查询,将它们提交以在服务器上进行后台处理:
1
2
3
4
5
6
7
8
9
10
11
await using var asyncQuery = await client.Esql.SubmitAsyncQueryAsync<Product>(
q => q.Where(p => 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);
服务器端异步查询在长时间运行的分析查询/大数据集处理中特别有用,这可能超过典型的超时阈值,或在具有负载均衡器、API网关或代理的超时敏感环境中。异步查询通过将提交与结果检索分开来避免连接中断。
LINQ to ES|QL从以下版本开始可用:
从NuGet安装:
dotnet add package Elastic.Clients.Elasticsearch
入口点在client.Esql上:
方法 | 返回 | 用例 |
|---|---|---|
Query(...) | IEnumerable | 同步执行 |
QueryAsync(...) | IAsyncEnumerable | 异步流 |
CreateQuery() | IEsqlQueryable | 高级组合与检查 |
SubmitAsyncQueryAsync(...) | EsqlAsyncQuery | 长时间运行的服务器端查询 |
有关完整的功能参考,包括查询选项、多字段访问、嵌套对象和多值字段处理,请参阅LINQ to ES|QL文档。
LINQ to ES|QL将C# LINQ的完整表达能力带入Elasticsearch的ES|QL查询语言中,让您无需手动编写查询字符串即可编写强类型、可组合的查询。通过自动参数捕获、流式实例化和可从独立翻译扩展到完整Elasticsearch客户端的分层包架构,它自然适用于任何规模的.NET应用程序。安装最新客户端,将您的LINQ表达式指向索引,剩下的交给提供程序处理。
😔没帮助
😐有点帮助
😁非常有帮助
报告问题
📡 更多 Elastic & AI 可观测性干货
关注公众号「点火三周」,第一时间获取最新技术文章