每次练习先从 Elastic 官方版本目录解析最新稳定版本,再拉取对应的官方镜像,启动一个只在本机开放 HTTP 的三节点集群。一次实验固定一个版本,结果同时记录镜像标识和服务器版本;下一次用新的空环境验证升级后的行为。

本章为前面的原理章节提供运行环境,直接复用《08 补充:查询为什么没命中》的 mapping、三份文档和查询附件。它不另写一套字段示例,也不把“请求返回 200”当成“命中结果正确”。

这是无认证、无 TLS 的本地实验环境,不是生产部署模板。 只有第一个节点的 HTTP 端口映射到 127.0.0.1:19200,节点间的 transport 通信留在容器网络中。默认关闭机器学习,插件、模型与安全实验需要另行准备。

先定义镜像、节点、集群与实验数据

镜像是启动程序和依赖的发行包。tag 是版本名称,image ID 与 digest 用于记录实际拉取的内容;容器是运行中的一个 Elasticsearch 节点。三个节点加入相同的 cluster,并通过容器网络发现彼此。

HTTP 的 9200 端口供 curl、Postman 或应用提交 REST 请求。transport 的 9300 端口供节点内部通信,不能拿它当 Search API 地址。映射后的本机端口是 19200,因此实验请求的完整地址是 http://127.0.0.1:19200/es-query-lab-v1/_search。

三节点不是三个独立集群,也不是每次查询都要请求三个地址。收到 HTTP 请求的节点可以协调其他节点完成操作。三个容器仍共享同一台主机,不能验证机房级高可用或真实生产性能。

flowchart TD
    V["官方版本目录:选稳定 X.Y.Z"] --> P["每次 pull 官方镜像并记录标识"]
    P --> N["独立容器网络"]
    C["curl / Postman / 实验执行器"] --> H["127.0.0.1:19200"]
    subgraph L["本机 Linux 容器环境"]
      H --> A["节点 1:HTTP + transport"]
      N --> A
      N --> B["节点 2:transport"]
      N --> D["节点 3:transport"]
      A <--> B
      B <--> D
      D <--> A
    end
    F["08 补充的 mapping-lab.http"] --> C
    C --> R["results.json:版本、预期、实际响应"]

准备 Docker 或 Podman

需要 Python 3.10 或以后版本,以及一个可用的 Docker 或 Podman Linux 容器环境。脚本只用 Python 标准库,不需要 Compose、第三方 Python 包或 ES 客户端库。

三个节点各设置 2 GiB 容器内存和 1 GiB JVM heap。建议给容器虚拟机预留至少 8 GiB 内存和 10 GiB 可用磁盘;这是本实验的资源预算,不是官方生产容量建议。模型推理、更多数据或 Kibana 会增加开销。

Docker Desktop 启动后先检查:

1
docker info

Podman 在 Linux 上可以直接运行;macOS 和 Windows 的容器运行在 Linux 虚拟机中。已有 machine 时先启动它,没有 machine 时才初始化:

1
2
3
4
podman machine list
podman machine init --cpus 4 --memory 8192 --disk-size 30
podman machine start
podman info

不要在已有虚拟机上重复执行 init。脚本不会自动创建虚拟机、修改主机 sysctl 或使用 privileged 容器;它只消费已经可用的容器环境。

Elastic 的容器运行要求目前要求 Linux 的 vm.max_map_count 为 1048576。在原生 Linux 上可检查并临时设置:

1
2
sysctl vm.max_map_count
sudo sysctl -w vm.max_map_count=1048576

Podman machine 的设置发生在 Linux VM 内,而不是 macOS 内核:

1
2
podman machine ssh 'sysctl vm.max_map_count'
podman machine ssh 'sudo sysctl -w vm.max_map_count=1048576'

Docker Desktop 使用其 Linux VM;WSL 后端也应在对应 Linux 环境设置,按官方平台说明处理。重启后应重新检查。遇到 bootstrap check 失败时不能靠关闭检查来绕过环境要求。

模式提炼:把运行环境作为实验前提

程序版本 + 内核条件 + 资源预算 + 输入数据 → 可解释的观察结果。查询失败、节点启动失败和内存不足是不同问题,应分别收集查询响应、容器日志和资源信息。环境不满足时,不能把失败算成 Query DSL 的反例。

启动:每次解析最新稳定版本并 pull

