大家好,我是晚安code。
Elasticsearch查询从头到尾就一套逻辑,DSL 是它的 JSON 面孔,JavaRestClient 是它的 Java 面孔,同一个查询模型,两种写法而已。这篇就把这层窗户纸捅破,先学会 DSL 语法,再看它怎么翻译成 Java 代码。点个收藏,我们开始。
一、Elasticsearch查询的 DSL 骨架:先把 JSON 看懂
ES 的查询只有一个入口,就是 DSL——不管你用官方客户端还是第三方工具,最后递到集群面前的都是这份 JSON。
DSL(Domain Specific Language):Elasticsearch 的领域查询语言,用 JSON 结构表达查询条件。你可以把它理解成「用 JSON 写的 SQL」。
只按 id 查 GET /books/_doc/1 远远不够。业务里的搜索往往是「书名含某个词、价格在某区间、出版社是某家」这种组合条件,这时候就得走 _search 接口,把条件塞进 query 对象。DSL 查询在结构上分两大类,先认清这个分类,后面看任何查询都不慌:
叶子查询(Leaf Query):在单个字段上查特定值的查询,是 ES 查询的最小单元,负责「一个字段、一个条件」。
复合查询(Compound Query):用逻辑运算把多个叶子查询组合起来,或者改变查询的算分方式,负责「把条件捏在一起」。

这张 ES 查询家族谱。叶子查询里日常最常用的是三种:全文检索(match、multi_match)、精确查询(term、range)、地理查询(geo_distance 这类,做「附近的书店」功能才用得上)。复合查询这边,九成场景都在 bool 上打转,另一小类是 function_score、dis_max 这种调整算分的,平时用得少,知道存在就行。
二、叶子查询:match 和 term 什么时候用
match 走分词、term 走精确,这是全文检索和精确查询的分水岭,字段类型选错就是白查。
全文检索这一卦的核心就是 match 和 multi_match。它俩都会先对用户输入做分词,再去倒排索引里匹配词条,区别只在一个字段还是多个字段:
GET /books/_search
{
"query": {
"match": {
"name": "程序员修炼之道"
}
}
}
查「程序员修炼之道」,match 会把它拆成「程序员」「修炼」「之道」等词条去索引里匹配,命中越多分数越高,排名越靠前。multi_match 是同样的逻辑,只是允许一次查多个字段——比如书名加简介一起搜。注意一点:参与查询的字段越多,性能越差,够用就行,别把整个 mapping 都填进去。
精确查询这卦是 term 和 range。它俩都不分词,拿你给的词条直接和字段值做比对:
GET /books/_search
{
"query": {
"term": {
"publisher": {
"value": "人民邮电出版社"
}
}
}
}
GET /books/_search
{
"query": {
"range": {
"price": {
"gte": 30,
"lte": 80
}
}
}
}
term 适合查 keyword、数值、日期、布尔这些「整体才有意义」的字段——出版社名、ISBN、是否上架都算。range 就一句话:给字段画个区间,价格、库存、评分都能用。
当初我拿 term 去查书名(text 类型),结果一本都查不出来,盯着空结果看了半天才反应过来:书名存进索引时早被分词拆开了,term 拿整句去比,哪比得上。

可能有人会问:match 和 term 到底怎么选?
一句话:用户输入是「一段话」,用 match(要分词去搜);字段是 keyword/数值/日期/布尔这类定死的值,用 term。反过来用——用 term 搜用户输入、用 match 搜 ID——都是坑。

三、bool查询:把多个条件拼成复合查询
bool 是 Elasticsearch 复杂查询的地基,九成组合条件都是靠它拼出来的。
真实业务里很少只用一个叶子查询。比如要查「书名含 Java、出版社是老牌社、价格别超过 100」——三个条件,怎么塞进一个 query?答案就是 bool,它内部有四种子句:
- must:必须满足,参与算分,类似「与」
- should:满足其一即可,参与算分,类似「或」
- must_not:必须不满足,不参与算分
- filter:必须满足,但不参与算分,还能被缓存
GET /books/_search
{
"query": {
"bool": {
"must": [
{
"match": {
"name": "Java" } }
],
"should": [
{
"term": {
"publisher": "人民邮电出版社" } },
{
"term": {
"publisher": "机械工业出版社" } }
],
"must_not": [
{
"range": {
"price": {
"gte": 150 } } }
],
"filter": [
{
"range": {
"price": {
"lte": 100 } } }
]
}
}
}
must_not 和 filter 都不参与算分,所以我建议把「过滤型」条件尽量丢进 filter:不掺和排序,还能走查询缓存,同样的条件查一次命中一次。

