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

张开发
2026/5/23 5:23:27 15 分钟阅读
LINQ 到 ES|QL:使用 C# 查询 Elasticsearch
作者来自 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 QueryLINQ提供程序可在运行时将 C# LINQ 表达式转换为 Elasticsearch Query LanguageES|QL查询。你无需手动编写 ES|QL 字符串而是使用 Where、Select、OrderBy、GroupBy 等标准操作符来构建查询。该提供程序负责处理转换、参数化以及结果反序列化包括逐行流式处理从而在结果集规模变化时仍保持内存使用恒定。你的第一个查询首先定义一个普通的 CLR 对象POCO用于映射到你的 Elasticsearch 索引。属性名称通过标准的 System.Text.Json 特性如 [JsonPropertyName]或配置的 JsonNamingPolicy 映射为 ES|QL 列名。与客户端其他部分一致的序列化规则在这里同样适用。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; } }在定义好类型之后一个查询如下所示var minPrice 100.0; var brand TechCorp; await foreach (var product in client.Esql.QueryAsyncProduct(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|QLFROM products | WHERE (in_stock true AND price_usd ?minPrice AND brand ?brand) | SORT price_usd DESC | LIMIT 10需要注意的几点属性名称解析p.Price 由于 [JsonPropertyName] 属性会被转换为 price_usd而 p.Brand 则根据默认的 camelCase 命名策略转换为 brand。参数捕获C# 变量 minPrice 和 brand 会被捕获为命名参数?minPrice、?brand。它们会作为 JSON 负载的一部分与查询字符串分开发送从而防止注入并支持服务器端查询计划缓存。流式处理QueryAsync 返回 IAsyncEnumerable。当数据从 Elasticsearch 返回时结果会逐行被实例化。你还可以在不执行查询的情况下检查生成的查询及其参数var query client.Esql.CreateQueryProduct() .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 和 IQueryable 之间的区别。当你在 IEnumerable 上调用 .Where(p p.Price 100) 时该 lambda 会被编译为 FuncProduct, bool这是一个普通的委托由运行时在进程内执行。这就是 LINQ-to-Objects。当你在 IQueryable 上调用相同方法时C# 编译器会将该 lambda 包装为 ExpressionFuncProduct, bool。这是一种数据结构用于表示代码的结构而不是其可执行形式。表达式树可以在运行时被检查、分析并转换为另一种语言。// IEnumerable: the lambda is a compiled delegate IEnumerableProduct local products.Where(p p.Price 100); // IQueryable: the lambda is an expression tree, a data structure IQueryableProduct remote queryable.Where(p p.Price 100);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 conditionSelectProjectionVisitor.Select(selector)EVAL KEEP RENAMEGroupByVisitor.GroupBy().Select()STATS ... BYOrderByVisitor.OrderBy() / .ThenBy()SORT field [ASC\|DESC]EsqlFunctionTranslatorEsqlFunctions.*, Math.*, string methods80 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 实例并通过IEnumerableT或IAsyncEnumerableT一次返回一条。分层架构LINQ 到 ES|QL 的功能分布在三个包中包架构Elastic.Esql 是纯翻译引擎。它没有任何 HTTP 依赖并包含 expression visitors、query model、formatter 和 response reader。你可以独立使用它来构建和检查 ES|QL 查询而无需 Elasticsearch 连接这对于测试、查询记录或构建你自己的执行层非常有用。// Translation-only: no Elasticsearch connection needed var provider new EsqlQueryProvider(); var query new EsqlQueryableProduct(provider) .From(products) .Where(p p.InStock) .OrderByDescending(p p.Price); Console.WriteLine(query.ToEsqlString()); // FROM products | WHERE in_stock true | SORT price_usd DESCElastic.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:var stats client.Esql.QueryProduct, 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 命令var query client.Esql.CreateQueryProduct() .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 函数涵盖 date/time、string、math、IP、pattern matching 和 scoring。标准 Math.* 和 string.* 方法也会被翻译.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 JOINvar enriched client.Esql.QueryProduct, object(q q .LookupJoinProduct, CategoryLookup, string, object( category-lookup-index, product product.Id, category category.CategoryId, (product, category) new { product.Name, category!.CategoryLabel }));原生 ES|QL 逃生舱对于 LINQ provider 尚未覆盖的 ES|QL 功能你可以追加 raw fragmentsvar results client.Esql.QueryProduct(q q .Where(p p.InStock) .RawEsql(| EVAL discounted price_usd * 0.9));服务器端异步查询对于长时间运行的查询将它们提交到服务器进行后台处理await using var asyncQuery await client.Esql.SubmitAsyncQueryAsyncProduct( 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 gateways 或强制严格 HTTP 超时的代理的超时敏感环境中。异步查询通过将提交与结果检索分离避免连接中断。入门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.EsqlMethodReturnsUse caseQueryT(...)IEnumerableTSynchronous executionQueryAsyncT(...)IAsyncEnumerableTAsync streamingCreateQueryT()IEsqlQueryableTAdvanced composition and inspectionSubmitAsyncQueryAsyncT(...)EsqlAsyncQueryTLong-running server-side queries有关完整功能参考包括查询选项、多字段访问、嵌套对象和多值字段处理请参阅 LINQ to ES|QL 文档。结论LINQ to ES|QL 将 C# LINQ 的完整表达能力引入 Elasticsearch 的 ES|QL 查询语言让你可以编写强类型、可组合的查询而无需手工构建查询字符串。通过自动参数捕获、流式实体化以及从独立翻译到完整 Elasticsearch 客户端的分层包架构它自然适用于任何规模的 .NET 应用。安装最新客户端将你的 LINQ 表达式指向索引让 provider 处理其余部分。原文https://www.elastic.co/search-labs/blog/linq-esql-c-elasticsearch-net-client

更多文章