Elasticsearch 常用操作语法速查手册


一、Elasticsearch 与 MySQL 概念对比

理解 Elasticsearch 和 MySQL 之间的对应关系,是快速上手 ES 的关键。

1.1 核心概念对照表

MySQL 概念 Elasticsearch 概念 说明
Database(数据库) Index(索引) 一个索引类似于一个数据库,存放一类相关的数据
Table(表) Type(类型,ES7+ 已废弃) ES7 之后一个索引只有一个默认类型 _doc,可以理解为"表"的概念被弱化了
Row(行) Document(文档) ES 中的一条数据就是一篇文档,以 JSON 格式存储
Column(列) Field(字段) 文档中的每个字段对应 MySQL 表中的一列
Schema(表结构) Mapping(映射) Mapping 定义了索引中每个字段的数据类型和分析方式
SQL DSL (Domain Specific Language) ES 使用 JSON 格式的 DSL 进行查询
SELECT GET / _search 查询操作
INSERT PUT / POST 新增操作
UPDATE POST / _update 更新操作
DELETE DELETE 删除操作

1.2 本质区别速览

对比维度 MySQL Elasticsearch
数据模型 关系型,表与表之间通过外键关联 文档型,数据以 JSON 文档形式存储
事务支持 完整的 ACID 事务支持 不支持事务(单文档级别的原子操作除外)
查询语言 SQL DSL(基于 JSON 的查询语言)
擅长场景 复杂的多表关联查询、事务性操作 全文搜索、日志分析、近实时检索
索引方式 B+ Tree 索引 倒排索引(Inverted Index)
扩展方式 通常纵向扩展(加硬件) 天然支持横向扩展(加节点)
数据实时性 实时 近实时(默认 1 秒刷新间隔)

1.3 查询语句对照示例

以一个"用户表"为例,对比 MySQL 和 ES 的写法:

场景:查询 age 大于 25 且 name 包含"张"的用户

MySQL 写法:

SELECT * FROM users WHERE age > 25 AND name LIKE '%张%';

Elasticsearch 写法:

GET /users/_search
{
  "query": {
    "bool": {
      "must": [
        {
          "range": {
            "age": {
              "gt": 25
            }
          }
        },
        {
          "match": {
            "name": "张"
          }
        }
      ]
    }
  }
}

1.4 增删改查对照

新增数据

MySQL:

INSERT INTO users (name, age, email) VALUES ('张三', 28, 'zhangsan@example.com');

Elasticsearch:

POST /users/_doc
{
  "name": "张三",
  "age": 28,
  "email": "zhangsan@example.com"
}
查询单条数据

MySQL:

SELECT * FROM users WHERE id = 1;

Elasticsearch:

GET /users/_doc/1
更新数据

MySQL:

UPDATE users SET age = 30 WHERE id = 1;

Elasticsearch:

POST /users/_update/1
{
  "doc": {
    "age": 30
  }
}
删除数据

MySQL:

DELETE FROM users WHERE id = 1;

Elasticsearch:

DELETE /users/_doc/1

1.5 聚合查询对照

场景:按 age 分组,统计每组人数

MySQL:

SELECT age, COUNT(*) AS count FROM users GROUP BY age;

Elasticsearch:

GET /users/_search
{
  "size": 0,
  "aggs": {
    "age_group": {
      "terms": {
        "field": "age",
        "size": 10
      }
    }
  }
}

二、Elasticsearch 常用操作语法详解

2.1 索引管理

2.1.1 创建索引

创建一个名为 products 的索引,设置 3 个分片和 1 个副本:

PUT /products
{
  "settings": {
    "number_of_shards": 3,
    "number_of_replicas": 1
  },
  "mappings": {
    "properties": {
      "name": {
        "type": "text",
        "analyzer": "ik_max_word"
      },
      "price": {
        "type": "float"
      },
      "category": {
        "type": "keyword"
      },
      "description": {
        "type": "text"
      },
      "created_at": {
        "type": "date",
        "format": "yyyy-MM-dd HH:mm:ss||yyyy-MM-dd||epoch_millis"
      }
    }
  }
}

说明:

  • number_of_shards:主分片数量,创建后不可修改
  • number_of_replicas:副本数量,可以随时调整
  • text 类型:会被分词,适合全文搜索
  • keyword 类型:不分词,适合精确匹配、聚合、排序
2.1.2 查看索引信息
GET /products
2.1.3 查看索引的 mapping 结构
GET /products/_mapping
2.1.4 修改索引设置

只能修改部分设置(比如副本数),分片数不可修改:

PUT /products/_settings
{
  "number_of_replicas": 2
}
2.1.5 删除索引
DELETE /products
2.1.6 判断索引是否存在
HEAD /products

返回 200 表示存在,404 表示不存在。

2.1.7 打开/关闭索引

关闭索引后无法读写,但可以恢复:

POST /products/_close
POST /products/_open
2.1.8 重建索引(Reindex)

当 mapping 不满足需求时,需要创建新索引并迁移数据:

POST /_reindex
{
  "source": {
    "index": "products"
  },
  "dest": {
    "index": "products_v2"
  }
}

2.2 文档操作(CRUD)

2.2.1 新增文档

指定 ID 新增:

PUT /products/_doc/1
{
  "name": "iPhone 15",
  "price": 7999.00,
  "category": "手机",
  "description": "苹果最新款智能手机,配备A17芯片",
  "created_at": "2024-01-15 10:30:00"
}

自动生成 ID 新增:

POST /products/_doc
{
  "name": "华为 Mate 60",
  "price": 6999.00,
  "category": "手机",
  "description": "华为旗舰手机,支持卫星通信",
  "created_at": "2024-02-20 14:00:00"
}
2.2.2 批量新增

使用 _bulk API 一次性添加多条数据:

