Loading

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")
		
  1. WHERE statusCode >= 500
  2. WHERE NOT isResolved
.OrderBy(l => l.Level).ThenByDescending(l => l.Timestamp)
.Take(50)
		
  1. SORT level, @timestamp DESC
  2. 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)
		
  1. KEEP message, @timestamp
  2. KEEP message, @timestamp
  3. DROP duration, host
  4. 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)
		
  1. WHERE @timestamp > (NOW() + -1 hours)
.Where(l => Math.Abs(l.Delta) > 0.5)
.Select(l => new { Root = Math.Sqrt(l.Value) })
		
  1. WHERE ABS(delta) > 0.5
  2. 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.