下载 集群启动与实验执行脚本 和 配套测试。从《查询为什么没命中》下载 mapping-lab.http,将三个文件放在同一个实验目录中。

脚本的 up 每次访问 Elastic 官方 artifacts 版本接口,仅接受 X.Y.Z 形式的稳定版本,从中按数值选取最高版本,排除 SNAPSHOT、alpha、beta 和 rc。然后始终执行:

1
docker/podman pull docker.elastic.co/elasticsearch/elasticsearch:X.Y.Z

官方Docker 安装教程同样使用版本化的官方镜像地址。本实验不依赖一个叫 latest 的 tag;“每次找最新”与“使用 latest tag”是两件事。2026-10-09 的接口查询解析为 9.5.5,这只是核对时的结果,代码没有写死它。

版本接口是这个启动器的发现入口,不是保证永久不变的公共兼容协议。接口不可达、返回结构改变或对应镜像不能 pull 时,脚本报错,不静默使用缓存或降低版本;可以在确认目标版本后用 --version X.Y.Z 复现实验。即使指定版本,仍会执行 pull。下载到的镜像内容可以被容器引擎复用,不等于每次重新下载全部层。

先使用 Podman:

1
python3 es_lab.py up --engine podman --workdir lab-current

Docker 的命令只替换 engine:

1
python3 es_lab.py up --engine docker --workdir lab-current

两条命令择一执行。启动器先检查容器引擎,再创建实验目录;已有目录会拒绝覆盖。容器名和网络名带有本次随机标识,数据留在容器的实验存储中,不复用旧版本的数据目录。

启动时三个节点都有 master、data、ingest 等实验所需角色。首次建立集群配置 cluster.initial_master_nodes;等到三节点加入、健康为 green 后,脚本从绑定的配置文件中移除这一引导设置,再写入 READY 状态。这遵循首次集群引导规则,后续重启不会重新配置初始投票节点。

lab-current/state.json 保存版本、容器引擎、网络、容器名、镜像 inspect、服务器信息和健康响应。镜像拉取成功还不够:脚本等待三个节点并核对实际服务器版本,默认等待上限为 240 秒;超时保留状态和容器,便于检查日志。

1
2
3
curl --fail-with-body http://127.0.0.1:19200/
curl --fail-with-body 'http://127.0.0.1:19200/_cluster/health?pretty'
curl --fail-with-body 'http://127.0.0.1:19200/_cat/nodes?v'

根响应应显示 cluster_name: es-http-tutorial,节点数应为 3。具体版本以此次服务器返回为准。

模式提炼:发现版本与固定实验分开

发现最新版本 → pull → 记录实际内容 → 固定这一轮输入和环境。后续比较用 state 和结果记录,不用“当时应该是最新版”作证据。指定旧版本重跑与启动最新版本可以共用同一套脚本,只改变版本参数和实验目录。

实用《08 补充》的实验附件

原附件有 137 个请求,其中 environment 组 3 个、core 组 96 个。默认执行这 99 个请求,顺序仍由原文件决定:检查环境、创建 mapping、写入三份文档,再执行查询和观察操作。

1
2
3
python3 es_lab.py run \
--http mapping-lab.http \
--output results-current

执行器只接受带端口的 http://127.0.0.1 地址,并检查集群名是 es-http-tutorial,避免把这套写入实验误指向别的集群。这个检查用于防止误操作,不是身份认证。

每个请求都保存到 results-current/results.json,包含案例编号、请求 body、原来的文字预期、HTTP 状态和完整响应。结果目录已存在时拒绝覆盖。发生网络错误也会记录;mapping 或文档写入失败时停止,不继续输出一串缺失索引的查询错误。其他查询失败会记录并继续,最后以非零退出码提示检查。

结果的几个状态含义不同:

状态 实际验证了什么
HTTP_OK 收到成功 HTTP 响应,仍需检查命中、排序、分数或聚合结果
PASS_HITS Q01、Q02、Q03、Q05、Q06 的命中文档集合与总数已通过明确断言
EXPECTED_HTTP_ERROR negative 组返回 4xx;还要核对实际错误原因是否对应原预期
FAIL / FAIL_HITS HTTP 状态异常,或有断言的命中集合不符
TRANSPORT_ERROR 请求未完成,不能解释为查询无命中