POST /_bulk
{"index": {"_index": "products", "_id": "3"}}
{"name": "MacBook Pro", "price": 14999.00, "category": "电脑", "description": "苹果笔记本电脑,搭载M3芯片"}
{"index": {"_index": "products", "_id": "4"}}
{"name": "联想 ThinkPad", "price": 8999.00, "category": "电脑", "description": "经典商务笔记本电脑"}
{"index": {"_index": "products", "_id": "5"}}
{"name": "AirPods Pro", "price": 1899.00, "category": "耳机", "description": "苹果无线降噪耳机"}
2.2.3 查询单个文档
GET /products/_doc/1

返回结果示例:

{
  "_index": "products",
  "_id": "1",
  "_version": 1,
  "found": true,
  "_source": {
    "name": "iPhone 15",
    "price": 7999.00,
    "category": "手机",
    "description": "苹果最新款智能手机",
    "created_at": "2024-01-15 10:30:00"
  }
}
2.2.4 查询部分字段(类似 SELECT name, price)

使用 _source 过滤返回的字段:

GET /products/_doc/1
{
  "_source": ["name", "price"]
}
2.2.5 更新文档

全量替换: 使用 PUT 方法,需要传入完整的文档数据:

PUT /products/_doc/1
{
  "name": "iPhone 15 Pro",
  "price": 8999.00,
  "category": "手机",
  "description": "苹果专业版智能手机",
  "created_at": "2024-01-15 10:30:00"
}

局部更新: 只修改需要变更的字段:

POST /products/_update/1
{
  "doc": {
    "price": 7499.00
  }
}
2.2.6 条件更新(脚本更新)

使用 painless 脚本进行运算:

POST /products/_update/1
{
  "script": {
    "source": "ctx._source.price -= params.discount",
    "params": {
      "discount": 500
    }
  }
}
2.2.7 删除文档
DELETE /products/_doc/1
2.2.8 判断文档是否存在
HEAD /products/_doc/1

返回 200 表示存在,404 表示不存在。


2.3 查询操作

2.3.1 查询所有文档(match_all)

相当于 MySQL 的 SELECT * FROM products

GET /products/_search
{
  "query": {
    "match_all": {}
  }
}
2.3.2 全文搜索(match)

搜索 description 中包含"苹果"的文档:

GET /products/_search
{
  "query": {
    "match": {
      "description": "苹果手机"
    }
  }
}

match 查询会先对搜索词进行分词,再匹配。"苹果手机"会被分成"苹果"和"手机"两个词,任一命中即可。

2.3.3 短语搜索(match_phrase)

要求搜索词作为一个整体出现,顺序也要一致:

GET /products/_search
{
  "query": {
    "match_phrase": {
      "description": "苹果手机"
    }
  }
}

只有 description 中连续出现"苹果手机"这个短语的文档才会被匹配。

2.3.4 精确搜索(term)

用于 keyword、数值、日期等不分词的字段:

GET /products/_search
{
  "query": {
    "term": {
      "category": "手机"
    }
  }
}

注意: term 查询不会对搜索词进行分词,所以 text 类型的字段使用 term 搜索可能搜不到结果。如果要在 text 字段上用 term,需要搜索分词后的词项。

2.3.5 范围查询(range)

查询价格在 5000 到 10000 之间的商品:

GET /products/_search
{
  "query": {
    "range": {
      "price": {
        "gte": 5000,
        "lte": 10000
      }
    }
  }
}

范围操作符说明:

  • gt:大于(greater than)
  • gte:大于等于(greater than or equal)
  • lt:小于(less than)
  • lte:小于等于(less than or equal)
2.3.6 多字段查询(multi_match)

在多个字段中搜索同一个关键词:

GET /products/_search
{
  "query": {
    "multi_match": {
      "query": "苹果",
      "fields": ["name", "description"]
    }
  }
}

相当于 MySQL 的 WHERE name LIKE '%苹果%' OR description LIKE '%苹果%'

2.3.7 布尔组合查询(bool)

bool 查询是 ES 中最灵活的查询方式,可以组合多个查询条件。

子句类型:

子句 说明 等价于 MySQL
must 必须满足,影响评分 AND
should 满足一个或多个,影响评分 OR
must_not 必须不满足,不影响评分 NOT / AND NOT
filter 必须满足,不影响评分,有缓存 AND(性能更好)

示例: 查询价格大于 5000 且类别为"手机"的商品,但排除价格低于 6000 的:

GET /products/_search
{
  "query": {
    "bool": {
      "must": [
        {
          "match": {
            "category": "手机"
          }
        }
      ],
      "filter": [
        {
          "range": {
            "price": {
              "gte": 5000
            }
          }
        }
      ],
      "must_not": [
        {
          "range": {
            "price": {
              "lt": 6000
            }
          }
        }
      ]
    }
  }
}

建议: 不需要评分的精确过滤条件放在 filter 中,性能更好,因为 filter 的结果可以被缓存。

2.3.8 分页查询(from + size)
GET /products/_search
{
  "from": 0,
  "size": 10,
  "query": {
    "match_all": {}
  }
}
  • from:从第几条开始(偏移量),相当于 MySQL 的 OFFSET
  • size:返回多少条,相当于 MySQL 的 LIMIT

注意: from + size 不能超过 10000(默认限制),深度分页建议使用 search_after

2.3.9 排序(sort)

按价格从高到低排序:

GET /products/_search
{
  "query": {
    "match_all": {}
  },
  "sort": [
    {
      "price": {
        "order": "desc"
      }
    }
  ]
}

按多个字段排序:

GET /products/_search
{
  "query": {
    "match_all": {}
  },
  "sort": [
    {"price": {"order": "desc"}},
    {"created_at": {"order": "asc"}}
  ]
}
2.3.10 高亮显示(highlight)

搜索并高亮显示匹配的关键词:

