Elasticsearch join 类型:实现文档间的父子关联
Elasticsearch 中的 join 类型是唯一支持文档间「父子关系」 的字段类型,核心作用是在同一个索引内建立「父文档」和「子文档」的关联(比如「订单 - 订单明细」「文章 - 评论」),解决 ES 无表关联查询的问题。
一、核心概念(通俗理解)
join类型本质是「字段级的关系定义」,需先声明「关系名称 + 父 / 子文档类型」;- 一个索引只能定义一个
join字段(ES 限制); - 父文档和子文档必须存储在同一个分片(通过路由保证,子文档需指定父文档 ID);
- 子文档可以关联到唯一的父文档,但一个父文档可关联多个子文档(一对多关系)。
二、核心特性
表格
| 特性 | 说明 |
|---|---|
| 支持的关系 | 仅「一对多」(一个父文档对应多个子文档),不支持多对多 / 多对一 |
| 分片要求 | 父子文档必须在同一分片(子文档路由值 = 父文档 ID) |
| 查询方式 | 需用 has_child/has_parent/parent_id 查询关联文档 |
| 性能 | 父子查询比普通查询开销大(需跨文档关联),适合小规模关联场景 |
| 适用场景 | 订单 - 明细、文章 - 评论、商品 - 评价等「一对多、数据量适中」的关联场景 |
三、完整使用示例(文章 - 评论 父子关系)
场景:存储「文章(父文档)」和「评论(子文档)」,实现关联查询。
1. 创建含 join 类型的索引
需先定义 join 字段,指定「关系名称 + 父 / 子类型」(示例中:article 是父类型,comment 是子类型):
json
PUT /article_comment_index
{
"mappings": {
"properties": {
"id": { "type": "integer" },
"title": { "type": "text" },
"content": { "type": "text" },
"comment_content": { "type": "text" },
"author": { "type": "keyword" },
// 定义join字段,声明父子关系
"relation": {
"type": "join",
"relations": {
"article": "comment" // article是父,comment是子
}
}
}
}
}
2. 写入父文档(文章)
父文档需指定 join 字段的「父类型」(article):
json
// 父文档1:文章ID=1
PUT /article_comment_index/_doc/1 // 文档ID=1(子文档需关联此ID)
{
"id": 1,
"title": "Elasticsearch入门",
"content": "ES是一款分布式搜索引擎",
"author": "张三",
"relation": "article" // 标记为父类型article
}
// 父文档2:文章ID=2
PUT /article_comment_index/_doc/2
{
"id": 2,
"title": "Java基础",
"content": "Java是面向对象语言",
"author": "李四",
"relation": "article"
}
3. 写入子文档(评论)
子文档需满足两个核心要求:
- 指定
join字段的「子类型 + 父文档 ID」; - 通过
routing参数指定路由值 = 父文档 ID(保证同分片)。
json
// 子文档1:关联父文档1(文章1的评论)
PUT /article_comment_index/_doc/3?routing=1 // routing=父文档ID=1
{
"id": 3,
"comment_content": "写得很详细,点赞!",
"author": "王五",
"relation": {
"name": "comment", // 子类型名称
"parent": "1" // 关联的父文档ID
}
}
// 子文档2:关联父文档1(文章1的第二条评论)
PUT /article_comment_index/_doc/4?routing=1
{
"id": 4,
"comment_content": "请问join类型性能如何?",
"author": "赵六",
"relation": {
"name": "comment",
"parent": "1"
}
}
// 子文档3:关联父文档2(文章2的评论)
PUT /article_comment_index/_doc/5?routing=2
{
"id": 5,
"comment_content": "基础知识点很清晰",
"author": "孙七",
"relation": {
"name": "comment",
"parent": "2"
}
}
四、join 类型的核心查询方式
场景 1:通过子文档查询父文档(has_child)
查询「包含评论作者为王五」的文章:
json
GET /article_comment_index/_search
{
"query": {
"has_child": {
"type": "comment", // 子类型名称
"query": {
"term": { "author.keyword": "王五" } // 子文档的查询条件
}
}
}
}
结果:返回父文档 1(文章 1),因为它有作者为王五的子评论。
场景 2:通过父文档查询子文档(has_parent)
查询「文章作者为张三」的所有评论:
json
GET /article_comment_index/_search
{
"query": {
"has_parent": {
"parent_type": "article", // 父类型名称
"query": {
"term": { "author.keyword": "张三" } // 父文档的查询条件
}
}
}
}
结果:返回子文档 3、4(文章 1 的所有评论)。
场景 3:通过父文档 ID 查询子文档(parent_id)
查询「父文档 ID=1」的所有评论(最精准):
json
GET /article_comment_index/_search
{
"query": {
"parent_id": {
"type": "comment", // 子类型名称
"id": "1" // 父文档ID
}
}
}
结果:返回子文档 3、4。
五、join 类型的常见操作与限制
1. 更新父子关系(仅支持修改子文档的父文档)
json
// 将子文档5的父文档从2改为1(需指定routing=新父文档ID)
POST /article_comment_index/_update/5?routing=1
{
"doc": {
"relation": {
"name": "comment",
"parent": "1"
}
}
}
2. 核心限制(必看)
- 一个索引只能定义一个
join字段; - 不支持「子文档再作为父文档」的多级嵌套(如需多级关联,建议用
nested或数据扁平化); - 父子文档必须在同一分片,删除父文档时,子文档不会自动删除(需手动清理);
- 父子查询性能较差,若子文档数量超 10 万级,建议拆分索引或用应用层关联。
六、join 类型 vs nested 类型(核心区别)
表格
| 维度 | join 类型 | nested 类型 |
|---|---|---|
| 数据存储 | 父 / 子是独立文档 | 嵌套对象是主文档的一部分 |
| 关系支持 | 一对多(跨文档) | 一对多(文档内嵌套) |
| 性能 | 查询慢(跨文档关联) | 查询快(文档内) |
| 数据更新 | 父 / 子可独立更新 | 嵌套对象需更新整个主文档 |
| 适用场景 | 父子数据更新频繁、数据量大 | 嵌套数据更新少、数据量小 |
总结
join类型是 ES 实现「同索引跨文档父子关联」的唯一方式,核心定义「父类型 + 子类型」,需保证父子文档在同一分片;- 核心查询语法:
has_child(子查父)、has_parent(父查子)、parent_id(按父 ID 查子); join类型适合「父子数据独立更新、关联规模适中」的场景,大规模关联建议优先用应用层处理,或选择nested类型(文档内嵌套)。
Elasticsearch 字段别名(Alias):字段的「快捷方式」
Elasticsearch 中的字段别名(Field Alias) 是给已有字段定义的「只读快捷方式」,核心作用是为字段提供别名,避免因字段名变更、多字段聚合等场景导致查询 / 聚合语句修改,是 ES 中提升字段使用灵活性的重要特性。
一、核心特性(必掌握)
表格
| 特性 | 说明 |
|---|---|
| 只读属性 | 别名仅支持查询 / 聚合,不支持写入(写入仍需用原字段名) |
| 关联关系 | 一个别名只能指向一个原字段,但一个原字段可被多个别名指向 |
| 动态性 | 可在索引创建后新增 / 修改别名(无需重建索引) |
| 适用字段 | 支持指向普通字段、object/nested 子字段(如 user.name),不支持指向其他别名 |
| 索引要求 | 别名仅对「已存在的索引」生效,无法在创建文档时通过别名写入数据 |
二、核心使用场景
- 字段名重构 :原字段名变更(如
user_name→username),通过别名保留旧字段名,无需修改历史查询语句; - 多场景适配 :为同一个字段定义不同别名(如
price别名amount),适配不同业务系统的字段命名习惯; - 简化嵌套字段路径 :为多层嵌套字段(如
user.info.address.city)定义别名user_city,简化查询语法。
三、完整使用示例
1. 创建索引时定义字段别名
json
PUT /product_index
{
"mappings": {
"properties": {
"product_id": { "type": "integer" },
"username": { "type": "keyword" }, // 原字段
"price": { "type": "float" }, // 原字段
// 定义字段别名
"user_name": { // 别名1:指向username
"type": "alias",
"path": "username"
},
"amount": { // 别名2:指向price
"type": "alias",
"path": "price"
}
}
}
}
2. 向索引写入数据(只能用原字段名)
json
PUT /product_index/_doc/1
{
"product_id": 1,
"username": "zhangsan", // 必须写原字段,写user_name会报错
"price": 99.9 // 必须写原字段,写amount会报错
}
3. 通过别名查询 / 聚合(核心用法)
场景 1:通过别名查询数据
json
GET /product_index/_search
{
"query": {
"term": {
"user_name": "zhangsan" // 用别名查询,等价于查询username
}
}
}
场景 2:通过别名做聚合
json
GET /product_index/_search
{
"size": 0,
"aggs": {
"avg_amount": {
"avg": {
"field": "amount" // 用别名聚合,等价于聚合price
}
}
}
}
场景 3:为嵌套字段定义别名
json
// 先新增嵌套字段
PUT /product_index/_mapping
{
"properties": {
"user": {
"type": "object",
"properties": {
"info": {
"type": "object",
"properties": {
"city": { "type": "keyword" }
}
}
}
},
// 为嵌套字段定义别名
"user_city": {
"type": "alias",
"path": "user.info.city"
}
}
}
// 写入嵌套数据
PUT /product_index/_doc/2
{
"product_id": 2,
"user": {
"info": {
"city": "Beijing"
}
}
}
// 通过别名查询嵌套字段
GET /product_index/_search
{
"query": {
"term": {
"user_city": "Beijing" // 简化嵌套字段查询
}
}
}
4. 新增 / 修改字段别名(索引已创建后)
新增别名
json
PUT /product_index/_mapping
{
"properties": {
"price_alias": { // 新增别名
"type": "alias",
"path": "price"
}
}
}
修改别名(需先删除旧别名,再新增)
ES 不支持直接修改别名,需先删除再重建:
json
// 1. 删除旧别名(通过更新mapping,移除别名定义)
PUT /product_index/_mapping
{
"properties": {
"amount": null // 置为null表示删除该别名
}
}
// 2. 新增新别名(指向其他字段)
PUT /product_index/_mapping
{
"properties": {
"amount": {
"type": "alias",
"path": "product_id" // 改为指向product_id
}
}
}
5. 查看字段别名(验证配置)
json
// 方式1:查看索引完整mapping,包含别名
GET /product_index/_mapping
// 方式2:通过字段别名API查询
GET /product_index/_alias/user_name // 查询特定别名的指向
四、常见限制与注意事项
- 写入限制 :别名仅支持查询 / 聚合,写入、更新、删除操作必须使用原字段名,否则会报错
illegal_argument_exception: alias [xxx] cannot be used in write operations; - 嵌套限制 :别名可指向嵌套字段(如
user.info.city),但不能指向别名(即不支持「别名的别名」); - 索引模板:可在索引模板(Index Template)中定义别名,让新建索引自动继承别名配置;
- 跨索引别名:字段别名仅作用于单个索引,跨索引的「别名」需使用「索引别名(Index Alias)」(注意区分字段别名和索引别名)。
五、字段别名 vs 索引别名(核心区别)
很多新手会混淆字段别名和索引别名,两者核心差异如下:
表格
| 维度 | 字段别名(Field Alias) | 索引别名(Index Alias) |
|---|---|---|
| 作用对象 | 索引内的单个字段 | 整个索引(或多个索引) |
| 核心用途 | 简化字段查询 / 聚合,适配字段名变更 | 简化索引访问,实现索引无缝切换 |
| 写入支持 | 不支持(仅只读) | 支持(可通过别名写入索引) |
示例:索引别名是给 product_index_v1 起别名 product_index,字段别名是给 product_index 内的 price 起别名 amount。
总结
- 字段别名(Alias)是字段的「只读快捷方式」,核心用于简化查询 / 聚合、适配字段名变更,不支持写入;
- 一个别名仅指向一个原字段,可在索引创建后新增 / 删除(修改需先删后加);
- 支持指向普通字段、嵌套字段,需区分「字段别名」和「索引别名」,前者作用于字段,后者作用于索引。
简单来说,字段别名解决的是「字段名层面的灵活访问」问题,是 ES 中优化字段使用体验的轻量级方案。
Elasticsearch 单值多字段类型(Multi-fields):一个值,多种索引方式
Elasticsearch 中的「单值多字段(Multi-fields)」是指为同一个原始字段值配置多种不同的字段类型 / 分析器,让一个字段值同时支持多种查询场景(比如既做全文检索,又做精准匹配),是处理「同一字段多维度使用」的核心方案。
一、核心概念(通俗理解)
你可以把它想象成:给同一个「原始数据」(比如商品名称 "华为手机Pro"),同时生成「两个不同格式的副本」:
- 副本 1:按
text类型 + 中文分词器处理,支持「全文检索」(比如查「华为」「手机」都能匹配); - 副本 2:按
keyword类型处理,支持「精准匹配 / 聚合排序」(比如按完整名称"华为手机Pro"分组)。原始字段值只存一份,但 ES 会为每个副本创建独立的倒排索引,满足不同查询需求。
二、核心特性
表格
| 特性 | 说明 |
|---|---|
| 数据存储 | 原始值仅存储一份,多字段是「索引层面的副本」,不额外占用存储 |
| 字段命名 | 多字段默认以「原字段名。子字段名」命名(如 name.keyword) |
| 分析器配置 | 不同子字段可配置不同分析器(如中文分词、英文分词、不分词) |
| 适用场景 | 同一字段需同时支持「全文检索 + 精准匹配」「分词查询 + 聚合排序」等场景 |
三、完整使用示例(商品名称多字段配置)
场景:商品名称需同时支持「全文检索」和「精准聚合」
1. 创建索引时显式定义多字段
json
PUT /product_multi_field
{
"mappings": {
"properties": {
"product_name": { // 原始字段(主字段)
"type": "text", // 主类型:text,支持全文检索
"analyzer": "ik_max_word", // 中文分词器(需安装IK插件)
"fields": { // 多字段配置:为同一个值定义其他类型
"keyword": { // 子字段1:keyword类型,精准匹配
"type": "keyword",
"ignore_above": 256 // 超过256字符的部分忽略
},
"english": { // 子字段2:text+英文分词器,适配英文名称
"type": "text",
"analyzer": "english"
}
}
},
"price": { "type": "float" }
}
}
}
配置说明:
product_name(主字段):text类型 + IK 分词,支持中文全文检索;product_name.keyword:keyword类型,支持精准匹配 / 聚合;product_name.english:text类型 + 英文分词器,支持英文名称检索。
2. 写入数据(只需写原始字段)
json
PUT /product_multi_field/_doc/1
{
"product_name": "华为手机Pro 2024款",
"price": 4999.0
}
PUT /product_multi_field/_doc/2
{
"product_name": "iPhone 15 Pro Max",
"price": 8999.0
}
关键 :写入时只需指定 product_name,ES 会自动为 product_name.keyword/product_name.english 生成对应索引,无需手动写入子字段。
3. 多字段的核心查询 / 聚合用法
场景 1:主字段全文检索(中文分词)
json
GET /product_multi_field/_search
{
"query": {
"match": {
"product_name": "华为手机" // 匹配分词后的「华为」「手机」
}
}
}
结果:返回 product_name 为「华为手机 Pro 2024 款」的文档。
场景 2:子字段精准匹配(keyword)
json
GET /product_multi_field/_search
{
"query": {
"term": {
"product_name.keyword": "华为手机Pro 2024款" // 精准匹配完整名称
}
}
}
结果:仅返回完全匹配该名称的文档(避免分词导致的模糊匹配)。
场景 3:子字段英文分词检索
json
GET /product_multi_field/_search
{
"query": {
"match": {
"product_name.english": "iphone pro" // 英文分词后匹配
}
}
}
结果:返回 product_name 为「iPhone 15 Pro Max」的文档。
场景 4:子字段聚合(keyword)
json
GET /product_multi_field/_search
{
"size": 0,
"aggs": {
"group_by_product_name": {
"terms": {
"field": "product_name.keyword", // 按完整名称精准分组
"size": 10
}
}
}
}
结果 :按「华为手机 Pro 2024 款」「iPhone 15 Pro Max」分别分组(若用主字段 product_name 聚合,会按分词后的单个词分组,结果无意义)。
4. 自动生成的多字段(ES 默认行为)
ES 对所有 text 类型字段,会自动生成一个 .keyword 子字段(无需显式定义),比如:
json
// 仅定义text类型字段
PUT /test_index
{
"mappings": {
"properties": {
"name": { "type": "text" }
}
}
}
// 写入数据后,可直接用 name.keyword 精准匹配
GET /test_index/_search
{
"query": {
"term": { "name.keyword": "张三" }
}
}
这是 ES 最常用的多字段场景,也是新手容易忽略的点(比如之前你遇到的 hobbies.port.keyword 就是自动生成的)。
四、多字段的常见配置与限制
1. 新增多字段(索引已创建后)
可通过更新 mapping 为已有字段新增多字段(无需重建索引,但需重新索引数据才能生效):
json
// 为product_name新增「拼音分词」子字段
PUT /product_multi_field/_mapping
{
"properties": {
"product_name": {
"type": "text",
"analyzer": "ik_max_word",
"fields": {
"keyword": { "type": "keyword", "ignore_above": 256 },
"english": { "type": "text", "analyzer": "english" },
"pinyin": { // 新增拼音子字段(需安装拼音分词器)
"type": "text",
"analyzer": "pinyin_analyzer"
}
}
}
}
}
2. 核心限制
- 多字段仅针对「同一个原始值」,无法为不同值配置不同子字段;
- 子字段的类型需与主字段兼容(比如主字段是
text,子字段可设为keyword/text,但不能设为integer); - 新增多字段后,需重新索引历史数据(否则历史数据的子字段无索引,查询不到)。
五、适用场景总结
表格
| 场景 | 多字段配置方案 |
|---|---|
| 中文全文检索 + 精准匹配 / 聚合 | 主字段:text+IK 分词;子字段:keyword |
| 英文名称检索 + 中文名称检索 | 主字段:text + 中文分词;子字段:text + 英文分词 |
| 日期字段(字符串)+ 排序 | 主字段:text;子字段:date |
| 数值字段(字符串)+ 范围查询 | 主字段:text;子字段:integer/float |
总结
- 单值多字段(Multi-fields)是让一个原始字段值同时支持多种索引方式的特性,核心解决「同一字段需满足不同查询 / 聚合需求」的问题;
- 最常用的配置是「text(主字段,全文检索)+ keyword(子字段,精准匹配 / 聚合)」,ES 会为 text 字段自动生成
.keyword子字段; - 多字段仅在索引层面生成副本,不额外存储原始数据,写入只需操作主字段,查询 / 聚合可指定子字段(格式:原字段名。子字段名)。
简单来说,多字段的核心价值是「一份数据,多种索引,适配多场景使用」,是 ES 处理复杂字段查询的必备技巧。
Elasticsearch Runtime 类型(Runtime Fields)详解
Runtime Fields(运行时字段)是 ES 7.11+ 引入的核心特性,简单来说:它是在查询 / 聚合时动态计算的字段,不存储在磁盘上,仅在内存中临时生成。
可以把它理解为 ES 的「虚拟字段」------ 不用提前写入文档,也不用修改索引映射,就能基于现有字段动态生成新字段,解决传统静态映射的灵活性问题。
一、核心特点
- 无存储开销:不占用磁盘空间,数据仅在查询时计算,适合临时分析、数据修正场景。
- 无需重建索引:传统字段修改需要重建索引(耗时耗资源),Runtime 字段直接在查询时定义,立即生效。
- 灵活适配:支持基于现有字段做数据转换(如字符串转数字、时间格式修正、多字段拼接)。
- 支持多类型 :Runtime 字段支持 ES 所有基础类型(
long/double/keyword/date/boolean等)。
二、使用场景(新手最易理解的场景)
1. 临时修正数据格式
比如原始日志中「金额」字段是字符串("amount": "100.5"),想按数值聚合,不用重新导入数据,直接用 Runtime 转成 double。
2. 多字段拼接 / 计算
比如现有 first_name 和 last_name,想临时生成 full_name 字段("full_name": "张三 李四")。
3. 兼容历史数据
老数据的时间字段格式不统一(有的是 yyyy-MM-dd,有的是 yyyy/MM/dd),用 Runtime 统一转成标准 date 类型。
4. 临时新增分析维度
比如基于 order_time(下单时间)动态生成 order_hour(下单小时)、order_weekday(下单星期),用于按小时 / 星期聚合。
三、使用方式(两种核心方式)
Runtime 字段有两种定义方式,新手优先掌握「查询时临时定义」,进阶再用「映射中预定义」。
方式 1:查询时临时定义(最常用,推荐新手)
在 search 请求中通过 runtime_mappings 定义,仅对本次查询生效。
示例 1:字符串转数值 原始文档:{"amount_str": "99.9", "product": "手机"}查询时把 amount_str 转成数值型 amount_num,并按数值聚合:
json
GET /order_index/_search
{
"runtime_mappings": {
"amount_num": { // 定义Runtime字段名
"type": "double", // 字段类型
"script": { // 核心:计算逻辑(Painless脚本)
"source": "emit(Double.parseDouble(doc['amount_str'].value))"
}
}
},
"aggs": {
"avg_amount": { // 用Runtime字段做聚合
"avg": {
"field": "amount_num"
}
}
},
"fields": ["amount_str", "amount_num"] // 返回Runtime字段
}
- 关键语法:
emit()是 Painless 脚本的「输出函数」,把计算结果赋值给 Runtime 字段。 - 效果:本次查询会临时生成
amount_num字段,值为99.9,可用于过滤、排序、聚合。
示例 2:动态生成日期维度 原始文档:{"order_time": "2026-03-21 14:30:00"}生成 order_hour(下单小时)和 order_weekday(星期几):
json
GET /order_index/_search
{
"runtime_mappings": {
"order_hour": {
"type": "integer",
"script": {
"source": "emit(LocalDateTime.parse(doc['order_time'].value, DateTimeFormatter.ofPattern('yyyy-MM-dd HH:mm:ss')).getHour())"
}
},
"order_weekday": {
"type": "keyword",
"script": {
"source": "def weekday = LocalDateTime.parse(doc['order_time'].value, DateTimeFormatter.ofPattern('yyyy-MM-dd HH:mm:ss')).getDayOfWeek(); emit(weekday.toString());"
}
}
},
"fields": ["order_time", "order_hour", "order_weekday"]
}
输出结果示例:
json
"fields": {
"order_time": ["2026-03-21 14:30:00"],
"order_hour": [14],
"order_weekday": ["SATURDAY"]
}
方式 2:映射中预定义(长期生效)
如果某个 Runtime 字段需要长期使用,可在索引映射中定义,后续所有查询都能直接使用:
json
# 1. 创建索引时定义Runtime字段
PUT /order_index
{
"mappings": {
"runtime": { // 预定义Runtime字段
"amount_num": {
"type": "double",
"script": {
"source": "emit(doc['amount_str'].value != null ? Double.parseDouble(doc['amount_str'].value) : 0)"
}
}
},
"properties": { // 普通存储字段
"amount_str": {"type": "keyword"},
"product": {"type": "keyword"}
}
}
}
# 2. 后续查询可直接使用amount_num
GET /order_index/_search
{
"aggs": {
"avg_amount": {
"avg": {"field": "amount_num"}
}
}
}
四、Runtime 字段 vs 普通字段(核心区别)
表格
| 特性 | 普通字段(Stored Fields) | Runtime 字段(Runtime Fields) |
|---|---|---|
| 存储方式 | 持久化到磁盘 | 仅查询时内存计算,不存储 |
| 索引重建 | 修改需重建索引 | 无需重建,立即生效 |
| 查询性能 | 快(直接读取磁盘) | 稍慢(需实时计算) |
| 适用场景 | 高频查询、核心业务字段 | 临时分析、数据修正、低频查询 |
五、新手注意事项
- 性能权衡:Runtime 字段会增加查询时的 CPU 开销(因为要实时计算),不建议用于高频、高并发的核心查询。
- 脚本限制 :计算逻辑基于 Painless 脚本,语法要符合 ES 规范(比如空值判断,避免
NullPointerException)。 - 版本要求:必须 ES 7.11+,Kibana 需同步版本才能在可视化中使用 Runtime 字段。
- 支持的操作 :Runtime 字段支持过滤、排序、聚合,但不支持全文检索(因为没有倒排索引)。
总结
- Runtime 字段是 ES 的「虚拟字段」:查询时动态计算,不存储、不占磁盘,无需重建索引。
- 核心价值是灵活性:解决静态映射的修改成本高、数据格式不统一等问题,适合临时分析 / 数据修正。
- 性能需权衡:查询时计算会增加 CPU 开销,优先用于低频分析场景,核心字段仍推荐用普通存储字段。
如果需要针对具体场景(比如日志分析、电商数据统计)写可直接运行的 Runtime 字段示例,可以告诉我,我帮你定制~