Q01 默认 OR 预期命中 1、2;Q02 的 AND 和 Q03 的短语查询预期仅命中 1;Q05 的 text 单词项查询预期无命中;Q06 的 keyword normalizer 查询预期命中 1。这几条正好把查询分析、词项匹配和完整值规范化联系起来。

HTTP_OK 没有自动变成 PASS。其余查询中的排序、数值精度、BM25、空间关系和聚合应对照响应逐项核实。脚本不改写原覆盖文件的 NOT_RUN;只有真正取得响应、完成对应断言的结果,才能成为这一版本的新证据。

主实验跑完后,可以单独重跑一组查询:

1
2
python3 es_lab.py run --http mapping-lab.http --output results-phrases \
--case Q01 --case Q02 --case Q03 --case Q05 --case Q06

故意失败的请求另跑:

1
2
python3 es_lab.py run --http mapping-lab.http --output results-negative \
--group negative

不要在同一索引上再次运行完整默认组:SETUP01 创建已存在索引会报错。重查已有文档用 --case;从建表开始重跑用一个新的空集群,或为原请求统一换一个实验索引后缀。脚本不自动删除已存在索引。

curl 与 Postman 使用同一个集群

原附件中的 POST /es-query-lab-v1/_search 是 Kibana Console 的相对地址形式,不能直接粘贴到普通终端。curl 要带完整地址:

1
2
3
4
5
curl --fail-with-body \
--request POST \
'http://127.0.0.1:19200/es-query-lab-v1/_search' \
--header 'Content-Type: application/json' \
--data-binary '{"query":{"match_phrase":{"title":"don\u0027t have"}}}'

JSON 的 \u0027 表示单引号,避免和 shell 的外层单引号冲突。使用《08 补充》的 curl 导出器也可以保留原输入:

1
2
export ES_URL='http://127.0.0.1:19200'
node export-mapping-lab.mjs curl Q01 Q03

若导入原 Postman collection,将集合变量 baseUrl 改为 http://127.0.0.1:19200,认证选择 No Auth,再按单个案例执行。已有 Kibana 时可在对应集群的 Dev Tools 使用相对请求;本章不会另外部署 Kibana。

扩展组不能跟着 core 全选运行

join、percolator、dynamic 可以在检查各自前提后单独运行;插件组需要带插件的同版本镜像,NEW 组要求目标版本支持全部配置,语义组要求真实推理端点、模型及许可。最新版本也不等于拥有全部插件、全部模型或全部订阅功能。

时间序列附件使用固定的样本时间和索引边界,升级重跑时应一起核对,而不是只把文档日期改成今天。原实验手填的向量仍然只演示运算,不会因为集群是真的就自动变成模型语义实验。

机器学习在本环境关闭,认证与 TLS 也关闭,因此第 15 章的用户、角色、API key 与加密实验不能在这个基线直接验证。需要安全实验时,应采用官方启用安全的部署流程,使用另一套环境,不在运行中的无认证集群上混用两种前提。

用同一环境练习系列中的其他知识

系列章节 在这个集群中能观察的操作 需要保留的边界
00、02、03、04、07、08、09 与 08 补充 _analyze、_termvectors、_mapping、_explain、_segments、查询、评分与聚合 原字段和请求继续复用;分数可能随数据、分片和版本变化
01、10、11、12、16 _cat/nodes、_cat/shards、_cluster/state、路由、副本及单节点故障恢复 三节点共享一台宿主机,不能证明跨主机高可用
05、06、14 refresh/flush、profile、统计与监控 小样本可以观察机制,不支持生产吞吐和延迟结论
13 与经典架构模式 ILM、alias、rollover、reindex;独立实验索引 冷热分层、快照仓库和跨集群检索还需额外拓扑或存储
15、17 安全前提对照与产品机制比较 本环境不验证认证加密,也不包含 Solr

原实验索引设置一个主分片、零副本,适合比较查询,不能直接用它证明副本容错。另建一个小索引做第 10、11 章实验:

1
2
3
4
5
6
7
8
9
10
11
12
curl --fail-with-body --request PUT \
'http://127.0.0.1:19200/es-lab-ha' \
--header 'Content-Type: application/json' \
--data-binary '{"settings":{"number_of_shards":3,"number_of_replicas":1},"mappings":{"properties":{"message":{"type":"text"}}}}'