GET /products/_search
{
  "query": {
    "match": {
      "description": "苹果"
    }
  },
  "highlight": {
    "fields": {
      "description": {}
    },
    "pre_tags": "<em>",
    "post_tags": "</em>"
  }
}

返回结果中匹配的词会被 <em> 标签包裹。

2.3.11 返回指定字段(_source 过滤)

只返回 name 和 price 字段:

GET /products/_search
{
  "_source": ["name", "price"],
  "query": {
    "match_all": {}
  }
}

不返回任何原文,只要搜索结果的 id 和评分:

GET /products/_search
{
  "_source": false,
  "query": {
    "match_all": {}
  }
}
2.3.12 前缀查询(prefix)

搜索 name 以"华为"开头的文档:

GET /products/_search
{
  "query": {
    "prefix": {
      "name": "华为"
    }
  }
}
2.3.13 通配符查询(wildcard)

* 匹配任意字符,? 匹配单个字符:

GET /products/_search
{
  "query": {
    "wildcard": {
      "name": "*Pro*"
    }
  }
}

注意: 通配符查询性能较差,不建议在大数据量场景使用。

2.3.14 正则查询(regexp)
GET /products/_search
{
  "query": {
    "regexp": {
      "name": "iP(hone|ad).*"
    }
  }
}
2.3.15 模糊查询(fuzzy)

用于搜索时的拼写纠错,允许一定的编辑距离:

GET /products/_search
{
  "query": {
    "fuzzy": {
      "name": {
        "value": "iPhon",
        "fuzziness": 2
      }
    }
  }
}

fuzziness 表示允许的最大编辑距离(插入、删除、替换字符的次数)。

2.3.16 exists 查询

查找某个字段存在的文档(类似 MySQL 的 IS NOT NULL):

GET /products/_search
{
  "query": {
    "exists": {
      "field": "description"
    }
  }
}

2.4 聚合分析

聚合(Aggregation)是 ES 的一大特色功能,类似于 MySQL 的 GROUP BY 和聚合函数。

2.4.1 指标聚合

统计总数量:

GET /products/_search
{
  "size": 0,
  "aggs": {
    "total_count": {
      "value_count": {
        "field": "price"
      }
    }
  }
}

计算平均值:

GET /products/_search
{
  "size": 0,
  "aggs": {
    "avg_price": {
      "avg": {
        "field": "price"
      }
    }
  }
}

计算最大值和最小值:

GET /products/_search
{
  "size": 0,
  "aggs": {
    "max_price": {
      "max": {
        "field": "price"
      }
    },
    "min_price": {
      "min": {
        "field": "price"
      }
    }
  }
}

求和:

GET /products/_search
{
  "size": 0,
  "aggs": {
    "total_price": {
      "sum": {
        "field": "price"
      }
    }
  }
}

一次性获取多个统计指标(stats):

GET /products/_search
{
  "size": 0,
  "aggs": {
    "price_stats": {
      "stats": {
        "field": "price"
      }
    }
  }
}

返回结果包含 count、min、max、avg、sum 五个指标。

2.4.2 分桶聚合(terms)

按类别分组,统计每个类别的商品数量(类似 GROUP BY):

GET /products/_search
{
  "size": 0,
  "aggs": {
    "by_category": {
      "terms": {
        "field": "category",
        "size": 10
      }
    }
  }
}
2.4.3 范围分桶(range)

按价格区间分组:

GET /products/_search
{
  "size": 0,
  "aggs": {
    "price_ranges": {
      "range": {
        "field": "price",
        "ranges": [
          {"to": 2000},
          {"from": 2000, "to": 5000},
          {"from": 5000, "to": 10000},
          {"from": 10000}
        ]
      }
    }
  }
}
2.4.4 嵌套聚合

按类别分组后,再统计每个类别的平均价格:

GET /products/_search
{
  "size": 0,
  "aggs": {
    "by_category": {
      "terms": {
        "field": "category",
        "size": 10
      },
      "aggs": {
        "avg_price": {
          "avg": {
            "field": "price"
          }
        }
      }
    }
  }
}
2.4.5 过滤后聚合

先过滤出"手机"类商品,再计算平均价格:

GET /products/_search
{
  "size": 0,
  "query": {
    "term": {
      "category": "手机"
    }
  },
  "aggs": {
    "avg_phone_price": {
      "avg": {
        "field": "price"
      }
    }
  }
}

2.5 批量操作

2.5.1 批量读取(_mget)

一次请求获取多个文档:

GET /_mget
{
  "docs": [
    {"_index": "products", "_id": "1"},
    {"_index": "products", "_id": "2"},
    {"_index": "products", "_id": "3"}
  ]
}

也可以在同一个索引中简化写法:

GET /products/_mget
{
  "ids": ["1", "2", "3"]
}
2.5.2 批量操作(_bulk)

支持在一个请求中执行多种操作(增、删、改):

POST /_bulk
{"index": {"_index": "products", "_id": "6"}}
{"name": "小米14", "price": 3999.00, "category": "手机", "description": "小米旗舰手机"}
{"update": {"_index": "products", "_id": "2"}}
{"doc": {"price": 6499.00}}
{"delete": {"_index": "products", "_id": "5"}}

2.6 Mapping 映射管理

2.6.1 查看 mapping
GET /products/_mapping
2.6.2 新增字段

向已有索引添加新字段(不能修改已有字段的类型):

PUT /products/_mapping
{
  "properties": {
    "brand": {
      "type": "keyword"
    },
    "stock": {
      "type": "integer"
    }
  }
}
2.6.3 常见字段类型
类型 说明 适用场景
text 全文检索,会被分词 文章标题、商品描述
keyword 精确匹配,不分词 状态码、标签、枚举值
integer / long 整数 数量、年龄
float / double 浮点数 价格、评分
boolean 布尔值 是否有效、是否删除
date 日期 创建时间、更新时间
object 对象(嵌套 JSON) 地址信息等
nested 嵌套数组对象 评论列表等需要独立查询的数组
ip IP 地址 用户 IP
geo_point 地理坐标 经纬度

