首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >LINQ to ES|QL:用C#查询Elasticsearch

LINQ to ES|QL:用C#查询Elasticsearch

作者头像
点火三周
发布2026-04-15 15:05:11
发布2026-04-15 15:05:11
120
举报
文章被收录于专栏:Elastic Stack专栏Elastic Stack专栏

LINQ to ES|QL:用C#查询Elasticsearch

深入了解Elasticsearch:您可以在Elasticsearch Labs仓库中查看我们的示例笔记本,开启免费的云试用,或在本地机器上尝试Elastic。

v9.3.4v8.19.18版本开始,Elasticsearch的.NET客户端引入了一种语言集成查询(LINQ)提供程序,它可以在运行时将C#的LINQ表达式转换为Elasticsearch查询语言(ES|QL)查询。您无需手动编写ES|QL字符串,而是使用WhereSelectOrderByGroupBy等标准操作符来组合查询。提供程序负责翻译、参数化和结果反序列化,包括每行流式处理,无论结果集大小如何,都能保持内存使用恒定。

您的第一个查询

首先定义一个普通的CLR对象(POCO),它映射到您的Elasticsearch索引。属性名称通过标准的System.Text.Json属性(如[JsonPropertyName])或配置的JsonNamingPolicy解析为ES|QL列名。适用于客户端其他部分的源序列化规则在此同样适用。

代码语言:javascript
复制






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; }
}



定义类型后,查询如下所示:

代码语言:javascript
复制






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:

代码语言:javascript
复制






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
  • • **参数捕获:**C#变量minPricebrand被捕获为命名参数(?minPrice?brand)。它们与查询字符串分开在JSON负载中发送,这可以防止注入并启用服务器端查询计划缓存。
  • 流式处理:QueryAsync<T>返回IAsyncEnumerable<T>。行在从Elasticsearch到达时逐一实例化。

您还可以在不执行查询的情况下检查生成的查询及其参数:

代码语言:javascript
复制






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快速回顾

使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>>中。这是一个表示代码结构而不是其可执行形式的数据结构。表达式树可以在运行时被检查、分析和转换为另一种语言。

代码语言:javascript
复制






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包裹OrderByDescendingOrderByDescending包裹WhereWhere包裹FromFrom包裹根EsqlQueryable<Product>常量。Where谓词本身是BinaryExpression节点的子树,用于&&>===运算符,以及用于属性访问的MemberExpression叶子和用于捕获闭包的minPricebrand变量。这是提供程序遍历以生成最终ES|QL的数据结构。

深入了解:翻译管道

从LINQ表达式到查询结果的路径遵循六阶段管道:

翻译管道概述。
翻译管道概述。

翻译管道概述。

翻译管道概述。

1. 表达式树捕获

当您在IQueryable<T>上链接.Where().OrderBy().Take()和其他操作符时,标准的LINQ基础设施会构建一个表达式树。EsqlQueryable<T>实现了IQueryable<T>并委托给EsqlQueryProvider

2. 翻译

当查询被执行(通过枚举、调用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#变量被捕获为命名参数。

3. 查询模型

访问者不会直接生成字符串。相反,它们生成QueryCommand对象,这是一个不可变的中间表示。一个FromCommand、一个WhereCommand、一个SortCommand和一个LimitCommand,每个都代表一个ES|QL处理命令。它们被收集到一个EsqlQuery模型中。

查询模型和命令模式。
查询模型和命令模式。

查询模型和命令模式。

查询模型和命令模式。

这个中间模型与表达式树和输出格式解耦。它可以被检查、拦截(通过IEsqlQueryInterceptor)或在格式化之前修改。

4. 格式化

EsqlFormatter按顺序访问每个QueryCommand并生成最终的ES|QL字符串。每个命令成为一行,由ES|QL用来链接处理命令的管道(|)操作符分隔。包含特殊字符的标识符会自动用反引号转义。

5. 执行

格式化后的ES|QL字符串和捕获的参数作为JSON负载发送到Elasticsearch的/_query端点。IEsqlQueryExecutor接口抽象了传输层,这是分层包架构发挥作用的地方。

6. 实例化

EsqlResponseReader在不缓冲整个结果集到内存中的情况下流式传输JSON响应。每个查询预先计算一次的ColumnLayout树,将平面ES|QL列名(如address.streetaddress.city)映射到嵌套的POCO属性。每行被组装成一个T实例并通过IEnumerable<T>IAsyncEnumerable<T>逐一生成。

分层架构

LINQ to ES|QL功能分布在三个包中:

包架构。
包架构。

包架构。

包架构。

Elastic.Esql是纯翻译引擎。它没有HTTP依赖,包含表达式访问者、查询模型、格式化器和响应读取器。您可以单独使用它来构建和检查ES|QL查询,而不需要Elasticsearch连接,这对于测试、查询日志记录或构建您自己的执行层非常有用。

代码语言:javascript
复制






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.TransportElastic.Esql之上添加HTTP执行。如果您的应用程序只需要ES|QL而不需要其他Elasticsearch API,这是最小依赖选项。

Elastic.Clients.Elasticsearch是完整的Elasticsearch .NET客户端。它也建立在Elastic.Esql之上,并通过client.Esql命名空间暴露LINQ提供程序。这是大多数应用程序的推荐入口。

这两个执行层包提供了自己的IEsqlQueryExecutor实现,这是连接翻译和传输的策略接口。

当与源生成的JsonSerializerContext一起使用时,所有三个包都兼容原生AOT。有关完整客户端的信息,请参阅原生AOT文档。

超越基础

上面的示例涵盖了过滤、排序和分页。该提供程序支持更广泛的操作集。

聚合

GroupBySelect中的聚合函数结合,转换为ES|QL STATS ... BY

代码语言:javascript
复制






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与匿名类型生成EVALKEEPRENAME命令:

代码语言:javascript
复制






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.*方法也被转换:

代码语言:javascript
复制






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"))



LOOKUP JOIN

跨索引查找转换为ES|QL LOOKUP JOIN

代码语言:javascript
复制






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 }));



原始ES|QL逃生舱

对于LINQ提供程序尚未覆盖的ES|QL功能,您可以附加原始片段:

代码语言:javascript
复制






1
2
3

var results = client.Esql.Query<Product>(q => q
    .Where(p => p.InStock)
    .RawEsql("| EVAL discounted = price_usd * 0.9"));



服务器端异步查询

对于长时间运行的查询,将它们提交以在服务器上进行后台处理:

代码语言:javascript
复制






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从以下版本开始可用:

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

从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 可观测性干货

关注公众号「点火三周」,第一时间获取最新技术文章

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2026-04-02,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 点火三周 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • LINQ to ES|QL:用C#查询Elasticsearch
    • 您的第一个查询
    • 这是如何工作的?LINQ快速回顾
    • 深入了解:翻译管道
      • 1. 表达式树捕获
      • 2. 翻译
      • 3. 查询模型
      • 4. 格式化
      • 5. 执行
      • 6. 实例化
    • 分层架构
    • 超越基础
      • 聚合
      • 投影
      • 丰富的函数库
      • LOOKUP JOIN
      • 原始ES|QL逃生舱
      • 服务器端异步查询
    • 开始使用
    • 结论
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档