Elastic.Esql
A translation library that converts C# LINQ expressions into Elasticsearch ES|QL query strings. No HTTP dependencies, no transport layer, AOT compatible -- just query generation.
Use Elastic.Esql directly when you need:
- ES|QL string generation without any HTTP dependency
- Query inspection or logging of generated ES|QL
- Integration with a transport layer you already have
If you want LINQ-to-ES|QL with real cluster execution, use Elastic.Clients.Esql instead -- it includes this package automatically.
dotnet add package Elastic.Esql
var query = new EsqlQueryable<Order>()
.From("orders")
.Where(o => o.Status == "shipped" && o.Total > 100)
.OrderByDescending(o => o.CreatedAt)
.Take(25)
.ToString();
Produces:
FROM orders
| WHERE (status == "shipped" AND total > 100)
| SORT createdAt DESC
| LIMIT 25
For Native AOT compatibility, pass a source-generated JsonSerializerContext so field names are resolved without reflection:
[JsonSerializable(typeof(Order))]
public partial class MyJsonContext : JsonSerializerContext;
var provider = new EsqlQueryProvider(MyJsonContext.Default);
var query = new EsqlQueryable<Order>(provider)
.From("orders")
.Where(o => o.Status == "shipped")
.ToString();
You can pass JsonSerializerOptions to control field name resolution:
var options = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};
var provider = new EsqlQueryProvider(options);
var query = new EsqlQueryable<Order>(provider)
.From("orders")
.Where(o => o.Status == "shipped")
.ToString();
var esql = (
from o in new EsqlQueryable<Order>().From("orders")
where o.Status == "shipped"
where o.Total > 100
orderby o.CreatedAt descending
select new { o.Id, o.Total, o.CreatedAt }
).ToString();
See the functions reference for the complete list and LINQ translation for how each LINQ command maps to ES|QL.
.Where(l => l.StatusCode >= 500)
.Where(l => l.Level == "ERROR" || l.Level == "FATAL") // WHERE (level == "ERROR" OR level == "FATAL")
.Where(l => !l.IsResolved)
.Where(l => tags.Contains(l.Tag)) // WHERE tag IN ("a", "b", "c")
- WHERE statusCode >= 500
- WHERE NOT isResolved
.OrderBy(l => l.Level).ThenByDescending(l => l.Timestamp)
.Take(50)
- SORT level, @timestamp DESC
- LIMIT 50
.Select(l => new { l.Message, Secs = l.Duration / 1000 }) // EVAL secs = (duration / 1000) | KEEP message, secs
.Keep("message", "@timestamp")
.Keep(l => l.Message, l => l.Timestamp)
.Drop("duration", "host")
.Drop(l => l.Duration, l => l.Host)
- KEEP message, @timestamp
- KEEP message, @timestamp
- DROP duration, host
- DROP duration, host
.GroupBy(l => l.Level)
.Select(g => new {
Level = g.Key,
Count = g.Count(),
AvgDuration = g.Average(l => l.Duration)
})
// STATS count = COUNT(*), avgDuration = AVG(duration) BY level
.Where(l => l.Message.Contains("timeout")) // WHERE message LIKE "*timeout*"
.Where(l => l.Host.StartsWith("prod-")) // WHERE host LIKE "prod-*"
.Where(l => l.Message.ToLower() == "error") // WHERE TO_LOWER(message) == "error"
.Where(l => l.Timestamp.Year == 2025) // WHERE DATE_EXTRACT("year", @timestamp) == 2025
.Where(l => l.Timestamp > DateTime.UtcNow.AddHours(-1))
.Select(l => new { Hour = l.Timestamp.Hour }) // EVAL hour = DATE_EXTRACT("hour", @timestamp)
- WHERE @timestamp > (NOW() + -1 hours)
.Where(l => Math.Abs(l.Delta) > 0.5)
.Select(l => new { Root = Math.Sqrt(l.Value) })
- WHERE ABS(delta) > 0.5
- EVAL root = SQRT(value)
.Completion(l => l.Message, InferenceEndpoints.OpenAi.Gpt41, column: "analysis")
.Row(() => new { prompt = "Tell me about Elasticsearch" })
.Completion("prompt", InferenceEndpoints.Anthropic.Claude46Opus, column: "answer")
See the COMPLETION docs for pipeline patterns, standalone completions, and well-known endpoint constants.
using static Elastic.Esql.Functions.EsqlFunctions;
.Where(l => Match(l.Message, "connection error")) // WHERE MATCH(message, "connection error")
.Where(l => CidrMatch(l.ClientIp, "10.0.0.0/8")) // WHERE CIDR_MATCH(client_ip, "10.0.0.0/8")
.Where(l => Like(l.Path, "/api/v?/users")) // WHERE path LIKE "/api/v?/users"
query.LookupJoin<Order, Customer, string, OrderWithCustomer>(
"customers",
o => o.CustomerId,
c => c.Id,
(o, c) => new OrderWithCustomer { Order = o, CustomerName = c.Name }
)
// LOOKUP JOIN customers ON customer_id
Captured C# variables can be parameterized instead of inlined:
var minStatus = 400;
var esql = query
.Where(l => l.StatusCode >= minStatus)
.ToEsqlString(inlineParameters: false);
// WHERE statusCode >= ?minStatus
var parameters = query.GetParameters();
Elastic.Esql is a pure translation library -- it generates ES|QL strings but does not execute them. Use Elastic.Clients.Esql for the official Elastic.Transport-based execution layer, or implement the IEsqlQueryExecutor interface to plug in your own transport.