2.7 查看集群和节点信息

2.7.1 查看集群健康状态
GET /_cluster/health

返回 green(健康)、yellow(副本不足)、red(主分片异常)。

2.7.2 查看节点信息
GET /_cat/nodes?v
2.7.3 查看所有索引
GET /_cat/indices?v
2.7.4 查看分片信息
GET /_cat/shards?v
2.7.5 查看磁盘使用情况
GET /_cat/allocation?v

2.8 别名管理(Alias)

别名类似于给索引取一个"外号",可以零停机切换索引。

2.8.1 创建别名
POST /_aliases
{
  "actions": [
    {
      "add": {
        "index": "products_v2",
        "alias": "products"
      }
    }
  ]
}
2.8.2 切换别名(原子操作)

将别名从旧索引指向新索引:

POST /_aliases
{
  "actions": [
    {
      "remove": {
        "index": "products_v1",
        "alias": "products"
      }
    },
    {
      "add": {
        "index": "products_v2",
        "alias": "products"
      }
    }
  ]
}
2.8.3 带过滤条件的别名

只索引某个类别的数据:

POST /_aliases
{
  "actions": [
    {
      "add": {
        "index": "products",
        "alias": "phone_products",
        "filter": {
          "term": {
            "category": "手机"
          }
        }
      }
    }
  ]
}

2.9 索引模板

当有大量格式相同的索引时(如按日期创建的日志索引),可以使用模板自动应用配置。

2.9.1 创建索引模板

以 logstash 日志索引为例:

PUT /_index_template/logs_template
{
  "index_patterns": ["logs-*"],
  "template": {
    "settings": {
      "number_of_shards": 3,
      "number_of_replicas": 1
    },
    "mappings": {
      "properties": {
        "timestamp": {
          "type": "date"
        },
        "level": {
          "type": "keyword"
        },
        "message": {
          "type": "text"
        },
        "service": {
          "type": "keyword"
        }
      }
    }
  }
}

此后任何以 logs- 开头的索引都会自动应用这个模板。

2.9.2 查看索引模板
GET /_index_template/logs_template
2.9.3 删除索引模板
DELETE /_index_template/logs_template

2.10 滚动查询(深分页方案)

当需要遍历大量数据时,使用 search_after 避免深度分页的性能问题。

GET /products/_search
{
  "size": 5,
  "query": {
    "match_all": {}
  },
  "sort": [
    {"price": "desc"},
    {"_id": "asc"}
  ]
}

取最后一条的 sort 值作为下一次查询的起点:

GET /products/_search
{
  "size": 5,
  "query": {
    "match_all": {}
  },
  "search_after": [8999.0, "2"],
  "sort": [
    {"price": "desc"},
    {"_id": "asc"}
  ]
}
Scroll API(全量导出场景)
GET /products/_search?scroll=1m
{
  "size": 100,
  "query": {
    "match_all": {}
  }
}

后续翻页:

POST /_search/scroll
{
  "scroll": "1m",
  "scroll_id": "上一次返回的 scroll_id"
}

用完后清除 scroll 上下文:

DELETE /_search/scroll
{
  "scroll_id": "scroll_id值"
}

三、ES 与 MySQL 语法速查对照表

操作 MySQL Elasticsearch
查询全部 SELECT * FROM table GET /index/_search {"query":{"match_all":{}}}
等值查询 WHERE field = 'value' {"term":{"field":"value"}}
模糊查询 WHERE field LIKE '%value%' {"match":{"field":"value"}}
范围查询 WHERE field BETWEEN a AND b {"range":{"field":{"gte":a,"lte":b}}}
多条件 AND WHERE a=1 AND b=2 {"bool":{"must":[{"term":{"a":1}},{"term":{"b":2}}]}}
多条件 OR WHERE a=1 OR b=2 {"bool":{"should":[{"term":{"a":1}},{"term":{"b":2}}]}}
非条件 WHERE NOT a=1 {"bool":{"must_not":[{"term":{"a":1}}]}}
IS NULL WHERE field IS NULL {"bool":{"must_not":[{"exists":{"field":"field"}}]}}
IS NOT NULL WHERE field IS NOT NULL {"exists":{"field":"field"}}
分页 LIMIT 10 OFFSET 20 "from":20,"size":10
排序 ORDER BY field DESC "sort":[{"field":{"order":"desc"}}]
聚合统计 COUNT / AVG / SUM / MAX / MIN 对应 value_count / avg / sum / max / min
分组统计 GROUP BY field {"terms":{"field":"field"}}
取部分字段 SELECT name, price "_source":["name","price"]
去重 SELECT DISTINCT field 使用 terms 聚合
更新 UPDATE SET field=value POST /_update/id {"doc":{"field":"value"}}
删除 DELETE FROM table WHERE id=1 DELETE /index/_doc/1

四、实际业务场景示例

4.1 电商商品搜索

搜索商品名称或描述中包含"蓝牙耳机",价格在 100-500 之间,按销量降序排列,返回前 20 条:

GET /products/_search
{
  "from": 0,
  "size": 20,
  "query": {
    "bool": {
      "must": [
        {
          "multi_match": {
            "query": "蓝牙耳机",
            "fields": ["name^3", "description"]
          }
        }
      ],
      "filter": [
        {
          "range": {
            "price": {
              "gte": 100,
              "lte": 500
            }
          }
        }
      ]
    }
  },
  "sort": [
    {"sales": {"order": "desc"}}
  ],
  "highlight": {
    "fields": {
      "name": {},
      "description": {}
    }
  }
}

说明:name^3 表示 name 字段的权重是 description 的 3 倍,搜索结果中 name 匹配的评分更高。

4.2 日志分析

查询最近 1 小时内 error 级别的日志,按服务名分组统计数量:

