作者:来自 Elastic Margaret Gu

探索用于 JavaScript 和 TypeScript 的 ES|QL 查询构建器,并通过实际示例讲解如何构建 ES|QL 查询。
开始动手使用 Elasticsearch:可以浏览 Elasticsearch Labs 仓库中的示例 notebooks,启动免费的云端试用,或在本地机器上直接体验 Elastic。
我们很高兴地宣布,Elasticsearch Query Language( ES|QL )查询构建器现已支持 JavaScript 和 TypeScript。它是一个流式(fluent)、类型安全的库,可以通过方法链式调用来构建 ES|QL 查询,并具备自动值转义以及完整的 IDE 支持;不再需要手动拼接原始字符串。
通过实际示例学习如何立即上手使用。
JavaScript 和 TypeScript 的 ES|QL 查询构建器
如果你曾在 JavaScript 中构建过 ES|QL 查询,你可能写过类似这样的代码:
const query = `FROM logs-*
| WHERE status_code >= ${minStatus}
AND host.name == ${hostname}
AND @timestamp >= "${startDate}"
| STATS error_count = COUNT(*) BY status_code
| SORT error_count DESC
| LIMIT 10`
它看起来没问题,直到 hostname 变成 O'Brien's server,然后整个查询因为解析错误直接崩掉。或者直到某个用户在搜索字段里传入 "; DROP INDEX logs,你才意识到你一直在用字符串拼接来构建查询。
有更好的方式。JavaScript 和 TypeScript 的 ES|QL 查询构建器可以让你改成这样写查询:
import { ESQL, E, f } from '@elastic/elasticsearch-esql-dsl'
const query = ESQL.from('logs-*')
.where(E('status_code').gte(minStatus))
.where(E('host.name').eq(hostname))
.where(E('@timestamp').gte(startDate))
.stats({ error_count: f.count() })
.by('status_code')
.sort(E('error_count').desc())
.limit(10)
值会被自动转义(escaped)。你可以在编辑器中获得自动补全(autocomplete),并且无需再"脑内解析"模板字符串,就能清晰理解查询实际在做什么。
ES|QL 查询构建器已经在 Elastic 的多种语言客户端中提供支持,包括 Python、Ruby 等。本篇文章重点介绍 JavaScript 和 TypeScript 版本,并通过一些可以直接上手的实际示例来讲解它的用法。
入门开始
安装该包:
npm install @elastic/elasticsearch-esql-dsl
这是一个最小查询示例:
import { ESQL, E } from '@elastic/elasticsearch-esql-dsl'
const query = ESQL.from('employees')
.where(E('still_hired').eq(true))
.sort(E('last_name').asc())
.limit(10)
console.log(query.render())
渲染结果如下:
FROM employees
| WHERE still_hired == true
| SORT last_name ASC
| LIMIT 10
要在 Elasticsearch 上执行它:
import { Client } from '@elastic/elasticsearch'
const client = new Client({ node: 'http://localhost:9200' })
const response = await client.esql.query({ query: query.render() })
就是这样。没有字符串插值,也不需要手动转义。
一步一步构建真实查询
我们来看一个更贴近实际的场景:你正在构建一个用于分析 Web 服务器错误日志的仪表盘。我们先从简单开始,然后逐步增加功能。
步骤 1:过滤错误日志
import { ESQL, E } from '@elastic/elasticsearch-esql-dsl'
const errors = ESQL.from('logs-*')
.where(E('status_code').gte(400))
.limit(100)
FROM logs-*
| WHERE status_code >= 400
| LIMIT 100
步骤 2:添加计算列
你的时间戳是以毫秒为单位,但你希望将响应时间转换为秒:
const errors = ESQL.from('logs-*')
.where(E('status_code').gte(400))
.eval({ response_secs: E('response_time_ms').div(1000) })
.limit(100)
FROM logs-*
| WHERE status_code >= 400
| EVAL response_secs = response_time_ms / 1000
| LIMIT 100
步骤 3:按状态码聚合错误
import { f } from '@elastic/elasticsearch-esql-dsl'
const errorBreakdown = ESQL.from('logs-*')
.where(E('status_code').gte(400))
.stats({
error_count: f.count(),
avg_response: f.avg('response_time_ms'),
})
.by('status_code')
.sort(E('error_count').desc())
FROM logs-*
| WHERE status_code >= 400
| STATS error_count = COUNT(*), avg_response = AVG(response_time_ms) BY status_code
| SORT error_count DESC
这个 f 命名空间提供了 150+ 个 ES|QL 函数封装:包括聚合函数、字符串函数、日期函数、数学函数、地理函数等等。它们都返回可链式调用的表达式,因此可以在任何原本使用 E() 的地方使用。
步骤 4:使用日期函数进行时间维度分析
const hourlyErrors = ESQL.from('logs-*')
.where(E('status_code').gte(400))
.eval({ hour: f.dateTrunc('@timestamp', '1 hour') })
.stats({ error_count: f.count() })
.by('hour')
.sort(E('hour'))
FROM logs-*
| WHERE status_code >= 400
| EVAL hour = DATE_TRUNC(@timestamp, "1 hour")
| STATS error_count = COUNT(*) BY hour
| SORT hour
步骤 5:安全地分支查询
每个方法都会返回一个新的查询对象,原始查询不会被修改。这意味着你可以先构建一个基础查询,然后为不同的视图进行分支扩展:
const base = ESQL.from('logs-*')
.where(E('status_code').gte(400))
.where(E('@timestamp').gte('2026-01-01T00:00:00Z'))
const byStatus = base
.stats({ count: f.count() })
.by('status_code')
.sort(E('count').desc())
const byHost = base
.stats({ count: f.count() })
.by('host.name')
.sort(E('count').desc())
.limit(20)
const recent = base
.sort(E('@timestamp').desc())
.keep('@timestamp', 'status_code', 'url.path', 'message')
.limit(50)
三个不同的查询,共用一个基础查询。修改 base 的过滤条件,三个查询都会同步更新。这在仪表盘场景中特别有用,因为多个面板通常会对同一数据集使用不同的聚合方式。
三种表达式写法
该领域专用语言(DSL)提供了多种编写条件的方式。下面是同一个 WHERE 子句的三种不同写法:
原始字符串(raw strings):适用于快速一次性查询:
.where('status_code >= 400 AND host.name == "web-01"')
E() 表达式构建器:当你需要类型安全和自动补全时使用:
import { and_ } from '@elastic/elasticsearch-esql-dsl'
.where(and_(
E('status_code').gte(400),
E('host.name').eq('web-01')
))
esql 模板标签(template tag):当你需要安全地插入动态值时使用:
import { esql } from '@elastic/elasticsearch-esql-dsl'
const minStatus = 400
const host = 'web-01'
.where(esql`status_code >= ${minStatus} AND host.name == ${host}`)
这三种方式最终都会生成相同的 ES|QL 查询。你可以根据具体场景选择合适方式:简单场景用 raw string,程序化构建表达式用 E(),而需要在静态 ES|QL 中安全插入动态值时使用 template tag。
保持查询安全
如果查询的任何部分来自用户输入,就必须考虑注入风险。ES|QL 支持参数绑定,而 DSL 可以让这一点变得更简单直观:
function searchLogs(userQuery: string) {
const query = ESQL.from('logs-*')
.where(E('message').eq(E('?')))
.limit(100)
return client.esql.query({
query: query.render(),
params: [userQuery],
})
}
**?**占位符会在 Elasticsearch 服务器端被替换,因此用户输入不会直接进入查询字符串中。这意味着无需手动转义,也能避免注入风险。
进阶内容
当你熟悉了基础用法之后,这个 DSL 支持所有高级 ES|QL 特性,例如:
混合搜索(Hybrid search)中的 FORK 和 FUSE:
const results = ESQL.from('articles')
.fork(
ESQL.branch()
.where(f.match('title', 'elasticsearch'))
.sort(E('_score').desc())
.limit(50),
ESQL.branch()
.where(f.knn('embedding', 10))
.sort(E('_score').desc())
.limit(50),
)
.fuse('RRF')
.limit(10)
数据增强(Data enrichment):
const enriched = ESQL.from('logs-*')
.enrich('ip_lookup')
.on('client.ip')
.with('geo.city', 'geo.country')
条件聚合(Conditional aggregation):
const stats = ESQL.from('employees')
.stats({
eng_avg: f.avg('salary').where(E('dept').eq('Engineering')),
sales_avg: f.avg('salary').where(E('dept').eq('Sales')),
total: f.count(),
})
AI / 机器学习(ML)集成:
const summarized = ESQL.from('docs')
.completion('Summarize this document')
.with({ inferenceId: 'my-llm' })
完整命令与函数列表请查看 ES|QL query builder 文档。
下一步
这是 @elastic/elasticsearch-esql-dsl 的首个版本发布。你可以在 npm 上找到该包,在 GitHub 上查看源码,并在仓库中阅读完整文档。如果你遇到问题或有功能需求,欢迎提交 issue;我们正在持续迭代,希望真正构建 JavaScript 和 TypeScript 开发者需要的工具。
原文:https://www.elastic.co/search-labs/blog/esql-query-builder-javascript-typescript