curl --fail-with-body --request PUT \
'http://127.0.0.1:19200/es-lab-ha/_doc/1?refresh=wait_for' \
--header 'Content-Type: application/json' \
--data-binary '{"message":"replica recovery example"}'

curl --fail-with-body 'http://127.0.0.1:19200/_cluster/health/es-lab-ha?wait_for_status=green&timeout=30s'
curl --fail-with-body 'http://127.0.0.1:19200/_cat/shards/es-lab-ha?v'

先确认三个节点加入和副本分配完成,再从 state.json 取出第 3 个容器名,仅停止它:

1
2
3
4
5
ESLAB_NODE3=$(python3 -c 'import json; print(json.load(open("lab-current/state.json"))["containers"][2])')
podman stop "$ESLAB_NODE3"
curl --fail-with-body 'http://127.0.0.1:19200/_cluster/health?pretty'
curl --fail-with-body 'http://127.0.0.1:19200/es-lab-ha/_search?pretty'
podman start "$ESLAB_NODE3"

Docker 环境将两条 podman 命令替换为 docker。两个 master-eligible 节点仍在,通常可以继续选举与服务;状态可能在恢复过程中变化,不能承诺一定长期停在 yellow。检查 health、shards 和搜索响应,区分“节点少了”“副本待恢复”和“数据不可读”。不要连续停止第二个节点来套用单节点故障结论。

保留证据、清理和下次升级

完成实验后,先保留 state.json 与 results.json。下面的命令会删除本次容器、其匿名数据卷及网络,实验数据不可恢复;镜像、配置文件和结果记录保留:

1
python3 es_lab.py down --workdir lab-current --discard-data

启动器通过状态文件定位确切资源,清理前核对所有现存资源的 owner 标签。标签不符就拒绝删除,不调用系统级 prune,也不删除其他网络或镜像。启动一半失败时也可用相同命令清理已创建的资源。

下一次使用新的目录重新解析最新版:

1
2
python3 es_lab.py up --engine podman --workdir lab-next
python3 es_lab.py run --http mapping-lab.http --output results-next

这是重新建立空实验集群,不是生产滚动升级。旧数据目录不挂到新主版本容器,原请求和样本文档重新写入;比较两份响应时可以把版本差异与输入差异分开。

需要固定版本复现时:

1
python3 es_lab.py up --engine podman --version 9.5.5 --workdir lab-pinned

同一默认端口不能同时被两个集群使用。保留旧集群做并排对照时,第二个使用 --port 19210,其执行器也指定 --base-url http://127.0.0.1:19210。

模式速查

需求 操作 证据
跟随最新稳定版本 新目录执行 up,不指定 version 官方版本目录、pull 结果、镜像 inspect、服务器版本
复现旧观察 指定 version,保持同一 mapping/doc/request 固定版本及逐条实际响应
验证查询结果 run 的请求记录加命中/排序/聚合断言 PASS_HITS 或人工核对结果,不仅是 HTTP_OK
清空实验 down --discard-data owner 校验与保留的状态、结果文件

故障定位与验证状态

启动失败时先查看 state.json 中的具体容器名:

1
podman logs --tail 100 '<state.json 中的容器名>'

错误中的 vm.max_map_count 对应 Linux 环境配置,退出码 137 常需要检查内存或容器事件,address already in use 对应本机端口冲突。discovery 错误应检查容器网络的 DNS 与节点名;Podman 的自定义网络需要名称解析能力。通过端口发布规则将 HTTP 限制在 loopback;Docker 文档还指出旧于 28.0.0 的版本存在同二层网络访问 localhost 发布端口的历史问题,应使用已修复的引擎,不把实验端口暴露给不可信网络。

脚本测试可以独立运行:

1
python3 -B -m unittest -v test_es_lab.py

编写时核对了官方版本接口,离线测试覆盖稳定版本筛选、启动命令、bootstrap 设置移除、资源归属与请求解析,并使用临时本机 HTTP 服务验证了请求发送、单引号输入和结果记录。真实 Docker/Podman 三节点启动以及原附件在 ES 上执行仍为 NOT_RUN:验证机器没有 Docker,也没有已创建的 Podman machine。模拟服务通过不代表 ES 接受了 mapping;实际实验的结果文件才用于填补这个验证空缺。

系列导航

参考资料