GET /logs-*/_search
{
  "size": 0,
  "query": {
    "bool": {
      "must": [
        {
          "term": {
            "level": "error"
          }
        },
        {
          "range": {
            "timestamp": {
              "gte": "now-1h"
            }
          }
        }
      ]
    }
  },
  "aggs": {
    "by_service": {
      "terms": {
        "field": "service",
        "size": 20
      }
    }
  }
}

4.3 用户画像查询

查询年龄在 25-35 之间,兴趣标签包含"摄影"或"旅行"的用户:

GET /users/_search
{
  "query": {
    "bool": {
      "filter": [
        {
          "range": {
            "age": {
              "gte": 25,
              "lte": 35
            }
          }
        }
      ],
      "should": [
        {
          "term": {
            "tags": "摄影"
          }
        },
        {
          "term": {
            "tags": "旅行"
          }
        }
      ],
      "minimum_should_match": 1
    }
  }
}

minimum_should_match: 1 表示 should 子句中至少满足一个条件。


五、常见注意事项

  1. text 和 keyword 的区别:text 类型会被分词,适合全文搜索;keyword 类型不分词,适合精确匹配和聚合。同一个字段可以同时设置两种类型(multi-fields)。

  2. term 和 match 的区别:term 不分词直接匹配,match 会先对搜索词分词再匹配。text 字段应该用 match,keyword 字段应该用 term。

  3. filter 和 must 的区别:filter 的结果会被缓存且不参与评分,性能更好。不需要评分的条件应尽量放在 filter 中。

  4. 深分页问题:from + size 方式在深度分页时性能急剧下降,超过 10000 条建议使用 search_after 或 scroll API。

  5. mapping 不可修改:已有的字段类型创建后不能直接修改,需要通过 reindex 重建索引来变更。

  6. near real-time(近实时):文档写入后默认 1 秒才能被搜索到(由 refresh_interval 控制),如果需要写入后立即可查,可以在写入请求中加 ?refresh=true(会影响性能)。

  7. 字段类型选择:不需要搜索的字段设置 "index": false,不需要返回的字段设置 "enabled": false,可以节省存储空间和提升性能。


六、在 Java 中使用 Elasticsearch

本章介绍如何在 Java 项目中操作 Elasticsearch。主要使用官方推荐的 Elasticsearch Java API Client(ES 7.15+ 推荐,ES 8.x 默认客户端)。

6.1 客户端选择

客户端 状态 说明
Elasticsearch Java API Client 推荐 ES 官方最新客户端,功能完整,类型安全
RestHighLevel Client (HLRC) 已废弃 ES 7.x 时代的主力客户端,ES 8.x 已标记为废弃
Spring Data Elasticsearch 可选 Spring 生态封装,适合 Spring Boot 项目
Jest 不推荐 第三方客户端,已停止维护

6.2 Maven 依赖配置

使用 Elasticsearch Java API Client 需要添加以下依赖:

<dependencies>
    <!-- Elasticsearch Java API Client -->
    <dependency>
        <groupId>co.elastic.clients</groupId>
        <artifactId>elasticsearch-java</artifactId>
        <version>8.11.0</version>
    </dependency>

    <!-- JSON 处理(Jackson) -->
    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-databind</artifactId>
        <version>2.15.2</version>
    </dependency>

    <!-- Jakarta JSON Processing(必需) -->
    <dependency>
        <groupId>jakarta.json</groupId>
        <artifactId>jakarta.json-api</artifactId>
        <version>2.1.2</version>
    </dependency>
</dependencies>

6.3 创建客户端连接

6.3.1 基础连接(无认证)
import co.elastic.clients.elasticsearch.ElasticsearchClient;
import co.elastic.clients.json.jackson.JacksonJsonpTransport;
import co.elastic.clients.transport.rest_client.RestClientTransport;
import org.apache.http.HttpHost;
import org.elasticsearch.client.RestClient;

public class EsClient {
    public static ElasticsearchClient createClient() {
        RestClient restClient = RestClient.builder(
            new HttpHost("localhost", 9200)
        ).build();

        RestClientTransport transport = new RestClientTransport(
            restClient, new JacksonJsonpTransport()
        );

        return new ElasticsearchClient(transport);
    }
}
6.3.2 带认证的连接
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.impl.client.BasicCredentialsProvider;

public class EsClient {
    public static ElasticsearchClient createClientWithAuth() {
        BasicCredentialsProvider credentialsProvider = new BasicCredentialsProvider();
        credentialsProvider.setCredentials(AuthScope.ANY,
            new UsernamePasswordCredentials("elastic", "your_password"));

        RestClient restClient = RestClient.builder(
            new HttpHost("localhost", 9200, "http")
        ).setHttpClientConfigCallback(httpClientBuilder ->
            httpClientBuilder.setDefaultCredentialsProvider(credentialsProvider)
        ).build();

        RestClientTransport transport = new RestClientTransport(
            restClient, new JacksonJsonpTransport()
        );

        return new ElasticsearchClient(transport);
    }
}
6.3.3 连接多个节点(集群)
RestClient restClient = RestClient.builder(
    new HttpHost("node1", 9200),
    new HttpHost("node2", 9200),
    new HttpHost("node3", 9200)
).build();

6.4 定义实体类

创建一个与索引文档对应的 Java 实体类:

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.List;

@JsonIgnoreProperties(ignoreUnknown = true)
public class Product {
    private String id;

    @JsonProperty("name")
    private String name;

    @JsonProperty("price")
    private Double price;

    @JsonProperty("category")
    private String category;

    @JsonProperty("description")
    private String description;

    @JsonProperty("tags")
    private List<String> tags;

    public Product() {}

    public Product(String name, Double price, String category, String description) {
        this.name = name;
        this.price = price;
        this.category = category;
        this.description = description;
    }

