LINQ 到 ES|QL:使用 C# 查询 Elasticsearch

作者:来自 Elastic Florian BerndMartijn Laarman

探索 Elasticsearch .NET 客户端中新推出的 LINQ 到 ES|QL 提供 程序 ,它允许你编写 C# 代码并自动转换为 ES|QL 查询。

动手体验 Elasticsearch:深入探索 Elasticsearch Labs repo 中的示例 notebooks,开始免费的 cloud 试用,或立即在本地机器上试用 Elastic。

v9.3.4v8.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写代码![](https://csdnimg.cn/release/blogv2/dist/pc/img/runCode/icon-arrowwhite.png)

在定义好类型之后,一个查询如下所示:

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写代码![](https://csdnimg.cn/release/blogv2/dist/pc/img/runCode/icon-arrowwhite.png)

该提供程序会将其转换为以下 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写代码![](https://csdnimg.cn/release/blogv2/dist/pc/img/runCode/icon-arrowwhite.png)

这是如何工作的?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.streetaddress.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写代码![](https://csdnimg.cn/release/blogv2/dist/pc/img/runCode/icon-arrowwhite.png)

投影

Select,使用匿名类型会生成 EVALKEEPRENAME 命令:

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写代码![](https://csdnimg.cn/release/blogv2/dist/pc/img/runCode/icon-arrowwhite.png)

服务器端异步查询对于长时间运行的分析查询 / 大型数据集处理特别有用,这类查询可能超过典型的超时阈值,或者在带有负载均衡器、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 处理其余部分。

原文:www.elastic.co/search-labs...

相关推荐
我爱学习好爱好爱4 小时前
Ansible 自动化部署Elasticsearch + Logstash + Kibana实战(基于RockyLinux 9.6)
elasticsearch·自动化·ansible
LiLiYuan.4 小时前
【Elasticsearch扫盲】
大数据·elasticsearch·搜索引擎
Elastic 中国社区官方博客5 小时前
从判断列表到训练好的 Learning to Rank( LTR )模型
大数据·数据库·人工智能·深度学习·elasticsearch·搜索引擎·全文检索
尽兴-5 小时前
MySQL 与 Elasticsearch 数据一致性保障的四大主流方案
数据库·mysql·elasticsearch
尽兴-6 小时前
Elasticsearch 生产集群最佳实践:模板治理、ILM 生命周期与运维体系
java·运维·elasticsearch·容量规划·ccs·分片设计
ACGkaka_6 小时前
ES 学习(五):DSL常用操作整理
大数据·学习·elasticsearch
SoulRoar.6 小时前
Armbian离线安装ES+SkyWalking并注册系统服务
大数据·elasticsearch·skywalking
菜鸡00017 小时前
把一个项目传到 GitLab 的某个群组
大数据·elasticsearch·gitlab
云原生指北16 小时前
命令行四件套:fd-rg-fzf-bat
java·大数据·elasticsearch