主题
Elasticsearch 指南
读者画像:前端转 Java 的新手——会写 JSON、懂 HTTP 请求,Java 刚学完基础语法与 Spring Boot 入门 学习目标:理解 Elasticsearch 为什么快、什么时候用;能写出 ES 8.x Java API Client 的连接、写入、查询、分页排序高亮与聚合代码 适用版本:Elasticsearch 8.x + Java API Client 8.18.3(8.x 系列最新版,2026-11 核实) 范围外:集群部署运维、性能调优、Kibana/Logstash、ES|QL、相关性评分算法推导
范围
本指南用「商品搜索」一个场景贯穿全文:假设你有一个电商项目,用户要在一个搜索框里找商品。前四章回答「ES 是什么、为什么快」——这是看任何 ES 代码之前必须建立的心智模型;第五到九章回答「Java 里怎么用」——每一步都给出最小可读的代码。文中所有代码都是教学示意:为了讲清概念而精简(省略异常处理、真实地址),未在真实 ES 实例上运行验证,不能直接复制到生产。
组织洞察:ES 的本质是把「找数据」这件事从数据库里拆出来的专用系统——写入时先分词、提前建好倒排索引,查询时按词直接命中,再靠评分把最相关的少量结果排在最前面。
目录
1. 为什么需要 Elasticsearch
1.1 先说场景:商品搜索
用户打开你的电商网站,在搜索框输入「可乐」,回车。你要返回:
- 名称或描述里包含「可乐」的商品;
- 最相关的排前面(比如名称含「可乐」的排在描述含「可乐」的前面);
- 哪怕用户输入了「可落」这种错别字,最好也能搜出「可乐」。
这个需求叫全文搜索(Full-text Search)。数据库能不能干?能,但干得很痛苦。
1.2 数据库 LIKE 的三大痛点
用 MySQL 做模糊搜索,你只能写:
sql
SELECT * FROM product WHERE name LIKE '%可乐%';这条 SQL 有三个问题:
痛点 1:慢。LIKE '%可乐%' 以 % 开头,B+Tree 索引完全用不上,
只能一行一行扫全表。数据量上亿时,查询要几秒甚至更久。
痛点 2:没有相关性排序。搜「可乐」时,「可口可乐」和
「可乐鸡翅调料」谁更相关?数据库不关心,返回顺序是乱的,
你得自己再写一套排序逻辑。
痛点 3:零容错。用户输入「可落」就查不到「可乐」;
输入「cola」也查不到中文名。数据库只会精确比对。前端出身的话,可以这样理解:数据库 LIKE 就像你在代码里用 str.includes("可乐") 暴力过滤一个大数组——能用,但既慢又傻。
1.3 ES 的三板斧
Elasticsearch(简称 ES)是专门解决上面问题的系统,官方一句话介绍:
Elasticsearch is a real-time, distributed storage, search, and analytics engine. (一个实时的、分布式的存储、搜索与分析引擎。)
对应当前场景,它有三个能力恰好对症下药:
| 数据库的痛点 | ES 的能力 | 怎么做到的(后面章节会展开) |
|---|---|---|
| LIKE 慢 | 按词快速命中 | 写入时提前分词建好倒排索引(第 2 章) |
| 无相关性排序 | 每条结果带相关性评分(_score),按分排序 | 默认相关度算法 BM25(第 11 章概述) |
| 零容错 | 分词匹配天然支持部分匹配、大小写、拼写近似 | 查询也分词后按词匹配(第 7 章) |
1.4 什么时候用 ES,什么时候不用
适合交给 ES 的:
- 商品、文章、文档的全文搜索;
- 日志检索(海量日志里按关键字快速定位);
- 搜索建议、聚合统计(第 9 章)。
不适合硬用 ES 的:
- 主数据存储:订单、账户这类强事务数据仍应放数据库。ES 是「搜索专用」的补充系统,不是数据库的替代品;
- 数据量小、只用精确查询:MySQL 的等值查询 + 索引足够,引入 ES 徒增运维成本。
典型架构:数据库负责「写入与事务」,数据同步到 ES 负责「搜索与读取」,ES 扮演从库角色。
2. 倒排索引与分词
第 1 章说 ES 快,是因为「按词直接命中」。这一章讲清楚这句话背后的机制——这是全篇最重要的概念。
2.1 正向索引:一行一行翻
数据库的索引是正向索引(Forward Index):根据记录找内容。像翻一本书的目录,从章节名找到对应的页码。
正向索引:按文档找词(数据库的做法)
────────────────────────────────────────────
文档 1: 「冰镇可乐,3 元」
文档 2: 「可乐鸡翅,20 元」
文档 3: 「冰镇雪碧,3 元」
查「可乐」→ 打开每个文档看一遍有没有这个词 → 命中 1、2记录多的时候,「打开每个文档看一遍」就是慢的根源。
2.2 倒排索引:书末的关键词索引表
**倒排索引(Inverted Index,本指南按惯例简称 index)**反过来:根据词找文档。就像技术书末尾的「关键词 → 页码」索引表——查「泛型」这个词,索引表直接告诉你它在第 3、5、12 页,不用翻整本书。
倒排索引:按词找文档(ES 的做法)
────────────────────────────────────────────
词(Term) 文档编号(Posting List)
可乐 → [1, 2]
冰镇 → [1, 3]
鸡翅 → [2]
雪碧 → [3]
查「可乐」→ 直接命中文档 1、2,不用看文档 3对比:
┌──────────────────┬──────────────────────┐
│ 正向索引 │ 倒排索引 │
│ 文档 → 词 │ 词 → 文档 │
│ 相当于书的目录 │ 相当于书末索引表 │
│ 查记录快,查词慢 │ 查词快 │
└──────────────────┴──────────────────────┘数据库在做模糊匹配时没有「词 → 文档」这张表,所以只能扫全表;ES 在写入时就把这张表建好了,查询时按词直接取——这就是「模糊搜索为什么快」的全部秘密。
术语口径:本指南里,小写
index指这里讲的倒排索引;大写Index指 ES 里存数据的地方(相当于数据库的表,见第 3 章)。二者是不同概念,后文严格按此口径使用。
2.3 写入时分词:倒排索引怎么建出来
「按词命中」的前提是:写入时把一段文字切成一个个词。这个过程叫分词(Tokenization),负责分词的组件叫分词器(Analyzer)。
以写入文档「冰镇可乐」为例(中文分词示意):
「冰镇可乐」 ──分词──▶ [冰镇, 可乐]
│
▼
倒排索引新增:
冰镇 → [文档1]
可乐 → [文档1]查询时,用户输入的「可乐」也经过同样的分词器切成 [可乐],再拿这个词去倒排索引里查——写入和查询用同一套分词逻辑,所以词对得上。
2.4 分词器:英文内置,中文用 IK
分词器内部由三部分组成(了解即可,不必深究):
输入文字 ──▶ Character Filter ──▶ Tokenizer ──▶ Token Filter ──▶ 词列表
(字符预处理: (按规则切分: (词再加工:
去 HTML 标签等) 按空格/标点) 转小写等)ES 内置的分词器主要面向英文:
| 分词器 | 规则 | 「Hello-World」的结果 |
|---|---|---|
standard(默认) | 按词切分 + 转小写 | [hello, world] |
simple | 按非字母过滤 + 转小写 | [hello, world] |
whitespace | 只按空格切分,不转小写 | [Hello-World] |
中文没有空格分界,内置分词器只能把整句当一个词,检索效果很差。因此中文场景普遍使用 IK 分词器(第三方插件,安装后以 ik_smart/ik_max_word 使用),例如「冰镇可乐」被切成 [冰镇, 可乐]。本指南的教学示意默认按中文分词后的效果理解,不展开 IK 的安装配置。
2.5 通用规则前置:keyword 与 text、match 与 term
这是全文反复使用的一组判定依据,先在这里定义,第 7 章会回链到这里。
规则 1:字段类型决定「是否分词」。 ES 里字符串字段分两种核心类型:
text 类型:写入时会分词、建立倒排索引 → 适合全文搜索
keyword 类型:写入时不分词、整个值原样保存 → 适合精确匹配、过滤、排序、聚合同一个需求里的「名称」通常按 text 存(要搜「可乐」命中「可口可乐」),而「分类」「状态」按 keyword 存(「饮料」就是「饮料」,不要拆成「饮」「料」)。
规则 2:查询类型决定「查询词是否分词」。 两个最常用的查询:
match 查询:先对查询词分词,再拿每个词去倒排索引里匹配 → 全文搜索用
term 查询:不分析查询词,拿原值去精确比对 → 筛选用(配 keyword 字段)统一判定口径:分词与否,由写入侧(字段类型 text/keyword)和查询侧(match/term)各自独立决定。
text字段必须用match才搜得自然;keyword字段必须用term才精确命中。交叉使用(term 查 text、match 查 keyword)得不到预期的搜索结果。第 7 章所有示例都按这条规则选择查询类型,第 10 章速查清单再次固化。
2.6 到这里你知道了什么
- 倒排索引 = 「词 → 文档编号」表,写入时建好,查询时按词直取;
- 分词器让「冰镇可乐」变成
[冰镇, 可乐],写入与查询共用同一套分词; text/keyword与match/term两组规则,决定了分词行为发生在哪一侧。
3. 核心术语对照
ES 有自己的术语体系。对数据库基础不熟没关系,跟着对照表走一遍即可;如果学过 MySQL,下面的类比会让你秒懂。
3.1 Index(索引库)≈ 数据库的表
ES 中数据存放的地方叫 Index(大写,索引库)。一个 Index 里放一类数据,比如商品搜索场景建一个 products Index。
注意区分:第 2 章讲的倒排索引(
index)是数据结构;这里的Index是存放数据的逻辑容器。可以理解为:每个 Index 内部都维护着多份倒排索引,用来检索这个 Index 里的数据。
3.2 Document(文档)≈ 表中的一行
一个 Document 就是 Index 里的一条记录。它是 JSON 对象——前端同学应该感到亲切:
json
{
"id": "1001",
"name": "冰镇可乐",
"category": "饮料",
"price": 3.0,
"description": "500ml 瓶装汽水"
}3.3 Field(字段)与 Mapping(映射)
Document 里的每个键叫 Field,相当于表的列。Mapping 描述每个 Field 的类型和索引规则,相当于表的 Schema(建表语句)。
对新手最友好的事实:ES 默认支持动态映射(Dynamic Mapping)——你写入第一个 Document 时,ES 会根据 JSON 值自动推断字段类型并建好 Index,不用像数据库那样先写 CREATE TABLE。教学示意里我们全程依赖动态映射,省掉手动建 Index 的步骤。
3.4 Query DSL ≈ SQL
Query DSL(Domain Specific Language,领域专用语言)是 ES 的查询语言,作用相当于数据库的 SQL。区别在于:SQL 是字符串文本,DSL 本质是结构化 JSON;在 Java 客户端里,DSL 表现为一串链式调用(第 7 章的主角)。
3.5 一图流对照表
┌────────────────┬─────────────────┬──────────────────┐
│ ES │ 数据库(MySQL) │ 前端视角 │
├────────────────┼─────────────────┼──────────────────┤
│ Index(索引库) │ Table │ 一个集合/表 │
│ Document │ Row │ 一个 JSON 对象 │
│ Field │ Column │ JSON 的 key │
│ Mapping │ Schema │ TypeScript 接口 │
│ Query DSL │ SQL │ 一个查询函数 │
│ Type(已废弃) │ (无对应) │ — │
└────────────────┴─────────────────┴──────────────────┘关于 Type:ES 7 以前的版本里,一个 Index 下还可以分多个 Type。这个设计已被官方移除,看到旧教程里
_doc、Type 相关字样直接跳过即可,本指南不提。
4. 分片与"近实时"
官方介绍里的「分布式」和「实时」,这一章用简化模型解释清楚。目标是理解两个常见现象的原因,不展开集群运维细节。
4.1 节点与集群
节点(Node)就是跑着一个 ES 进程的机器;多台节点互相协作组成集群(Cluster)。你从任何一台节点发出的请求,集群都会统一处理,具体落在哪台机器上对调用方透明——就像你把请求打到 Nginx,不关心后面哪台应用服务器响应。
4.2 分片与副本分片
一个 Index 的数据量大到单机放不下(或单机处理不过来)时,ES 会把数据切成多份,分散到不同节点,这就是分片(Shard):
Index: products,3 个主分片(数据各占一份)
┌──────────┐ ┌──────────┐ ┌──────────┐
│ 节点 1 │ │ 节点 2 │ │ 节点 3 │
│ 分片 P0 │ │ 分片 P1 │ │ 分片 P2 │
└──────────┘ └──────────┘ └──────────┘
写入/查询时 3 个分片并行处理 → 吞吐量更高但「一份数据放在一台机器上」,机器一挂数据就没了。于是每个主分片(Primary Shard)再配一个副本分片(Replica Shard),副本放在不同节点:
节点 1: P0 | R1 节点 2: P1 | R2 节点 3: P2 | R0
写入总是先进主分片,再同步到副本;读取时主副本都能读。
节点 2 挂了 → 它的副本 R2 被提升为主分片 → 数据不丢、服务不断对新手记住两点就够:分片解决「大」的问题,副本解决「挂」的问题;分片和副本的个数都是建 Index 时可配置的(教学场景用默认值即可)。
4.3 近实时:为什么写入后要等约 1 秒才能搜到
ES 的写入不像数据库那样「落盘即见」。简化版写入流程:
写入请求
│
▼
① 写进内存缓冲(Memory Buffer) ← 到此还不能被搜索到
│
│ 约每 1 秒执行一次 refresh
▼
② 生成 segment 文件(可被搜索的最小单元) ← 此时才能被搜到因为 refresh 默认每 1 秒执行一次,所以「刚写入的数据」平均要等约 1 秒才能被搜索到。这就是官方说 ES 是 near real-time(近实时,NRT) 而不是强实时的原因。这也解释了第 1.1 节的场景:后台刚上架的商品,搜索框里可能 1 秒后才出现——这是设计取舍,不是 bug。
4.4 translog:防止内存数据丢失
「等 1 秒才可搜索」还能忍,「节点断电内存数据全丢」不能忍。ES 的解法是写一份操作日志(translog):
写入请求 ──▶ 内存缓冲(1 秒后 refresh 可搜索)
└────▶ translog 日志(按配置定期刷入磁盘)
节点宕机重启后,从 translog 重放未落盘的数据简化理解:内存数据负责「快」,translog 负责「稳」。极端情况下(比如两次刷盘之间宕机)仍有极小窗口的数据丢失可能,这也是 ES 不被当作「强一致主库」使用的原因之一。segment 的合并、commit 细节属于进阶话题,本指南止步于此,需要时看第 11 章链接。
5. Java 集成:连上 ES
概念打底完毕,开始写代码。第一步:让 Spring Boot 项目里有一个可用的 ES 客户端。
5.1 Maven 依赖
ES 官方为 Java 提供Java API Client(8.x 起的主流客户端)。在 pom.xml 中加入:
xml
<!-- pom.xml(教学示意:只列出 ES 相关依赖,其余省略) -->
<dependency>
<groupId>co.elastic.clients</groupId>
<artifactId>elasticsearch-java</artifactId>
<version>8.18.3</version>
</dependency>说明:
- 8.18.3 是 8.x 系列的最新版(2026-11 从 Maven Central 核实;8.x 之后是 9.x,9.x 的写法与本节不同,注意版本边界);
- JSON 序列化依赖 Jackson:Spring Boot 项目自带
jackson-databind,无需额外添加; - 低层 HTTP 客户端由该依赖自动引入,也无需单独添加。
5.2 yml 配置
application.yml:
yaml
# application.yml(教学示意)
elasticsearch:
host: http://localhost:9200
username: elastic # 可选:未开启安全认证时留空
password: changeme # 可选5.3 配置类:组装 ElasticsearchClient
8.x 客户端由三个组件组装而成,结构是固定的三段式:
① RestClient 低层 HTTP 客户端,管网络连接
│
▼
② RestClientTransport 传输层,把 Java 对象和 JSON 互转(配 JacksonJsonpMapper)
│
▼
③ ElasticsearchClient 你业务代码里使用的 API 客户端java
// ElasticsearchConfig.java(教学示意:省略异常处理与连接池调优)
import co.elastic.clients.elasticsearch.ElasticsearchClient;
import co.elastic.clients.json.jackson.JacksonJsonpMapper;
import co.elastic.clients.transport.ElasticsearchTransport;
import co.elastic.clients.transport.rest_client.RestClientTransport;
import org.apache.http.HttpHost;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.impl.client.BasicCredentialsProvider;
import org.elasticsearch.client.RestClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class ElasticsearchConfig {
@Value("${elasticsearch.host}")
private String host;
@Value("${elasticsearch.username:}")
private String username;
@Value("${elasticsearch.password:}")
private String password;
@Bean(destroyMethod = "close")
public ElasticsearchClient elasticsearchClient() {
// ① 低层客户端:负责 HTTP 连接
RestClient.Builder builder = RestClient.builder(HttpHost.create(host));
// 配置了账号密码时,附上认证信息
if (username != null && !username.isEmpty()) {
BasicCredentialsProvider credentials = new BasicCredentialsProvider();
credentials.setCredentials(
AuthScope.ANY,
new UsernamePasswordCredentials(username, password));
builder.setHttpClientConfigCallback(
hc -> hc.setDefaultCredentialsProvider(credentials));
}
// ② 传输层:负责对象与 JSON 互转
ElasticsearchTransport transport =
new RestClientTransport(builder.build(), new JacksonJsonpMapper());
// ③ 业务代码使用的客户端
return new ElasticsearchClient(transport);
}
}之后在任何类里注入即可:
java
@Service
public class ProductSearchService {
private final ElasticsearchClient esClient;
public ProductSearchService(ElasticsearchClient esClient) {
this.esClient = esClient;
}
// 后续章节的方法都写在这个类里
}5.4 看到旧代码里的 RestHighLevelClient?
网上大量教程用 RestHighLevelClient,包括很多公司 7.x 时代的存量代码。请注意:该客户端已被官方弃用,其构件版本停在 7.17.x(Maven Central 已无 8.x 版本),8.x 起一律使用本节的 ElasticsearchClient。遇到旧代码时按本章模式改写成新客户端即可,不要在 8.x 项目里继续引入它。
6. 写入商品
6.1 商品模型
先定义一个最简单的商品类(教学示意:无 Lombok,普通 POJO):
java
// Product.java(教学示意:字段精简的普通 POJO)
public class Product {
private String id; // 商品编号,同时用作 Document 的 _id
private String name; // 名称 → text,全文搜索
private String category; // 分类 → keyword,精确过滤
private Double price; // 价格
private String description; // 描述 → text,全文搜索
public Product() {}
public Product(String id, String name, String category, Double price, String description) {
this.id = id;
this.name = name;
this.category = category;
this.price = price;
this.description = description;
}
public String getId() { return id; }
public void setId(String id) { this.id = id; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getCategory() { return category; }
public void setCategory(String category) { this.category = category; }
public Double getPrice() { return price; }
public void setPrice(Double price) { this.price = price; }
public String getDescription() { return description; }
public void setDescription(String description) { this.description = description; }
}按第 2.5 节规则:
name、description用于搜索,属于text;category用于筛选和聚合,属于keyword。依赖动态映射时 ES 会按 JSON 类型自动推断,教学示意里不手动建 Mapping。
6.2 单条写入
写入一条数据在 ES 里叫索引(index)——注意这里的 index 是动词(把文档放进 Index 的动作),与名词 Index(索引库)、名词 index(倒排索引)是三种不同用法,本指南统一用「写入」表达这个动作,避免歧义。
java
// ProductSearchService 内(教学示意:省略异常处理)
public String save(Product product) throws IOException {
IndexResponse response = esClient.index(i -> i
.index("products") // 目标 Index 名(不存在会自动创建)
.id(product.getId()) // 指定 Document 的 _id,相同 _id 覆盖旧数据
.document(product) // Java 对象自动序列化为 JSON
);
return response.result().name(); // 如 "Created"(新建)或 "Updated"(覆盖)
}写入成功后(约 1 秒,见第 4.3 节),这条商品就能被搜索到。
6.3 批量写入
逐条调用 index() 意味着每条数据一次 HTTP 往返,上万条商品初始化时会非常慢。批量接口 Bulk 把多条操作打包成一个请求:
java
// ProductSearchService 内(教学示意:省略异常处理)
public void saveAll(List<Product> products) throws IOException {
BulkRequest.Builder bulk = new BulkRequest.Builder();
for (Product p : products) {
bulk.operations(op -> op
.index(idx -> idx
.index("products")
.id(p.getId())
.document(p)));
}
BulkResponse response = esClient.bulk(bulk.build());
// 批量请求整体成功,不代表每条都成功,需要逐条检查
if (response.errors()) {
for (BulkResponseItem item : response.items()) {
if (item.error() != null) {
// item.error().reason() 是该条失败的原因
System.out.println("写入失败: " + item.error().reason());
}
}
}
}关键点:response.errors() 为 true 时,说明这批里至少有一条失败,成功和失败会混在一起,生产代码必须像上面这样逐条检查。
6.4 什么时候用批量
单条写入:业务实时写入(用户提交一条商品)→ save()
批量写入:初始化导入、定时同步(几万条起步)→ saveAll()比 BulkRequest 更进一步的 BulkIngester(自动攒批、自动刷盘)适合持续大流量场景,教学阶段了解存在即可,见第 11 章链接。
7. DSL 查询入门
查询是 ES 的精华。这一章逐个认识最常用的四种查询,判定依据已在第 2.5 节定义,选型时回链到那里。
7.1 match:全文搜索的主力
match 会把查询词分词后拿去匹配 text 字段,支持「搜『可乐』命中『冰镇可乐』」:
java
// 搜索名称或描述里包含关键词的商品
public List<Product> searchByName(String keyword) throws IOException {
SearchResponse<Product> response = esClient.search(s -> s
.index("products")
.query(q -> q
.match(m -> m
.field("name") // 在 name 字段上做全文匹配
.query(keyword))),// 查询词会先被分词
Product.class); // 命中结果反序列化为 Product
List<Product> result = new ArrayList<>();
for (Hit<Product> hit : response.hits().hits()) {
result.add(hit.source()); // hit.source() 是命中的商品对象
}
return result;
}几个新手必懂的响应结构:
SearchResponse
└── hits 命中信息
├── total 总命中条数(可能带 relation 标记:eq 精确 / gte 至少)
└── hits[] 本页结果列表
├── source() 商品对象(你想要的业务数据)
└── score() 相关性评分,越高越靠前(第 1.3 节)match 的典型行为(教学示意,按第 2.3 节分词规则):
| 查询词 | 行为 |
|---|---|
可乐 | 分词成 [可乐],命中名称含「可乐」的所有商品 |
可乐 雪碧 | 分词成 [可乐, 雪碧],命中含任一词的商品,两个都含的评分更高 |
COLA | 分词时转小写,能否命中中文名取决于分词器配置 |
7.2 term:精确匹配的筛子
term 不分词,拿原值精确比对,配 keyword 字段使用(回链第 2.5 节规则 2):
java
// 筛选 category 精确等于「饮料」的商品
SearchResponse<Product> response = esClient.search(s -> s
.index("products")
.query(q -> q
.term(t -> t
.field("category")
.value(v -> v.stringValue("饮料")))), // 原样比对,不分词
Product.class);用错时的现象很典型:如果拿 term 去查 text 字段的「冰镇可乐」,会查不到——因为 text 字段在倒排索引里存的是分词后的 冰镇、可乐,不存在完整的「冰镇可乐」这个词。反过来,拿 match 查 keyword 字段时,只有当整个查询词与字段值完全相等时才命中(等于绕了个弯的 term),「部分匹配」能力丢失。这就是 2.5 节「交叉使用得不到预期的搜索结果」的含义。
7.3 range:范围查询
价格区间、时间范围用 range:
java
// 价格在 [1, 10) 之间的商品
SearchResponse<Product> response = esClient.search(s -> s
.index("products")
.query(q -> q
.range(r -> r
.number(n -> n
.field("price")
.gte(1.0) // >= 1
.lt(10.0)))),// < 10
Product.class);gt/gte/lt/lte 四个边界参数 = 大于 / 大于等于 / 小于 / 小于等于,和 SQL 的 >/>=/</<= 一一对应。
7.4 bool:把查询组合起来
真实搜索几乎都是多条件:「名称含关键词 并且 分类是饮料 并且 价格低于 10」。bool 查询就是用来组合的,它有三个常用位置:
must 必须满足(类似 AND),参与评分 → 影响「排前面」的条件放这里
filter 必须满足(类似 AND),不参与评分 → 纯筛选条件放这里,性能更好
should 应该满足(类似 OR / 加分项) → 「最好满足」的条件放这里java
// 名称含「可乐」,分类必须是饮料,价格低于 10
SearchResponse<Product> response = esClient.search(s -> s
.index("products")
.query(q -> q
.bool(b -> b
.must(m -> m // ① 全文搜索 + 参与评分
.match(mm -> mm
.field("name")
.query("可乐")))
.filter(f -> f // ② 精确筛选 + 不评分(可缓存)
.term(t -> t
.field("category")
.value(v -> v.stringValue("饮料"))))
.filter(f -> f // ③ 范围筛选
.range(r -> r
.number(n -> n
.field("price")
.lt(10.0))))))
Product.class);must 和 filter 结果相同,但 filter 不影响评分且能被缓存——「参与排名」用 must,「只要满足」用 filter,是新手最容易内化的取舍。
7.5 查询类型怎么选(速记表)
| 需求 | 查询 | 判定依据(回链) |
|---|---|---|
| 关键词搜商品,容忍部分匹配 | match | 查询词分词(2.5 规则 2) |
| 分类等于「饮料」 | term + keyword | 字段不分词 + 查询不分词(2.5 规则 1、2) |
| 价格/时间区间 | range | 数值/日期字段 |
| 多个条件组合 | bool | must/filter/should 语义(7.4) |
8. 分页、排序与高亮
上一章查出了结果,这一章让结果「好看」:翻页、按价格排、把命中词标红。最后用一个完整示例收束 5~8 章。
8.1 from/size 分页
ES 分页用 from(跳过前 N 条)+ size(返回条数):
java
// 第 2 页:跳过前 10 条,取 10 条
SearchResponse<Product> response = esClient.search(s -> s
.index("products")
.query(q -> q.match(m -> m.field("name").query("可乐")))
.from(10) // (页码 - 1) * 每页条数
.size(10), // 每页条数
Product.class);深分页提示:
from越大,ES 要在各分片上跳过越多的数据再汇总,性能越差。翻到几百页之后会明显变慢,且「from + size」合计默认上限 10000 条。真实系统的深翻页需求应改用search_after等方案(进阶话题,见第 11 章链接),教学场景记住「别让用户翻太深」即可。
8.2 排序
sort 支持按多个字段排序,并可用 SortOrder.Asc / SortOrder.Desc 指定方向:
java
SearchResponse<Product> response = esClient.search(s -> s
.index("products")
.query(q -> q.match(m -> m.field("name").query("可乐")))
.sort(so -> so // ① 价格升序
.field(f -> f
.field("price")
.order(SortOrder.Asc)))
.sort(so -> so // ② 同价按评分降序(默认)
.score(sc -> sc.order(SortOrder.Desc))),
Product.class);注意:不写 sort 时 ES 默认按相关性评分降序返回;一旦指定了排序字段,评分排序就被取代,上面的例子因此显式把评分作为第二排序条件。
8.3 高亮
高亮让命中词包上 HTML 标签(如 <em>),前端直接渲染即可:
java
SearchResponse<Product> response = esClient.search(s -> s
.index("products")
.query(q -> q.match(m -> m.field("name").query("可乐")))
.highlight(h -> h
.fields("name", hf -> hf // 对 name 字段高亮
.preTags("<em>") // 命中词前插入
.postTags("</em>"))), // 命中词后插入
Product.class);
for (Hit<Product> hit : response.hits().hits()) {
// highlight() 返回 Map<字段名, 高亮片段列表>;未命中的字段不在 Map 里
List<String> fragments = hit.highlight().get("name");
if (fragments != null) {
// 例如:["冰镇<em>可乐</em>,500ml"]
System.out.println(fragments.get(0));
}
}8.4 完整示例:一个真正的搜索接口
把 5~8 章串起来——关键词搜索 + 筛选 + 分页 + 排序 + 高亮:
java
// ProductSearchService 内(教学示意:省略参数校验与异常处理)
public List<SearchResult> search(SearchCondition condition) throws IOException {
SearchResponse<Product> response = esClient.search(s -> s
.index("products")
.query(q -> q
.bool(b -> b
.must(m -> m
.match(mm -> mm
.field("name")
.query(condition.getKeyword())))
.filter(f -> f
.term(t -> t
.field("category")
.value(v -> v.stringValue(condition.getCategory()))))
.filter(f -> f
.range(r -> r
.number(n -> n
.field("price")
.lte(condition.getMaxPrice()))))))
.from(condition.getFrom())
.size(condition.getSize())
.sort(so -> so.field(f -> f.field("price").order(SortOrder.Asc)))
.highlight(h -> h
.fields("name", hf -> hf
.preTags("<em>")
.postTags("</em>"))),
Product.class);
List<SearchResult> results = new ArrayList<>();
for (Hit<Product> hit : response.hits().hits()) {
Product product = hit.source();
SearchResult result = new SearchResult(product);
List<String> fragments = hit.highlight().get("name");
result.setHighlightedName(
fragments != null ? fragments.get(0) : product.getName());
results.add(result);
}
return results;
}到这里,「为什么 + 怎么做」的主线已经闭环:概念解释了机制,代码完成了商品搜索的完整链路。
9. 聚合入门
搜索之外,ES 的另一个看家本领是聚合(Aggregation):对命中数据做分组统计,类似 SQL 的 GROUP BY。
9.1 需求:按分类统计商品数
搜索「可乐」时,侧边栏想展示「饮料 23 个、零食 5 个」这样的分类计数,方便用户继续筛选。用 terms 聚合实现「按某字段分组计数」:
java
// 按 category 分组统计「可乐」命中商品的个数
SearchResponse<Void> response = esClient.search(s -> s
.index("products")
.size(0) // 只要统计,不要商品明细
.query(q -> q.match(m -> m.field("name").query("可乐")))
.aggregations("by_category", a -> a // 聚合名自定义,响应里用它取值
.terms(t -> t
.field("category")
.size(10))), // 只取计数最多的前 10 个分类
Void.class); // 不关心明细时用 Void.class9.2 解析聚合结果
java
Aggregate aggregate = response.aggregations().get("by_category");
for (StringTermsBucket bucket : aggregate.sterms().buckets().array()) {
System.out.println(bucket.key() + " 有 " + bucket.docCount() + " 个");
}
// 输出示意:
// 饮料 有 23 个
// 零食 有 5 个读响应时记住两个约定:aggregations().get(聚合名) 取结果;sterms() 对应 terms 聚合(s 开头表示字符串分组)。不同聚合类型的取法不同,但模式一致。
9.3 聚合与查询的关系
搜索请求 = query(先缩小范围) + aggregation(再对命中数据统计)上面例子的语义是「在名称含『可乐』的商品里,按分类统计」。query 和 aggregation 是先后关系,不是二选一——这也正是电商筛选侧边栏的标准做法。
9.4 常见聚合一览
| 聚合 | 用途 | 类比 SQL |
|---|---|---|
terms | 按字段值分组计数 | GROUP BY + COUNT(*) |
avg / max / min / sum | 数值统计 | AVG() / MAX() / MIN() / SUM() |
histogram | 按区间分桶(如价格每 50 元一档) | 分组区间统计 |
10. 速查清单
10.1 术语表(本指南单一口径)
| 术语 | 含义 | 易混点 |
|---|---|---|
index(小写) | 倒排索引:词 → 文档编号的数据结构 | 不是存数据的地方 |
Index(大写) | 索引库:存数据的容器,相当于表 | 内部靠倒排索引实现检索 |
| 写入(index 动词) | 把文档放进 Index 的动作 | 本指南统一说「写入」 |
| Document | 一条记录,JSON 对象 | 相当于行 |
| Field | 文档里的键 | 相当于列 |
| Mapping | 字段类型与索引规则 | 相当于 Schema |
| Query DSL | ES 的查询语言 | 相当于 SQL |
| Shard / Replica | 分片 / 副本分片 | 解决「大」/ 解决「挂」 |
10.2 查询类型选择表
| 触发条件 | 选择 |
|---|---|
| 用户输入的搜索词,容忍部分匹配 | match + text 字段 |
| 分类、状态等固定枚举值精确匹配 | term + keyword 字段 |
| 价格、时间区间 | range |
| 多条件组合 | bool:参与排名用 must,纯筛选用 filter |
| 只统计不取明细 | 查询 + aggregations,size(0) + Void.class |
10.3 常见坑
① match 与 term 混用:拿 term 查 text 字段查不到结果;拿 match 查 keyword
会丢失部分匹配能力。
→ 记住 2.5 节:字段类型与查询类型要同侧对齐。
② 忘记「近实时」:写入后立刻搜,发现没有,以为写丢了。
→ refresh 默认约 1 秒,稍等再搜(4.3 节)。
③ 深分页:from 越翻越大,越来越慢,且 from+size 默认上限 10000。
→ 教学场景限制翻页深度;生产深翻页研究 search_after(8.1 节)。
④ 用旧客户端:照着旧教程抄 RestHighLevelClient,8.x 项目里找不到类。
→ 8.x 一律使用 Java API Client 的 ElasticsearchClient(5.4 节)。
⑤ 错把 ES 当主库:用 ES 存强事务数据,出了事故怪 ES。
→ ES 是搜索专用系统,主数据仍在数据库(1.4 节)。11. 扩展阅读
11.1 为什么还能更快:倒排索引的工程优化(概述)
第 2 章的倒排索引模型解释了「为什么快」,ES 内部还叠加了三层工程优化,让「词 → 文档」的查询从快变得极快(了解即可):
- Term Index(FST):词典里的词太多不可能全放内存,用有限状态转换器(Finite State Transducers)压缩词的前缀放在内存,空间小、查询快;
- Posting List 压缩(FOR):文档编号列表用 Frame Of Reference 编码压缩,省磁盘;
- Roaring Bitmap:多条件查询要做文档编号的交集/并集,用位图结构快速计算。
11.2 相关性评分 BM25(概述)
第 1.3 节说结果按 _score 排序。8.x 默认评分算法是 BM25,直觉理解:一个词在越少的文档里出现、在当前文档里出现次数越多,说明它对这篇文档越重要——「可乐」出现在 100 个商品里权重低,只出现在 2 个商品里权重就高。评分调试属于进阶话题,入门阶段先知道「有评分、可排序」即可。
11.3 进阶路线
- 手动 Mapping:控制
text/keyword类型与 IK 分词器; search_after深分页、BulkIngester流式批量写入;- 数据库 → ES 的数据同步方案(如 Logstash、Canal);
- 集群规划与调优。
References
本指南的版本敏感事实(客户端版本、API 形态、弃用状态)在 2026-11 撰写时核实自以下官方主源:
- Java API Client 8.19 官方文档:安装(installation)、连接(connecting)、单条写入(indexing)、批量写入(indexing-bulk)、搜索(searching)、聚合(aggregations)
- elasticsearch-java 8.19 分支生成源码(GitHub):
FieldSort.java(sort 的field/order方法)、HighlightBase.java(preTags/postTags方法) - Maven Central 构件查询:
co.elastic.clients:elasticsearch-java8.x 最新版为 8.18.3;org.elasticsearch.client:elasticsearch-rest-high-level-client最高版为 7.17.29(无 8.x 版本) - 概念章节的动机与结构参考:掘金《「扫盲」Elasticsearch》(Java3y,2020)