    // getter 和 setter 方法
    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 Double getPrice() { return price; }
    public void setPrice(Double price) { this.price = price; }
    public String getCategory() { return category; }
    public void setCategory(String category) { this.category = category; }
    public String getDescription() { return description; }
    public void setDescription(String description) { this.description = description; }
    public List<String> getTags() { return tags; }
    public void setTags(List<String> tags) { this.tags = tags; }
}

6.5 索引管理操作

6.5.1 创建索引
public void createIndex(ElasticsearchClient client) throws IOException {
    CreateIndexResponse response = client.indices().create(c -> c
        .index("products")
        .settings(s -> s
            .numberOfShards("3")
            .numberOfReplicas("1")
        )
        .mappings(m -> m
            .properties("name", p -> p
                .text(t -> t.analyzer("ik_max_word"))
            )
            .properties("price", p -> p
                .float_(f -> f)
            )
            .properties("category", p -> p
                .keyword(k -> k)
            )
            .properties("description", p -> p
                .text(t -> t)
            )
        )
    );

    System.out.println("索引创建结果: " + response.acknowledged());
}
6.5.2 判断索引是否存在
public boolean indexExists(ElasticsearchClient client, String indexName) throws IOException {
    return client.indices().exists(e -> e.index(indexName)).value();
}
6.5.3 删除索引
public void deleteIndex(ElasticsearchClient client, String indexName) throws IOException {
    client.indices().delete(d -> d.index(indexName));
    System.out.println("索引 " + indexName + " 已删除");
}

6.6 文档 CRUD 操作

6.6.1 新增文档(指定 ID)
public void indexDocument(ElasticsearchClient client) throws IOException {
    Product product = new Product("iPhone 15", 7999.00, "手机", "苹果最新款智能手机");

    IndexResponse response = client.index(i -> i
        .index("products")
        .id("1")
        .document(product)
    );

    System.out.println("文档ID: " + response.id());
    System.out.println("结果: " + response.result());
}
6.6.2 新增文档(自动生成 ID)
public void indexDocumentAutoId(ElasticsearchClient client) throws IOException {
    Product product = new Product("华为 Mate 60", 6999.00, "手机", "华为旗舰手机");

    IndexResponse response = client.index(i -> i
        .index("products")
        .document(product)
    );

    System.out.println("自动生成的ID: " + response.id());
}
6.6.3 根据 ID 查询文档
public Product getDocument(ElasticsearchClient client, String id) throws IOException {
    GetResponse<Product> response = client.get(g -> g
        .index("products")
        .id(id),
        Product.class
    );

    if (response.found()) {
        Product product = response.source();
        System.out.println("找到文档: " + product.getName());
        return product;
    } else {
        System.out.println("文档不存在");
        return null;
    }
}
6.6.4 局部更新文档
public void updateDocument(ElasticsearchClient client, String id) throws IOException {
    UpdateResponse<Product> response = client.update(u -> u
        .index("products")
        .id(id)
        .action(a -> a
            .doc(Map.of("price", 6999.00))
        ),
        Product.class
    );

    System.out.println("更新结果: " + response.result());
}
6.6.5 删除文档
public void deleteDocument(ElasticsearchClient client, String id) throws IOException {
    DeleteResponse response = client.delete(d -> d
        .index("products")
        .id(id)
    );

    System.out.println("删除结果: " + response.result());
}
6.6.6 判断文档是否存在
public boolean documentExists(ElasticsearchClient client, String id) throws IOException {
    return client.exists(e -> e
        .index("products")
        .id(id)
    ).value();
}

6.7 查询操作

6.7.1 查询所有文档
public List<Product> matchAll(ElasticsearchClient client) throws IOException {
    SearchResponse<Product> response = client.search(s -> s
        .index("products")
        .query(q -> q
            .matchAll(m -> m)
        ),
        Product.class
    );

    return response.hits().hits().stream()
        .map(hit -> hit.source())
        .collect(Collectors.toList());
}
6.7.2 全文搜索(match 查询)
public List<Product> searchByKeyword(ElasticsearchClient client, String keyword) throws IOException {
    SearchResponse<Product> response = client.search(s -> s
        .index("products")
        .query(q -> q
            .match(m -> m
                .field("description")
                .query(keyword)
            )
        ),
        Product.class
    );

    return response.hits().hits().stream()
        .map(hit -> hit.source())
        .collect(Collectors.toList());
}
6.7.3 精确查询(term 查询)
public List<Product> searchByCategory(ElasticsearchClient client, String category) throws IOException {
    SearchResponse<Product> response = client.search(s -> s
        .index("products")
        .query(q -> q
            .term(t -> t
                .field("category")
                .value(category)
            )
        ),
        Product.class
    );

    return response.hits().hits().stream()
        .map(hit -> hit.source())
        .collect(Collectors.toList());
}
6.7.4 范围查询
public List<Product> searchByPriceRange(ElasticsearchClient client,
                                          Double minPrice, Double maxPrice) throws IOException {
    SearchResponse<Product> response = client.search(s -> s
        .index("products")
        .query(q -> q
            .range(r -> r
                .field("price")
                .gte(JsonData.of(minPrice))
                .lte(JsonData.of(maxPrice))
            )
        ),
        Product.class
    );

    return response.hits().hits().stream()
        .map(hit -> hit.source())
        .collect(Collectors.toList());
}
6.7.5 布尔组合查询(bool 查询)
public List<Product> boolQuery(ElasticsearchClient client, String keyword,
                                Double minPrice, String category) throws IOException {
    SearchResponse<Product> response = client.search(s -> s
        .index("products")
        .query(q -> q
            .bool(b -> b
                .must(m -> m
                    .multiMatch(mm -> mm
                        .fields("name", "description")
                        .query(keyword)
                    )
                )
                .filter(f -> f
                    .range(r -> r
                        .field("price")
                        .gte(JsonData.of(minPrice))
                    )
                )
                .filter(f -> f
                    .term(t -> t
                        .field("category")
                        .value(category)
                    )
                )
            )
        ),
        Product.class
    );

    return response.hits().hits().stream()
        .map(hit -> hit.source())
        .collect(Collectors.toList());
}
6.7.6 分页查询
public List<Product> searchWithPagination(ElasticsearchClient client,
                                            int from, int size) throws IOException {
    SearchResponse<Product> response = client.search(s -> s
        .index("products")
        .query(q -> q
            .matchAll(m -> m)
        )
        .from(from)
        .size(size)
        .sort(so -> so
            .field(f -> f.field("price").order(SortOrder.Desc))
        ),
        Product.class
    );

    return response.hits().hits().stream()
        .map(hit -> hit.source())
        .collect(Collectors.toList());
}
6.7.7 高亮搜索
public List<Map<String, Object>> searchWithHighlight(ElasticsearchClient client,
                                                       String keyword) throws IOException {
    SearchResponse<Product> response = client.search(s -> s
        .index("products")
        .query(q -> q
            .match(m -> m
                .field("description")
                .query(keyword)
            )
        )
        .highlight(h -> h
            .fields("description", hf -> hf
                .preTags("<em>")
                .postTags("</em>")
            )
        ),
        Product.class
    );

    List<Map<String, Object>> results = new ArrayList<>();
    for (Hit<Product> hit : response.hits().hits()) {
        Map<String, Object> result = new HashMap<>();
        result.put("product", hit.source());
        result.put("highlights", hit.highlight());
        results.add(result);
    }
    return results;
}