四、排序、分页与高亮:深度分页为什么不能硬翻
分页不是想翻到哪翻到哪,ES 的 from+size 深翻页会翻不动,得提前换方案。
查询结果默认按相关度算分排序,也可以用 sort 显式指定字段排序。分页靠 from 和 size 两个参数:from 是跳过多少条,size 是取多少条。再叠一个高亮,把命中的关键词包上 <em> 标签返回给前端,页面就能直接渲染。这三件事在 DSL 里是平级的兄弟:
GET /books/_search
{
"query": {
"match": {
"name": "设计模式" } },
"from": 0,
"size": 10,
"sort": [
{
"price": {
"order": "asc" } }
],
"highlight": {
"fields": {
"name": {
"pre_tags": "<em>",
"post_tags": "</em>"
}
}
}
}
真正的坑在深翻页。ES 的数据是分片存的,一个索引拆成多份放在不同节点上。查第 100 页(from=990, size=10)时,协调节点得先让每个分片各自取前 1000 条,再汇总排序,最后只留下第 990~1000 名——前面那 990 条全白算。页码越深,白算的量越大,这就是深翻页慢的根源:

更难受的是默认限制:from+size 查询默认最多只能翻到第 10000 条(max_result_window 的默认值)。我线上就被真实用户教育过——列表页有人一路点到第 200 页,接口直接 500。

可能有人会问:深度分页到底怎么解决?
官方给两个方案:search_after 和 scroll。search_after 记住上一次的排序值,从那个位置往后接着查,没有上限,官方推荐;scroll 是把排序结果在内存里做一份快照,适合数据迁移这类一次性任务,官方已经不推荐。它俩有个共同点:都只能往后翻,不能像 from+size 那样随机跳页。
| 方案 | 原理 | 上限 | 适用场景 |
|---|---|---|---|
| from+size | 从头偏移取 | 默认 10000 条封顶 | 前几页展示 |
| search_after | 从上次排序位置继续 | 无上限 | 无限滚动、深翻页 |
| scroll | 服务端内存存快照 | 内存占用大 | 数据迁移、全量导出 |
五、聚合查询:让 Elasticsearch 帮你算报表
聚合就是 ES 的 GROUP BY——按某个字段分组,再对每组做统计,一次请求把报表算出来。
聚合(Aggregation):对搜索结果做分组和统计的机制,对应 SQL 里的 GROUP BY 加聚合函数。
比如商城首页要显示「每个分类下有多少本书」,本质就是按 category 字段分组,这就是 terms 聚合(bucket 类):
GET /books/_search
{
"size": 0,
"aggs": {
"category_agg": {
"terms": {
"field": "category",
"size": 20
}
}
}
}
size 设成 0,意思是结果里我不要文档,只要聚合结果。想更进一步,比如「编程」分类下各出版社的书,价格最低、最高、平均各是多少——在 terms 里再嵌一层 stats 聚合就行:
GET /books/_search
{
"query": {
"term": {
"category": "编程" } },
"size": 0,
"aggs": {
"publisher_agg": {
"terms": {
"field": "publisher", "size": 10 },
"aggs": {
"price_stats": {
"stats": {
"field": "price" }
}
}
}
}
}
stats 一次返回 min、max、avg、sum、count 五个值,做个价格分布报表连 SQL 都省了,聚合结果直接喂给前端画图就行。

