Elasticsearch 使用手册
索引创建、文档读写、全文检索、过滤、分页与聚合的常用写法。
REST API 与 DSL 的关系
Elasticsearch 通过 REST API 操作索引和文档,Query DSL 用 JSON 编写查询条件。下面的请求搜索名称包含 wireless 或 mouse 的商品,最多返回 10 条。在 Kibana 的 Dev Tools 中可以直接执行。
POST /es_manual_products/_search
{
"query": { "match": { "name": "wireless mouse" } },
"size": 10
}| 概念 | 含义 | 本手册示例 |
|---|---|---|
| index | 存放文档并定义映射和设置的逻辑集合 | es_manual_products |
| document / _id | 一条 JSON 文档及其索引内标识 | 商品文档 / p001 |
| field / mapping | 文档字段及其类型、索引方式 | price_cents 为 long |
| shard / replica | 索引的分片及其副本 | 由集群设置和索引设置管理 |
| _source | 返回文档内容的来源字段 | 商品名、价格等 JSON 内容 |
| 目的 | 方法与路径 | 说明 |
|---|---|---|
| 查看版本 | GET / | 读取 version.number |
| 创建索引 | PUT /es_manual_products | 显式提供 mapping |
| 写入指定 ID | PUT /es_manual_products/_doc/p001 | 存在时替换文档内容 |
| 仅创建 | PUT /es_manual_products/_create/p001 | 已存在则返回 409 |
| 读取文档 | GET /es_manual_products/_doc/p001 | 按 ID 获取 |
| 局部更新 | POST /es_manual_products/_update/p001 | 通过 doc 或 script 更新 |
| 删除文档 | DELETE /es_manual_products/_doc/p001 | 只删除该 ID |
| 搜索 / 批量 | POST /es_manual_products/_search 或 /_bulk | JSON 查询 / NDJSON 批量操作 |
搜索请求使用 GET 或 POST,query 指定查询条件,sort 指定排序,aggs 定义聚合,_source 选择返回字段。
在终端使用 curl
export ES_URL='https://localhost:9200'
export ES_CA='/path/to/http_ca.crt'
# ES_API_KEY 由运行环境注入,值为编码后的 API Key。
curl --cacert "$ES_CA" -H "Authorization: ApiKey $ES_API_KEY" "$ES_URL/"
curl --cacert "$ES_CA" \
-H "Authorization: ApiKey $ES_API_KEY" \
-H 'Content-Type: application/json' \
-X POST "$ES_URL/es_manual_products/_search" \
--data-binary '{"query":{"match_all":{}},"size":10}'将 ES_URL 和 ES_CA 换成集群地址与证书路径。使用用户名密码认证时,把认证头换成 -u 用户名,curl 会提示输入密码。
创建索引与字段映射
PUT /es_manual_products
{
"mappings": {
"dynamic": "strict",
"properties": {
"product_id": { "type": "keyword" },
"name": {
"type": "text",
"analyzer": "standard",
"fields": { "keyword": { "type": "keyword" } }
},
"description": { "type": "text", "analyzer": "standard" },
"category": { "type": "keyword" },
"brand": { "type": "keyword" },
"price_cents": { "type": "long" },
"stock": { "type": "integer" },
"status": { "type": "keyword" },
"tags": { "type": "keyword" },
"created_at": { "type": "date" }
}
}
}
GET /es_manual_products/_mapping| 类型 | 用途 | 例子 |
|---|---|---|
| text | 分词后的全文检索 | 商品名、描述 |
| keyword | 精确过滤、排序和分组 | 品牌、状态、业务 ID |
| integer / long | 整数及范围比较 | 库存、以分为单位的价格 |
| date | 时间范围、时间排序和时间分桶 | 带时区的 ISO 8601 时间 |
| boolean | 真假状态 | 是否启用等二值字段 |
name 用于全文检索,name.keyword 用于完整名称匹配和排序,两者取自同一个 name 值。price_cents 以分为单位,19900 表示 199.00 元。tags 可以直接写入字符串数组。
dynamic: strict 拒绝未声明的字段。已有字段要改类型,需要新建索引并迁移数据。
Bulk 批量导入
POST /es_manual_products/_bulk?refresh=wait_for
{"index":{"_id":"p001"}}
{"product_id":"p001","name":"Wireless Mouse","description":"Quiet wireless mouse for office","category":"accessories","brand":"Acme","price_cents":9900,"stock":30,"status":"active","tags":["wireless","office"],"created_at":"2026-10-01T09:00:00+08:00"}
{"index":{"_id":"p002"}}
{"product_id":"p002","name":"Wireless Keyboard","description":"Compact keyboard for office","category":"accessories","brand":"Acme","price_cents":19900,"stock":12,"status":"active","tags":["wireless","office"],"created_at":"2026-10-02T09:00:00+08:00"}
{"index":{"_id":"p003"}}
{"product_id":"p003","name":"Gaming Mouse","description":"Wired mouse for gaming","category":"accessories","brand":"Nova","price_cents":29900,"stock":0,"status":"inactive","tags":["gaming"],"created_at":"2026-10-03T09:00:00+08:00"}
{"index":{"_id":"p004"}}
{"product_id":"p004","name":"USB Hub","description":"Multiport adapter for laptop","category":"adapters","brand":"Nova","price_cents":15900,"stock":8,"status":"active","tags":["office"],"created_at":"2026-10-04T09:00:00+08:00"}
Bulk 使用 NDJSON,每行一条 JSON:第一行写动作,第二行写文档,最后保留一个换行符。delete 只有动作行;update 的第二行写 doc 或 script。
Bulk 会分别处理每个动作。收到 HTTP 200 后,还要检查 errors 和 items 中的 status/error,重试失败项。脚本累加等操作重复执行会再次修改数据。
用 curl 导入时,将数据行保存为 products.ndjson,去掉 POST 首行。
curl --cacert "$ES_CA" \
-H "Authorization: ApiKey $ES_API_KEY" \
-H 'Content-Type: application/x-ndjson' \
-X POST "$ES_URL/es_manual_products/_bulk?refresh=wait_for" \
--data-binary @products.ndjsonGET /es_manual_products/_count
GET /es_manual_products/_doc/p001导入后,_count 为 4,读取 p001 返回 found: true。重新导入相同 ID 会替换对应文档。
写入成功与搜索可见
按 ID 执行 GET 可以立即读到新写入的文档,搜索则要等 refresh。写入时加上 refresh=wait_for,会等到文档可被搜索后再返回。
批量写入通常省略 refresh 参数。refresh=true 会主动刷新相关分片,频繁使用会增加写入开销。
搜索请求与响应结构
POST /es_manual_products/_search
{
"query": { "match_all": {} },
"_source": ["product_id", "name", "price_cents"],
"from": 0,
"size": 2,
"sort": [{ "product_id": "asc" }],
"track_total_hits": true
}请求返回 p001、p002,总数为 4。下面截取响应中的一条文档:
{
"took": 2,
"timed_out": false,
"_shards": { "total": 1, "successful": 1, "skipped": 0, "failed": 0 },
"hits": {
"total": { "value": 4, "relation": "eq" },
"max_score": null,
"hits": [
{
"_index": "es_manual_products",
"_id": "p001",
"_score": null,
"_source": { "product_id": "p001", "name": "Wireless Mouse", "price_cents": 9900 },
"sort": ["p001"]
}
]
}
}hits.hits 保存当前页文档,hits.total 表示匹配总量。relation 为 eq 表示精确值,gte 表示至少有这么多条;需要精确计数时设置 track_total_hits: true。_score 是相关性得分,按字段排序时通常为 null。
took 是服务端处理耗时。timed_out 表示是否超时,_shards.failed 是失败的分片数。_source 只控制返回哪些字段。
全文检索:match、match_phrase 与 multi_match
match:按词项匹配
POST /es_manual_products/_search
{
"query": {
"match": {
"name": { "query": "wireless mouse", "operator": "and" }
}
}
}match 先对查询文本分词。operator 默认是 or,wireless mouse 会匹配 p001、p002、p003;改成 and 后,两个词都要出现,只匹配 p001。
match_phrase:限制词项顺序与位置
POST /es_manual_products/_search
{
"query": { "match_phrase": { "name": "wireless mouse" } }
}match_phrase 按词项的顺序和位置匹配短语,slop 默认为 0。整个名称精确匹配使用 name.keyword。
multi_match:同时搜索多个字段
POST /es_manual_products/_search
{
"query": {
"multi_match": {
"query": "wireless",
"fields": ["name^3", "description"],
"type": "best_fields"
}
}
}name^3 将名称字段的权重设为 3。best_fields 取匹配最好的字段计算得分,名称和描述都可以命中。
通过 _analyze 检查分词
POST /es_manual_products/_analyze
{
"field": "name",
"text": "Wireless Mouse"
}返回的词项是 wireless 和 mouse。中文分词也可以用 _analyze 查看,选分析器时用实际搜索词试一下结果。
精确条件:term、terms、range 与 exists
| 查询 | 示例片段 | 语义 |
|---|---|---|
| term | {"term":{"brand":"Acme"}} | 匹配一个精确值 |
| terms | {"terms":{"brand":["Acme","Nova"]}} | 匹配集合中的任意值 |
| range | {"range":{"price_cents":{"gte":10000,"lt":20000}}} | 大于等于 100 元且小于 200 元 |
| exists | {"exists":{"field":"tags"}} | 字段存在可检索的值 |
POST /es_manual_products/_search
{
"query": { "term": { "name.keyword": "Wireless Mouse" } }
}term 直接匹配词项。这里 brand 的 Acme 与 acme 是两个值;name 经过分词,完整名称要查 name.keyword。
terms 匹配列表中的任意值。exists 检查字段是否有索引值:null 和空数组通常算缺失,空字符串算存在。
POST /es_manual_products/_search
{
"query": {
"range": {
"created_at": {
"gte": "2026-10-01T00:00:00+08:00",
"lt": "2026-11-01T00:00:00+08:00"
}
}
}
}gte 包含起点,lt 排除终点,这样可以查完整个十月。最近七天可以写 now-7d,今天零点可以写 now/d;按本地日期查询时指定时区。
bool:组合检索与业务过滤
| 子句 | 是否要求匹配 | 评分行为 |
|---|---|---|
| must | 每一项都必须匹配 | 在查询上下文中参与评分 |
| filter | 每一项都必须匹配 | 不参与评分 |
| must_not | 每一项都不得匹配 | 不参与评分 |
| should | 由 minimum_should_match 决定至少匹配几项 | 在查询上下文中可增加评分 |
POST /es_manual_products/_search
{
"query": {
"bool": {
"must": [
{ "match": { "name": "wireless" } }
],
"filter": [
{ "term": { "status": "active" } },
{ "range": { "price_cents": { "lte": 20000 } } },
{ "range": { "stock": { "gt": 0 } } }
],
"must_not": [
{ "term": { "tags": "refurbished" } }
],
"should": [
{ "term": { "brand": "Acme" } }
],
"minimum_should_match": 0
}
}
}结果为 p001、p002:名称包含 wireless,已上架、有库存,价格不超过 200 元。Acme 品牌加分,refurbished 标签被排除。状态、分类和价格范围等条件放在 filter 中,不计算相关性得分。
有 must 或 filter 时,minimum_should_match 默认是 0,should 只用于加分。只有 should 时默认是 1。下面要求品牌和标签至少匹配一项,所以显式设为 1。
POST /es_manual_products/_search
{
"query": {
"bool": {
"filter": [{ "term": { "status": "active" } }],
"should": [
{ "term": { "brand": "Acme" } },
{ "term": { "tags": "gaming" } }
],
"minimum_should_match": 1
}
}
}排序、分页与高亮
浅分页:from 与 size
POST /es_manual_products/_search
{
"query": { "term": { "status": "active" } },
"sort": [{ "price_cents": "asc" }, { "product_id": "asc" }],
"from": 0,
"size": 2,
"_source": ["product_id", "name", "price_cents"]
}结果按价格升序返回 p001、p004,同价时再按 product_id 排序。第 n 页的 from 为 (n - 1) × size。文本排序使用 keyword 字段。
from + size 默认最多为 10000。连续翻页或导出使用 search_after,配合 PIT 固定查询期间的数据视图。
深分页:PIT 与 search_after
POST /es_manual_products/_pit?keep_alive=1m
POST /_search
{
"pit": { "id": "PIT_ID", "keep_alive": "1m" },
"query": { "match_all": {} },
"sort": [{ "product_id": "asc" }, { "_shard_doc": "asc" }],
"size": 2
}PIT_ID 填创建请求返回的 id。下一页保持 query、sort 不变,把上一页最后一条的完整 sort 数组填入 search_after。响应有新的 pit_id 时使用新值。下面假设 sort 为 ["p002", 1],执行时换成实际返回值。
POST /_search
{
"pit": { "id": "LATEST_PIT_ID", "keep_alive": "1m" },
"query": { "match_all": {} },
"sort": [{ "product_id": "asc" }, { "_shard_doc": "asc" }],
"search_after": ["p002", 1],
"size": 2
}
DELETE /_pit
{
"id": "LATEST_PIT_ID"
}PIT 搜索使用 /_search。_shard_doc 用于区分排序值相同的文档,keep_alive 设置 PIT 的保留时间。逐页传递游标,结束后用 DELETE /_pit 关闭;过期后重新创建并开始查询。
高亮:返回命中文本片段
POST /es_manual_products/_search
{
"query": { "match": { "name": "wireless" } },
"highlight": {
"pre_tags": ["[hit]"],
"post_tags": ["[/hit]"],
"fields": { "name": {} }
}
}高亮片段放在每条结果的 highlight.name 数组中。前端先转义文本,再把 [hit] 标记转换为高亮样式。
聚合:分组、指标与时间统计
按分类统计上架商品
POST /es_manual_products/_search
{
"size": 0,
"query": { "term": { "status": "active" } },
"aggs": {
"by_category": {
"terms": { "field": "category", "size": 10 },
"aggs": {
"avg_price_cents": { "avg": { "field": "price_cents" } },
"stock_total": { "sum": { "field": "stock" } }
}
}
}
}size: 0 只返回统计结果。query 筛出 active 商品,aggs 对这些商品分组,不受分页影响。每个桶的 key 是分类名,doc_count 是商品数,子聚合计算均价和库存。
accessories 有 2 条,均价 14900 分、库存 42;adapters 有 1 条,均价 15900 分、库存 8。
terms 返回数量最多的前 N 个桶,多分片下计数可能有误差,可通过 shard_size 增加候选桶。遍历全部分组使用 composite 和 after_key。分组字段使用 keyword。
按业务时区统计每日新增量
POST /es_manual_products/_search
{
"size": 0,
"query": {
"range": {
"created_at": {
"gte": "2026-10-01T00:00:00+08:00",
"lt": "2026-10-08T00:00:00+08:00"
}
}
},
"aggs": {
"by_day": {
"date_histogram": {
"field": "created_at",
"calendar_interval": "day",
"time_zone": "Asia/Shanghai",
"min_doc_count": 0,
"extended_bounds": { "min": "2026-10-01", "max": "2026-10-07" }
}
}
}
}calendar_interval 按日历分桶,time_zone 指定时区。min_doc_count: 0 配合 extended_bounds 补齐没有数据的日期,query 控制统计范围。筛选商品列表又想保留筛选前的分组统计时,可用 post_filter 过滤命中结果。
文档写入、局部更新与删除
创建 p-demo,将价格从 5000 改为 4500,再读取文档。
PUT /es_manual_products/_create/p-demo?refresh=wait_for
{
"product_id": "p-demo",
"name": "Demo Mouse",
"price_cents": 5000,
"stock": 10,
"status": "active"
}
POST /es_manual_products/_update/p-demo?refresh=wait_for
{
"doc": { "price_cents": 4500 }
}
GET /es_manual_products/_doc/p-demo_create 遇到已有 ID 返回 409。PUT /_doc/ID 替换整个文档,_update 的 doc 只更新指定字段,所以改价后 stock 仍为 10。
脚本更新与 upsert
POST /es_manual_products/_update/p-demo?refresh=wait_for
{
"script": {
"lang": "painless",
"source": "ctx._source.stock += params.delta",
"params": { "delta": 2 }
}
}脚本每执行一次,库存增加 2。文档不存在时也要写入,可设置 doc_as_upsert: true,例如 {"doc":{"product_id":"p-demo","stock":10},"doc_as_upsert":true}。
并发修改:避免覆盖其他写入
GET 文档后,将 _seq_no 和 _primary_term 填入下面的 SEQ_NO、PRIMARY_TERM。期间若有其他写入,更新返回 409,需要重新读取后处理冲突。
POST /es_manual_products/_update/p-demo?if_seq_no=SEQ_NO&if_primary_term=PRIMARY_TERM&refresh=wait_for
{
"doc": { "price_cents": 4300 }
}
DELETE /es_manual_products/_doc/p-demo?refresh=wait_forDELETE 删除 p-demo。
对象数组:何时需要 nested
商品有两种规格:红色 S 码、蓝色 L 码。要让颜色和尺码匹配同一个规格,将 variants 定义为 nested。普通 object 数组会展开字段,可能把不同规格的值匹配到一起。
PUT /es_manual_variants
{
"mappings": {
"properties": {
"variants": {
"type": "nested",
"properties": {
"color": { "type": "keyword" },
"size": { "type": "keyword" }
}
}
}
}
}
PUT /es_manual_variants/_doc/v001?refresh=wait_for
{
"variants": [
{ "color": "red", "size": "S" },
{ "color": "blue", "size": "L" }
]
}
POST /es_manual_variants/_search
{
"query": {
"nested": {
"path": "variants",
"score_mode": "none",
"query": {
"bool": {
"filter": [
{ "term": { "variants.color": "red" } },
{ "term": { "variants.size": "L" } }
]
}
}
}
}
}red + L 没有结果,改成 red + S 会返回 v001。两个条件放在同一个 nested 查询里,匹配同一条规格。
索引维护:别名与映射迁移
给商品索引加上 es_manual_catalog 别名,应用通过别名读写,迁移时再切换它指向的索引。
POST /_aliases
{
"actions": [
{ "add": { "index": "es_manual_products", "alias": "es_manual_catalog", "is_write_index": true } }
]
}
GET /_alias/es_manual_catalog先用前面的 mapping 创建 es_manual_products_v2,改好字段类型,再执行 _reindex。目标索引的 mapping 和 settings 需要单独配置。
POST /_reindex
{
"source": { "index": "es_manual_products" },
"dest": { "index": "es_manual_products_v2" }
}迁移时暂停写入,或同步这段时间的增量数据。完成后检查 failures、文档数和查询结果,再切换别名。
POST /_aliases
{
"actions": [
{ "remove": { "index": "es_manual_products", "alias": "es_manual_catalog" } },
{ "add": { "index": "es_manual_products_v2", "alias": "es_manual_catalog", "is_write_index": true } }
]
}将移除旧索引和添加新索引放在同一个请求中完成切换。保留旧索引便于回退。
常见问题与诊断顺序
| 现象 | 优先检查 | 处理方向 |
|---|---|---|
| 401 / 403 | 认证信息 / 角色权限 | 区分未认证与无权执行该操作 |
| 400 解析或映射错误 | error.type、reason、字段类型、JSON 格式 | 修正请求或数据,不盲目重试 |
| 404 | 索引名、文档 ID、别名指向 | 确认资源是否存在 |
| 409 | _create 重复 ID、并发条件过期 | 确认预期或重新读取并合并 |
| 429 | 写入并发、拒绝数、节点资源 | 退避重试并降低并发或批次大小 |
| 写入后搜不到 | Bulk 单项失败、refresh、查询字段类型 | 先按 ID 读取,再检查映射和分词 |
| 排序或聚合报 text 错误 | 是否误用了 text 字段 | 改用 keyword 子字段 |
| should 没有缩小结果集 | 是否同时存在 must/filter | 显式设置 minimum_should_match |
| 单节点 yellow | 副本无法分配到同一个节点 | 测试环境可调整副本;生产按冗余需求扩容和排查 |
| 分页重复或遗漏 | 排序键、索引刷新、PIT 生命周期 | 稳定排序并正确传递 PIT 与完整 sort 数组 |
GET /_cluster/health
GET /_cat/indices/es_manual_*?v
GET /es_manual_products/_mapping
POST /es_manual_products/_validate/query?explain=true
{
"query": { "match": { "name": "wireless" } }
}
POST /es_manual_products/_explain/p001
{
"query": { "match": { "name": "wireless" } }
}_validate/query 检查查询语法,_explain 查看文档的匹配和评分过程。分析查询耗时可加入 profile: true。
查询结果速查
| 验证动作 | 初始数据上的预期 |
|---|---|
| GET /es_manual_products/_count | 4 条文档 |
| match name: wireless mouse,operator: and | 仅 p001 |
| term name.keyword: Wireless Mouse | 仅 p001 |
| active 且价格升序,size: 2 | p001、p004 |
| active 按 category 聚合 | accessories: 2,adapters: 1 |
| PIT 每页 2 条完整遍历 | 四个不同 ID,无遗漏重复 |
| nested 查询 red + L | 0 条;red + S 为 1 条 |