6.8 聚合操作

6.8.1 分组统计
public Map<String, Long> aggregateByCategory(ElasticsearchClient client) throws IOException {
    SearchResponse<Void> response = client.search(s -> s
        .index("products")
        .size(0)
        .aggregations("by_category", a -> a
            .terms(t -> t
                .field("category")
                .size(10)
            )
        ),
        Void.class
    );

    Aggregate aggregate = response.aggregations().get("by_category");
    List<StringTermsBucket> buckets = aggregate.sters().buckets().array();

    Map<String, Long> result = new LinkedHashMap<>();
    for (StringTermsBucket bucket : buckets) {
        result.put(bucket.key(), bucket.docCount());
    }
    return result;
}
6.8.2 计算平均值
public Double averagePrice(ElasticsearchClient client) throws IOException {
    SearchResponse<Void> response = client.search(s -> s
        .index("products")
        .size(0)
        .aggregations("avg_price", a -> a
            .avg(avg -> avg
                .field("price")
            )
        ),
        Void.class
    );

    return response.aggregations().get("avg_price").avg().value();
}
6.8.3 多指标聚合
public Map<String, Object> priceStats(ElasticsearchClient client) throws IOException {
    SearchResponse<Void> response = client.search(s -> s
        .index("products")
        .size(0)
        .aggregations("price_stats", a -> a
            .stats(st -> st
                .field("price")
            )
        ),
        Void.class
    );

    StatsAggregate stats = response.aggregations().get("price_stats").stats();

    Map<String, Object> result = new LinkedHashMap<>();
    result.put("count", stats.count());
    result.put("min", stats.min());
    result.put("max", stats.max());
    result.put("avg", stats.avg());
    result.put("sum", stats.sum());
    return result;
}

6.9 批量操作

public void bulkIndex(ElasticsearchClient client) throws IOException {
    List<Product> products = Arrays.asList(
        new Product("MacBook Pro", 14999.00, "电脑", "苹果笔记本电脑"),
        new Product("ThinkPad", 8999.00, "电脑", "联想商务笔记本"),
        new Product("AirPods Pro", 1899.00, "耳机", "苹果无线降噪耳机")
    );

    BulkRequest.Builder bulkBuilder = new BulkRequest.Builder();

    for (int i = 0; i < products.size(); i++) {
        bulkBuilder.operations(op -> op
            .index(idx -> idx
                .index("products")
                .id(String.valueOf(i + 10))
                .document(products.get(i))
            )
        );
    }

    BulkResponse response = bulkBuilder.build();

    if (response.errors()) {
        for (BulkResponseItem item : response.items()) {
            if (item.error() != null) {
                System.out.println("错误: " + item.error().reason());
            }
        }
    } else {
        System.out.println("批量操作成功,共 " + products.size() + " 条");
    }
}

6.10 使用 Spring Data Elasticsearch

如果你使用 Spring Boot,可以使用 Spring Data Elasticsearch 来简化操作。

6.10.1 Maven 依赖
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>
6.10.2 application.yml 配置
spring:
  elasticsearch:
    uris: http://localhost:9200
    username: elastic
    password: your_password
6.10.3 定义实体
import org.springframework.data.annotation.Id;
import org.springframework.data.elasticsearch.annotations.Document;
import org.springframework.data.elasticsearch.annotations.Field;
import org.springframework.data.elasticsearch.annotations.FieldType;

@Document(indexName = "products")
public class Product {

    @Id
    private String id;

    @Field(type = FieldType.Text, analyzer = "ik_max_word")
    private String name;

    @Field(type = FieldType.Double)
    private Double price;

    @Field(type = FieldType.Keyword)
    private String category;

    @Field(type = FieldType.Text)
    private String description;

    // getter 和 setter
}
6.10.4 定义 Repository
import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;
import java.util.List;

public interface ProductRepository extends ElasticsearchRepository<Product, String> {

    List<Product> findByCategory(String category);

    List<Product> findByPriceBetween(Double minPrice, Double maxPrice);

    List<Product> findByNameContaining(String keyword);
}
6.10.5 使用 Repository
@Service
public class ProductService {

    @Autowired
    private ProductRepository productRepository;

    public Product save(Product product) {
        return productRepository.save(product);
    }

    public Optional<Product> findById(String id) {
        return productRepository.findById(id);
    }