六、JavaRestClient查询:把 DSL 翻译成 Java 代码
JavaRestClient 和 DSL 是同一套查询逻辑的两副面孔,DSL 调通了,Java 里就是照着翻译。
JavaRestClient:Elasticsearch 官方 Java 客户端(7.x 里叫 REST High Level Client),用 Java 对象构建查询请求,底层发给集群的还是那份 DSL。
先引依赖、初始化连接:
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-high-level-client</artifactId>
<version>7.12.1</version>
</dependency>
RestHighLevelClient client = new RestHighLevelClient(
RestClient.builder(new HttpHost("192.168.10.20", 9200, "http"))
);
这里有个版本坑要先说清楚:REST High Level Client 在 7.15 起被标记弃用,8.0 已被官方移除。你要是上了 8.x,就得换新的 Java API Client(co.elastic.clients:elasticsearch-java),类名写法变了,但「先 DSL 后 Java」的翻译思路一点没变。

查询的套路是固定的:new 一个 SearchRequest → 用 QueryBuilders 搭条件 → 塞进 request.source() → client.search 发出去。前面 DSL 里每个查询,在这里都有对应方法:
SearchRequest request = new SearchRequest("books");
request.source().query(QueryBuilders.matchQuery("name", "设计模式"));
// 分页 + 排序 + 高亮
request.source().from(0).size(10);
request.source().sort("price", SortOrder.ASC);
request.source().highlighter(SearchSourceBuilder.highlight()
.field("name").preTags("<em>").postTags("</em>"));
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
组合条件用 QueryBuilders.boolQuery(),一个方法返回一个组装器,must、should、filter 挨个往里面加:
BoolQueryBuilder bool = QueryBuilders.boolQuery();
bool.must(QueryBuilders.matchQuery("name", "Java"));
bool.should(QueryBuilders.termQuery("publisher", "人民邮电出版社"));
bool.mustNot(QueryBuilders.rangeQuery("price").gte(150));
bool.filter(QueryBuilders.rangeQuery("price").lte(100));
request.source().query(bool);
响应的解析也是对照翻译:getHits() 拿结果数组,getTotalHits() 拿总数,getSourceAsString() 拿文档 JSON,高亮按字段名取出来拼回对象:
SearchHits hits = response.getHits();
for (SearchHit hit : hits.getHits()) {
Book book = new ObjectMapper()
.readValue(hit.getSourceAsString(), Book.class);
HighlightField hf = hit.getHighlightFields().get("name");
if (hf != null) {
book.setName(hf.getFragments()[0].string());
}
}
依赖、初始化、文档的增删改查在 JavaRestClient 里也是一套固定模板:IndexRequest 写、GetRequest 查、UpdateRequest 改,要批量就塞进 BulkRequest。这张对照表值得存下来,写 Java 时直接翻:
| 查询 | DSL 写法 | JavaRestClient 写法 |
|---|---|---|
| 全量 | match_all | QueryBuilders.matchAllQuery() |
| 单字段分词 | match | QueryBuilders.matchQuery() |
| 多字段分词 | multi_match | QueryBuilders.multiMatchQuery() |
| 词条精确 | term | QueryBuilders.termQuery() |
| 范围 | range | QueryBuilders.rangeQuery() |
| 组合条件 | bool | QueryBuilders.boolQuery() |
| 分组统计 | aggs.terms | AggregationBuilders.terms().field() |
学 Elasticsearch查询,我最大的体会就是「别把 DSL 和 Java 客户端当两门课学」。先在 Dev Tools 里把 DSL 调通、看到真实返回,再照着对照表翻译成 JavaRestClient 代码,比直接啃 Java API 文档快得多。这套「DSL 先行、Java 翻译」的路数,用 7.x 的 High Level Client 和 8.x 的新客户端都成立。
如果你也刚开始接 ES,建议按这个顺序刷技能点:match 打底 → bool 组合 → 聚合出报表,三个点够应付绝大多数搜索需求。本文基于 Elasticsearch 7.12.1 实测
我是晚安code,持续分享编程干货。觉得有用的话记得点赞收藏和关注~也欢迎在评论区聊聊:你在 Elasticsearch查询里用 from+size 还是 search_after 做分页?