    public List<Product> findByCategory(String category) {
        return productRepository.findByCategory(category);
    }

    public List<Product> searchByName(String keyword) {
        return productRepository.findByNameContaining(keyword);
    }

    public void deleteById(String id) {
        productRepository.deleteById(id);
    }
}

6.11 完整示例代码

以下是一个包含所有基本操作的完整示例类:

import co.elastic.clients.elasticsearch.ElasticsearchClient;
import co.elastic.clients.elasticsearch.core.*;
import co.elastic.clients.elasticsearch.core.search.Hit;
import co.elastic.clients.elasticsearch.indices.CreateIndexResponse;
import co.elastic.clients.json.JsonData;
import co.elastic.clients.json.jackson.JacksonJsonpTransport;
import co.elastic.clients.transport.rest_client.RestClientTransport;
import org.apache.http.HttpHost;
import org.elasticsearch.client.RestClient;

import java.io.IOException;
import java.util.*;
import java.util.stream.Collectors;

public class ElasticsearchDemo {

    private final ElasticsearchClient client;

    public ElasticsearchDemo() {
        RestClient restClient = RestClient.builder(
            new HttpHost("localhost", 9200)
        ).build();

        RestClientTransport transport = new RestClientTransport(
            restClient, new JacksonJsonpTransport()
        );

        this.client = new ElasticsearchClient(transport);
    }

    public void createIndex() throws IOException {
        CreateIndexResponse response = client.indices().create(c -> c
            .index("products")
            .settings(s -> s
                .numberOfShards("1")
                .numberOfReplicas("1")
            )
            .mappings(m -> m
                .properties("name", p -> p.text(t -> t.analyzer("ik_max_word")))
                .properties("price", p -> p.float_(f -> f))
                .properties("category", p -> p.keyword(k -> k))
                .properties("description", p -> p.text(t -> t))
            )
        );
        System.out.println("创建索引: " + response.acknowledged());
    }

    public void addDocument(String id, Product product) throws IOException {
        IndexResponse response = client.index(i -> i
            .index("products")
            .id(id)
            .document(product)
        );
        System.out.println("文档 " + id + " 添加成功");
    }

    public Product getDocument(String id) throws IOException {
        GetResponse<Product> response = client.get(g -> g
            .index("products").id(id), Product.class
        );
        return response.found() ? response.source() : null;
    }

    public void updateDocument(String id, Map<String, Object> fields) throws IOException {
        client.update(u -> u
            .index("products")
            .id(id)
            .action(a -> a.doc(fields)),
            Product.class
        );
        System.out.println("文档 " + id + " 更新成功");
    }

    public void deleteDocument(String id) throws IOException {
        client.delete(d -> d.index("products").id(id));
        System.out.println("文档 " + id + " 已删除");
    }

    public List<Product> search(String keyword) throws IOException {
        SearchResponse<Product> response = client.search(s -> s
            .index("products")
            .query(q -> q
                .multiMatch(m -> m
                    .fields("name^2", "description")
                    .query(keyword)
                )
            ),
            Product.class
        );
        return response.hits().hits().stream()
            .map(Hit::source)
            .collect(Collectors.toList());
    }

    public List<Product> searchWithFilter(String keyword, Double minPrice,
                                           String category) throws IOException {
        SearchResponse<Product> response = client.search(s -> s
            .index("products")
            .query(q -> q
                .bool(b -> b
                    .must(m -> m.multiMatch(mm -> mm
                        .fields("name^2", "description")
                        .query(keyword)
                    ))
                    .filter(f -> f.range(r -> r
                        .field("price")
                        .gte(JsonData.of(minPrice))
                    ))
                    .filter(f -> f.term(t -> t
                        .field("category")
                        .value(category)
                    ))
                )
            ),
            Product.class
        );
        return response.hits().hits().stream()
            .map(Hit::source)
            .collect(Collectors.toList());
    }

    public void close() throws IOException {
        client._transport().close();
    }

    public static void main(String[] args) throws IOException {
        ElasticsearchDemo demo = new ElasticsearchDemo();

        demo.createIndex();

        demo.addDocument("1", new Product("iPhone 15", 7999.00, "手机", "苹果手机"));
        demo.addDocument("2", new Product("华为 Mate 60", 6999.00, "手机", "华为手机"));
        demo.addDocument("3", new Product("MacBook Pro", 14999.00, "电脑", "苹果电脑"));

        Product product = demo.getDocument("1");
        System.out.println("查询到: " + product.getName());

        List<Product> results = demo.search("苹果");
        System.out.println("搜索结果: " + results.size() + " 条");

        demo.updateDocument("1", Map.of("price", 7499.00));

        demo.deleteDocument("3");

        demo.close();
    }
}

附录:常见问题解答

Q1: 应该使用哪个 Java 客户端?

推荐使用 Elasticsearch Java API Client,它是 ES 官方最新维护的客户端,类型安全且功能完整。如果是新项目,不要使用已废弃的 RestHighLevel Client。

Q2: Spring Boot 项目推荐哪种方式?

简单查询使用 Spring Data Elasticsearch 的 Repository 接口即可,复杂查询建议注入 ElasticsearchClient 原生客户端。

Q3: 如何处理日期字段?

@Field(type = FieldType.Date, format = DateFormat.date_hour_minute_second)
@JsonProperty("created_at")
private LocalDateTime createdAt;

Q4: 如何实现深分页?

使用 search_after 方式,避免 from + size 超过 10000 的限制:

SearchResponse<Product> response = client.search(s -> s
    .index("products")
    .query(q -> q.matchAll(m -> m))
    .size(10)
    .sort(so -> so.field(f -> f.field("price").order(SortOrder.Desc)))
    .sort(so -> so.field(f -> f.field("_id").order(SortOrder.Asc)))
    .searchAfter(lastSortValues),
    Product.class
);
Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