ElasticSearch基础数据查询和管理详解

目录

[一、 ElasticSearch核心概念](#一、 ElasticSearch核心概念)

[1. 全文搜索(Full-Text Search)](#1. 全文搜索(Full-Text Search))

[2. 倒排索引(Inverted Index)](#2. 倒排索引(Inverted Index))

[3. ElasticSearch常用术语](#3. ElasticSearch常用术语)

[3.1 映射(Mapping)](#3.1 映射(Mapping))

[3.2 索引(Index)](#3.2 索引(Index))

[3.3 文档(Document)](#3.3 文档(Document))

二、映射管理(Mapping)

三、索引管理(Index)

[1. 索引的基本操作](#1. 索引的基本操作)

[1.1 创建索引](#1.1 创建索引)

[1.2 删除索引](#1.2 删除索引)

[1.3 查询索引](#1.3 查询索引)

[1.4 修改索引](#1.4 修改索引)

[2. 索引别名](#2. 索引别名)

如何为索引添加别名

多索引检索的实现方案

四、文档管理(Document)

[1. 文档的基本操作](#1. 文档的基本操作)

[1.1 新增文档](#1.1 新增文档)

[1.2 查询文档](#1.2 查询文档)

[1.3 删除文档](#1.3 删除文档)

[1.4 更新文档](#1.4 更新文档)

[五、Elasticsearch 多表关联方案详解](#五、Elasticsearch 多表关联方案详解)

[1. 方案对比概览](#1. 方案对比概览)

[2. 嵌套对象 (Nested Object)](#2. 嵌套对象 (Nested Object))

[3. Join 父子文档 (Parent-Child)](#3. Join 父子文档 (Parent-Child))

[4. 宽表冗余存储 (Denormalization)](#4. 宽表冗余存储 (Denormalization))

[5. 终极建议](#5. 终极建议)


一、 ElasticSearch核心概念

1. 全文搜索(Full-Text Search)

全文搜索是通过扫描文档内容,为每个词语建立包含出现次数和位置信息的索引。当用户发起查询时,系统根据预先构建的索引快速定位匹配结果并返回。

在Elasticsearch中,全文搜索专门针对文本类型(text)字段,能够智能处理自然语言:它不仅匹配精确词语,还会识别词语变体和同义词,并按相关性对结果进行排序。

2. 倒排索引(Inverted Index)

倒排索引是全文检索系统的核心数据结构。与传统正排索引(通过文档ID定位文档内容)不同,倒排索引通过单词(term)反向查找包含该词的文档。

倒排索引包含两个关键组成部分:

  1. 词典:存储所有不重复单词的列表
  2. 倒排列表:记录每个单词对应的所有文档信息,包括出现位置、频率等关键数据

3. ElasticSearch常用术语

我们可以对比MySQL来理解Elasticsearch,如下图所示。左侧是MySQL的基本概念,右侧是Elasticsearch对应的相似概念的定义。借由这种对比,我们可以更直观地看出Elasticsearch与传统数据库之间的关系及差异。

注意:在Elasticsearch 6.X之前的版本中,索引类似于SQL数据库,而type(类型)类似于表。然而,从ES 7.x版本开始,类型已经被弃用,一个索引只能包含一个文档类型。

3.1 映射(Mapping)

定义

映射是索引的数据结构定义,相当于关系型数据库的表结构(Schema)。

3.2 索引(Index)

定义

索引是文档的容器,相当于关系型数据库中的表。

注意:在Elasticsearch 6.X之前的版本中,索引类似于SQL数据库,从ES 7.x版本开始,一个索引只能包含一个文档类型。

3.3 文档(Document)

定义

文档是 Elasticsearch 中的基本数据单元,相当于关系型数据库中的一行记录。

二、映射管理(Mapping)

映射是索引的数据结构定义,相当于关系型数据库的表结构(Schema)。创建表的时候,需要加上表结构,而在elasticSearch中,创建索引,也就需要映射。

创建索引基本语法

Lua 复制代码
PUT /索引名称
{
  "settings": {
    // 索引设置
  },
  "mappings": {
    "properties": {
      // 字段映射
    }
  }
}

详细栗子

Lua 复制代码
PUT /books
{
  "settings": {
    "analysis": {  // 自定义分析器配置
      "analyzer": {
        "my_custom_analyzer": {  // 自定义分析器
          "type": "custom",
          "tokenizer": "ik_max_word",  // 使用IK分词器
          "filter": ["lowercase", "asciifolding"]  // 转小写+去音标符号
        }
      }
    },
    "number_of_shards": 3,    // 主分片数
    "number_of_replicas": 1    // 副本数
  },
  "mappings": {
    "dynamic": "strict",  // 动态映射策略:strict=禁止未知字段
    "_source": {          // 源数据存储控制
      "enabled": true,    // 存储原始JSON
      "excludes": ["internal_notes"]  // 排除敏感字段
    },
    "properties": {
      // 文本类型字段
      "title": { 
        "type": "text",
        "analyzer": "ik_max_word",    // 索引时使用细粒度分词
        "search_analyzer": "ik_smart", // 搜索时使用粗粒度分词
        "fielddata": true,  //启用在 text 字段上的聚合(Aggregation)/启动在 text 字段上的排序(Sorting)
        "fields": {                   // 多字段配置(多字段配置 - 为同一数据创建多种索引方式)
          "keyword": {                // 子字段用于精确匹配
            "type": "keyword",
            "ignore_above": 256       // 超过256字符不索引(意思就是大于256字符,就不会查询得到)
          },
          "english": {                // 英文分词字段
            "type": "text",
            "analyzer": "english"
          }
        },
        "norms": false,               // 是否存储长度信息
        "index_options": "positions"  // 存储位置信息用于短语查询(存储哪些内容)
      },
      
      // 关键词类型字段
      "author": { 
        "type": "keyword",
        "null_value": "佚名",          // 空值替换
        "ignore_above": 256            // 超长值处理
      },
      
      // 数值类型字段
      "price": { 
        "type": "scaled_float",       // 推荐数值类型
        "scaling_factor": 100,        // 放大因子(存储整数)
        "doc_values": true            // 启用聚合排序
      },
      "discount_rate": {
        "type": "float",              // 标准浮点类型
        "index": false                // 禁用索引
      },
      
      // 日期类型字段
      "publish_date": { 
        "type": "date",
        "format": "yyyy-MM-dd||epoch_millis",  // 多格式支持
        "doc_values": false,          // 禁用列式存储
        "ignore_malformed": true      // 忽略格式错误
      },
      
      // 复杂类型字段
      "tags": { 
        "type": "keyword",            // 标签数组
        "index": true,                // 启用索引
        "boost": 2.0                  // 权重提升
      },
      "metadata": {                   // 对象类型
        "type": "object",
        "properties": {
          "isbn": {"type": "keyword"},
          "page_count": {"type": "integer"}
        }
      },
      "reviews": {                    // 嵌套类型
        "type": "nested",             // 保持数组对象独立
        "properties": {
          "user": {"type": "keyword"},
          "rating": {"type": "byte"},
          "comment": {"type": "text"}
        }
      },
      
      // 其他特殊类型
      "description": { 
        "type": "text",
        "index": false,               // 仅存储不索引
        "store": true                 // 单独存储
      },
      "cover_image": {
        "type": "binary",             // 二进制类型
        "store": true
      },
      "last_access_ip": {
        "type": "ip"                  // IP地址类型
      },
      "coordinates": {
        "type": "geo_point"           // 地理坐标
      },
      "embedding": {                  // 向量搜索(8.x+)
        "type": "dense_vector",
        "dims": 128,                  // 向量维度
        "index": true,
        "similarity": "dot_product"   // 相似度算法
      }
    }
  }
}

字段类型定义(type)

  • 文本类型:text(全文搜索), keyword(精确匹配)

  • 数值类型:long, integer, float, double

  • 日期类型:date

  • 复杂类型:object, nested

具体关键配置详解如下

  • 字段类型定义
类型 配置参数 说明 适用场景
text analyzer search_analyzer 指定分词器 中文内容、描述文本
keyword ignore_above null_value 长度限制/空值处理 ID、状态码、标签
scaled_float scaling_factor 数值缩放因子 价格、评分等小数
date format ignore_malformed 格式/容错处理 时间戳、日期
nested - 保持数组对象独立 评论、子文档
dense_vector dims similarity 维度/相似度算法 AI向量搜索
  • 分析器配置
分析器类型 特点 示例值
standard 单字拆分 "中国" → ["中","国"]
ik_smart 最粗粒度 "中国人民银行" → ["中国人民银行"]
ik_max_word 最细粒度 "中国人民银行" → ["中国人","中国","人民","银行"]
english 英文处理 "running" → ["run"]
自定义分析器 组合处理 IK分词+小写转换

  • 索引控制参数
参数 值范围 作用 性能影响
index true/false 是否创建倒排索引 禁用可节省90%存储
doc_values true/false 是否启用列式存储 禁用将无法聚合排序
store true/false 是否单独存储 增加存储但加速检索
norms true/false 是否存储长度信息 节省5-10%空间
index_options docs/freqs/positions 索引内容粒度 位置信息支持短语查询
ignore_above 数字 超长值处理 避免大字段影响性能
null_value 指定值 空值替换 保证数据完整性

index_options 参数详解

index_options 是 Elasticsearch 中控制倒排索引内容粒度的关键参数,它决定了在索引过程中存储哪些信息,直接影响搜索功能和性能。以下是深度解析:

可选值及含义:

存储内容 支持功能 资源消耗 适用场景 限制
docs 只存储文档ID 仅能判断文档是否存在 最低 过滤型字段(如状态标志) ❌ 不支持相关性排序 ❌ 不支持短语查询
freqs 文档ID + 词频 支持相关性评分 中等 简单搜索(无需短语查询) ❌ 不支持短语查询 ❌ 不支持高亮
positions (默认) 文档ID + 词频 + 位置 支持短语/邻近查询 较高 常规文本字段 ❌ 高亮效率较低
offsets 文档ID + 词频 + 位置 + 字符偏移 支持高亮显示 最高 需要高亮的字段 ⚠️ 存储开销增加40%

注:该参数仅适用于 textkeyword 类型字段


  • 高级特性
特性 配置 说明
多字段 fields 同一字段多种索引方式
动态映射 dynamic: strict 禁止未知字段自动映射
源数据过滤 _source.excludes 排除敏感字段
字段权重 boost 提升特定字段相关性
时间序列 time_series 优化时序数据存储(8.10+)

动态映射 dynamic 参数用于控制如何处理文档中出现的未在映射中定义的字段(即动态字段)。它有 4 种可选值,每种值对应不同的处理策略:

行为 适用场景 风险/限制
true (默认值) 自动检测新字段类型并添加到映射 开发环境、数据结构变化频繁 映射膨胀、类型推断错误
false 忽略新字段(不索引),但保留在 _source 日志类数据、未知字段不需搜索 无法搜索新字段
strict 拒绝包含新字段的文档 生产环境、严格数据管控 需预先定义完整映射
runtime (7.11+) 将新字段作为运行时字段处理 灵活查询未定义字段 性能低于索引字段

三、索引管理(Index)

索引是文档的容器,相当于关系型数据库中的表。索引是具有相同结构的文档的集合,由唯一索引名称标定。一个集群中有多个索引,不同的索引代表不同的业务类型数据。

1. 索引的基本操作

1.1 创建索引

创建索引的基本语法如下:

Lua 复制代码
PUT /index_name
{
  "settings": {
    // 索引设置
  },
  "mappings": {
    "properties": {
      // 字段映射
    }
  }
}
  • **索引名称 (index_name):**索引名称必须是小写字母,可以包含数字和下划线。

  • 索引设置 (settings)

    • 分片数量 (number_of_shards):一个索引的分片数决定了索引的并行度和数据分布。

    • 副本数量 (number_of_replicas):副本提高了数据的可用性和容错能力。

  • **映射 (mappings):**字段属性 (properties)定义索引中文档的字段及其类型。

具体定义看映射管理。


实践练习

创建一个名为 student_index 的索引,并设置一些以下自定义字段

  • name(学生姓名):text 类型
  • age(年龄):integer 类型
  • enrolled_date(入学日期):date 类型
Lua 复制代码
PUT /student_index
{
  "settings": {
    "number_of_shards": 1,
    "number_of_replicas": 1
  },
  "mappings": {
    "properties": {
      "name": {
        "type": "text"
      },
      "age": {
        "type": "integer"
      },
      "enrolled_date": {
        "type": "date"
      }
    }
  }
}

插入一条文档,方便下面进行测试

Lua 复制代码
POST /student_index/_create/1
{
  "name": "John",
  "age": 18,
  "enrolled_date": "2006-12-12"
}
1.2 删除索引
ruby 复制代码
DELETE /student_index   // 谨慎操作!不可逆
1.3 查询索引

查询操作可以分为两类:检索索引信息搜索索引中的文档

1.3.1 检索索引信息

基本语法如下:

Lua 复制代码
GET /index_name

栗子

Lua 复制代码
GET student_index

1.3.2 搜索索引中的文档

基本语法如下:

Lua 复制代码
GET /index_name/_search 
{ "query": { 
    // 查询条件 
    } 
}

栗子

Lua 复制代码
# 搜索 name 字段包含 John 的文档
GET /student_index/_search
{
  "query": {
    "match": {
      "name": "John"
    }
  }
}
1.4 修改索引

1.4.1 动态更新 settings

基本语法

Lua 复制代码
PUT /index_name/_settings
{
  "index": {
    "setting_name": "setting_value"
  }
}

代码示例

将 student_index 的副本数量更新为 2:

Lua 复制代码
PUT /student_index/_settings
{
  "index": {
    "number_of_replicas": 2
  }
}

1.4.2 动态更新 mapping

基本语法

Lua 复制代码
PUT /index_name/_mapping
{
  "properties": {
    "new_field": {
      "type": "field_type"
    }
  }
}

代码示例

向 student_index 添加一个名为 grade 的新字段,类型为 integer:

Lua 复制代码
PUT /student_index/_mapping
{
  "properties": {
    "grade": {
      "type": "integer"
    }
  }
}

2. 索引别名

索引别名是 Elasticsearch 中一个强大的功能,它允许你为索引创建一个或多个替代名称,类似于文件系统中的快捷方式或符号链接。下面我将全面解析索引别名的概念、用法和实战场景。

  • 一个逻辑名称,可以指向一个或多个物理索引

  • 提供抽象层,解耦应用和物理存储结构

  • 支持无缝切换索引(零停机维护)

如何为索引添加别名
  • 创建索引的时候可以指定别名
Lua 复制代码
PUT student_index
{
  "aliases": {
    "student_index_alias": {}
  },
  "settings": {
    "refresh_interval": "30s",
    "number_of_shards": 1,
    "number_of_replicas": 0
  },
  "mappings": {
    "properties": {
      "name": {
        "type": "text"
      },
      "age": {
        "type": "integer"
      },
      "enrolled_date": {
        "type": "date"
      }
    }
  }
}
  • 为已有索引添加别名
Lua 复制代码
POST /_aliases
{
  "actions": [
    {
      "add": {
        "index": "student_index",
        "alias": "student_index_alias"
      }
    }
  ]
}
多索引检索的实现方案
  • 不使用别名的方案
  • 方式一:使用逗号对多个索引名称进行分隔
bash 复制代码
POST gorgor_logs_202501,gorgor_logs_202502,gorgor_logs_202503/_search
  • 方式二:使用通配符进行多索引检索
bash 复制代码
POST gorgor_logs_*/_search
  • 使用别名的方案
  1. 使别名关联已有索引
    示例
bash 复制代码
PUT gorgor_logs_202501
PUT gorgor_logs_202502
PUT gorgor_logs_202503

POST _aliases
{
  "actions": [
    {
      "add": {
        "index": "gorgor_logs_202501",
        "alias": "gorgor_logs_2025"
      }
    },
    {
      "add": {
        "index": "gorgor_logs_202502",
        "alias": "gorgor_logs_2025"
      }
    },
    {
      "add": {
        "index": "gorgor_logs_202503",
        "alias": "gorgor_logs_2025"
      }
    }
  ]
}
  1. 使用别名进行检索
    示例
bash 复制代码
POST gorgor_logs_2025/_search

四、文档管理(Document)

文档是 Elasticsearch 中的基本数据单元,相当于关系型数据库中的一行记录。文档是指存储在Elasticsearch索引中的JSON对象。

1. 文档的基本操作

1.1 新增文档

在ES8.x中,新增文档的操作可以通过POST或PUT请求完成,具体取决于是否指定了文档的唯一性标识(即ID)。如果在创建数据时指定了唯一性标识,可以使用POST或PUT请求;如果没有指定唯一性标识,只能使用POST请求。

**简单理解:PUT 必须指定唯一性标识,**POST可以指定,也可以不指定,不指定则 elasticsearch 会帮忙生成一个。

1.1.1 使用POST请求新增文档

当不指定文档ID时,可以使用POST请求来新增文档,Elasticsearch会自动生成一个唯一的ID。语法如下:

bash 复制代码
POST /<index_name>/_doc
{
 "field1": "value1",
 "field2": "value2",
 // ... 其他字段
}

1.1.2 使用PUT请求新增文档

当指定了文档的唯一性标识(ID)时,可以使用PUT请求来新增或更新文档。如果指定的ID在索引中不存在,则会创建一个新文档;如果已存在,则会替换现有文档。语法如下:

bash 复制代码
PUT /<index_name>/_doc/<document_id>
{
 "field1": "value1",
 "field2": "value2",
 // ... 其他字段
}

1.1.3 批量新增文档
在Elasticsearch 8.x中,批量新增文档可以通过_bulk API来实现。这个API允许您将多个索引、更新或删除操作组合成一个单一的请求,从而提高批量操作的效率。
Elasticsearch的_bulk API支持以下四种操作类型:

  • Index: 用于创建新文档或替换已有文档。
  • Create: 如果文档不存在则创建,如果文档已存在则返回错误。
  • Update: 用于更新现有文档。
  • Delete: 用于删除指定的文档。

以下是使用_bulk API的基本语法:

bash 复制代码
POST /<index_name>/_bulk
# index
{ "index" : { "_index" : "<index_name>", "_id" : "<optional_document_id>" } }
{ "field1" : "value1", "field2" : "value2", ... }

# update
{ "update" : { "_index" : "<index_name>", "_id" : "<document_id>" } }
{ "doc" : {"field1" : "new_value1", "field2" : "new_value2", ... }, "_op_type" : "update" }

# delete
{ "delete" : { "_index" : "<index_name>", "_id" : "<document_id>" } }

# create
{ "create" : { "_index" : "<index_name>", "_id" : "<optional_document_id>" } }
{ "field1" : "value1", "field2" : "value2", ... }

PUT和POST的区别
在Elasticsearch 8.x中,PUT和POST请求在新增文档时的行为有所不同,主要体现在以下几个方面:

  • 指定文档ID:
    • PUT请求在创建或更新文档时必须指定文档的唯一ID。如果指定的ID已经存在,PUT请求会替换现有文档;如果不存在,则创建一个新文档。
  • POST请求在创建新文档时可以指定ID,也可以不指定。如果不指定ID,Elasticsearch会自动生成一个唯一的ID。
  • 幂等性:
    • PUT请求是幂等的,这意味着多次执行相同的PUT请求,即使是针对同一个文档,最终的结果都是一致的。
  • POST请求不是幂等的,多次执行相同的POST请求可能会导致创建多个文档。
  • 更新行为:
    • PUT请求在更新文档时会替换整个文档的内容,即使是文档中未更改的部分也会被新内容覆盖。
    • POST请求在更新文档时可以使用_update API,这样可以只更新文档中的特定字段,而不是替换整个文档。

1.2 查询文档

1.2.1 根据id查询文档

在Elasticsearch 8.x中,根据文档的ID查询单个文档的标准语法是使用GET请求配合文档所在的索引名和文档ID。以下是具体的请求格式:

bash 复制代码
GET /<index_name>/_doc/<document_id>

1.2.2 根据id批量查询文档

在Elasticsearch 8.x中,使用Multi GET API可以根据ID查询多个文档。该API允许您在单个请求中指定多个文档的ID,并返回这些文档的信息。以下是Multi GET API的基本语法:

bash 复制代码
GET /<index_name>/_mget
{
 "ids" : ["id1", "id2", "id3", ...]
}

1.2.3 根据搜索关键词查询文档

在Elasticsearch 8.x中,查询文档通常使用Query DSL(Domain Specific Language),这是一种基于JSON的语言,用于构建复杂的搜索查询。

以下是一些常用的查询语法:

  • 匹配所有文档
bash 复制代码
GET /<index_name>/_search
{
 "query": {
    "match_all": {}
 }
}
  • 模糊匹配(分词)
bash 复制代码
GET /<index_name>/_search
{
 "query": {
    "match": {
      "<field_name>": "<query_string>"
    }
 }
}
  • 精确匹配(不分词)
bash 复制代码
GET /<index_name>/_search
{
 "query": {
    "term": {
      "<field_name>": {
        "value": "<exact_value>"
      }
    }
 }
}
  • 范围查询
bash 复制代码
GET /<index_name>/_search
{
 "query": {
    "range": {
      "<field_name>": {
        "gte": <lower_bound>,
        "lte": <upper_bound>
      }
    }
 }
}

1.3 删除文档

1.3.1 删除单个文档

在Elasticsearch 8.x中,删除单个文档的基本HTTP请求语法是:

bash 复制代码
DELETE /<index_name>/_doc/<document_id>

1.3.2 批量删除文档
在Elasticsearch 8.x中,删除多个文档可以通过两种主要方法实现:

  • 使用 _bulk API_bulk API允许您发送一系列操作请求,包括删除操作。每个删除请求是一个独立的JSON对象,格式如下:
bash 复制代码
POST /_bulk
{ "delete": {"_index": "{index_name}", "_id": "{id}"} }
{ "delete": {"_index": "{index_name}", "_id": "{id}"} }
{ "delete": {"_index": "{index_name}", "_id": "{id}"} }
  • 使用 _delete_by_query API_delete_by_query API允许您根据查询条件删除文档。如果您想删除特定索引中匹配特定查询的所有文档,可以使用以下请求格式:
bash 复制代码
POST /{index_name}/_delete_by_query
{
 "query": {
    "<your_query>"
 }
}

1.4 更新文档

1.4.1 更新单个文档

在Elasticsearch 8.x版本中,更新操作通常通过_update接口执行,该接口允许您部分更新现有文档的字段。以下是更新文档的基本语法:

bash 复制代码
POST /{index_name}/_update/{id}
{
 "doc": {
    "<field>: <value>"
 }
}

1.4.2 批量更新文档
在Elasticsearch 8.x中,更新多个文档可以通过两种主要方法实现:

  • 使用 _bulk API
bash 复制代码
POST /_bulk
{ "update" : {"_index" : "<index_name>", "_id" : "<document_id>"} }
{ "doc" : {"field1" : "new_value1", "field2" : "new_value2"}, "upsert" : {"field1" : "new_value1", "field2" : "new_value2"} }
...

在这个请求中,每个update块代表一个更新操作,其中_index和_id指定了要更新的文档,doc部分包含了更新后的文档内容,upsert部分定义了如果文档不存在时应该插入的内容。

  • 使用 _update_by_query API_update_by_query API允许您根据查询条件更新多个文档。这个操作是原子性的,意味着要么所有匹配的文档都被更新,要么一个都不会被更新。
bash 复制代码
POST /<index_name>/_update_by_query
{
 "query": {
    <!-- 定义更新文档的查询条件 -->
 },
 "script": {
    "source": "ctx._source.field = 'new_value'",
    "lang": "painless"
 }
}

在这个请求中,是您要更新的索引名称,query部分定义了哪些文档需要被更新,script部分定义了如何更新这些文档的字段。

并发场景下更新文档如何保证线程安全

在Elasticsearch 7.x及以后的版本中,_seq_no和_primary_term取代了旧版本的_version字段,用于控制文档的版本。_seq_no代表文档在特定分片中的序列号,而_primary_term代表文档所在主分片的任期编号。这两个字段共同构成了文档的唯一版本标识符,用于实现乐观锁机制,确保在高并发环境下文档的一致性和正确更新。

当在高并发环境下使用乐观锁机制修改文档时,要带上当前文档的_seq_no和_primary_term进行更新:

bash 复制代码
POST /employee/_doc/1?if_seq_no=13&if_primary_term=1
{
  "name": "小明",
  "sex": 1,
  "age": 25
}

如果_seq_no和_primary_term不对,会抛出版本冲突异常:

{ "error": { "root_cause": [ { "type": "version_conflict_engine_exception", "reason": "[1]: version conflict, required seqNo [13], primary term [1]. current document has seqNo [14] and primary term [1]", "index_uuid": "7JwW1djNRKymS5P9FWgv7Q", "shard": "0", "index": "employee" } ], "type": "version_conflict_engine_exception", "reason": "[1]: version conflict, required seqNo [13], primary term [1]. current document has seqNo [14] and primary term [1]", "index_uuid": "7JwW1djNRKymS5P9FWgv7Q", "shard": "0", "index": "employee" }, "status": 409 }

五、Elasticsearch 多表关联方案详解

在 Elasticsearch 中实现类似关系型数据库的多表关联,主要有四种核心方案,各有适用场景和性能特点:

1. 方案对比概览

方案 原理 优点 缺点 适用场景
嵌套对象 文档内嵌子对象 查询快、数据局部性 更新成本高 一对少、读多写少
Join父子文档 父子文档跨存储 支持一对多、独立更新 查询性能较低 频繁更新子文档
宽表冗余 字段冗余存储 最佳性能、简单 数据冗余、更新复杂 读密集型场景
业务端关联 应用层处理 灵活、无ES限制 网络开销大 复杂关联关系

各项详解:

  • 嵌套对象(Nested Object)
    Nested类型适用于一对少量、子文档偶尔更新、查询频繁的场景。如果需要索引对象数组并保持数组中每个对象的独立性,则应使用Nested数据类型而不是Object数据类型。
    Nested类型的优点是Nested文档可以将父子关系的两部分数据关联起来(例如博客与评论),可以基于Nested类型做任何查询。其缺点则是查询相对较慢,更新子文档时需要更新整篇文档。
  • Join父子文档类型
    Join类型用于在同一索引的文档中创建父子关系。Join类型适用于子文档数据量明显多于父文档的数据量的场景,该场景存在一对多量的关系,子文档更新频繁。举例来说,一个产品和供应商之间就是一对多的关联关系。当使用父子文档时,使用has_child或者has_parent做父子关联查询。
    Join类型的优点是父子文档可独立更新。缺点则是维护Join关系需要占据部分内存,查询较Nested类型更耗资源。
  • 宽表冗余存储
    宽表适用于一对多或者多对多的关联关系。
    宽表的优点是速度快。缺点则是索引更新或删除数据时,应用程序不得不处理宽表的冗余数据;并且由于冗余存储,某些搜索和聚合操作的结果可能不准确。
  • 业务端关联
    这是普遍使用的技术,即在应用接口层面处理关联关系。一般建议在存储层面使用两个独立索引存储,在实际业务层面这将分为两次请求来完成。
    业务端关联适用于数据量少的多表关联业务场景。数据量少时,用户体验好;而数据量多时,两次查询耗时肯定会比较长,反而影响用户体验。

2. 嵌套对象 (Nested Object)

原理

  • 将关联数据作为内嵌对象数组存储

  • 每个嵌套对象独立索引和查询

示例:订单与商品

bash 复制代码
PUT /orders
{
  "mappings": {
    "properties": {
      "order_id": {"type": "keyword"},
      "order_date": {"type": "date"},
      "items": {
        "type": "nested",  // 关键声明
        "properties": {
          "product_id": {"type": "keyword"},
          "name": {"type": "text"},
          "price": {"type": "float"},
          "quantity": {"type": "integer"}
        }
      }
    }
  }
}

查询嵌套对象

bash 复制代码
GET /orders/_search
{
  "query": {
    "nested": {
      "path": "items",
      "query": {
        "bool": {
          "must": [
            {"match": {"items.name": "手机"}},
            {"range": {"items.price": {"lt": 3000}}}
          ]
        }
      }
    }
  }
}

适用场景

  • 订单项与订单

  • 文章与评论

  • 用户与地址


3. Join 父子文档 (Parent-Child)

原理

  • 父子文档独立存储在同一分片

  • 通过 join 类型字段建立关联

示例:部门与员工

bash 复制代码
PUT /company
{
  "mappings": {
    "properties": {
      "relation": {
        "type": "join",  // 声明父子关系
        "relations": {
          "department": "employee"  // 部门是父,员工是子
        }
      },
      "name": {"type": "text"},
      "budget": {"type": "float"}
    }
  }
}

插入数据

bash 复制代码
// 父文档(部门)
PUT /company/_doc/d001
{
  "name": "研发部",
  "budget": 5000000,
  "relation": "department"  // 类型标识
}

// 子文档(员工)
PUT /company/_doc/e001?routing=d001  // 必须指定路由
{
  "name": "张三",
  "salary": 25000,
  "relation": {
    "name": "employee",  // 类型标识
    "parent": "d001"     // 父文档ID
  }
}

查询父子文档

bash 复制代码
// 1. 查询部门下所有员工
GET /company/_search
{
  "query": {
    "parent_id": {
      "type": "employee",
      "id": "d001"
    }
  }
}

// 2. 查询高薪员工的部门
GET /company/_search
{
  "query": {
    "has_child": {
      "type": "employee",
      "query": {"range": {"salary": {"gte": 30000}}}
    }
  }
}

适用场景

  • 部门与员工

  • 产品与评论

  • 博客与点赞


4. 宽表冗余存储 (Denormalization)

原理

  • 将关联数据直接复制到主文档

  • 通过应用层维护一致性

示例:用户+最新订单

bash 复制代码
PUT /users
{
  "mappings": {
    "properties": {
      "user_id": {"type": "keyword"},
      "name": {"type": "text"},
      "latest_order": {  // 冗余订单数据
        "properties": {
          "order_id": {"type": "keyword"},
          "amount": {"type": "float"},
          "items": {
            "type": "nested",
            "properties": {...}
          }
        }
      }
    }
  }
}

查询包含特定商品的用户

bash 复制代码
GET /users/_search
{
  "query": {
    "nested": {
      "path": "latest_order.items",
      "query": {
        "bool": {
          "must": [
            {"match": {"latest_order.items.product_name": "手机"}}
          ]
        }
      }
    }
  },
  "highlight": {
    "fields": {
      "latest_order.items.product_name": {}
    }
  }
}

适用场景

  • 用户最新订单

  • 商品最新评论

  • 实时统计指标


5. 终极建议

  • 读多写少 → 嵌套对象

  • 频繁更新 → 父子文档

  • 极致性能 → 宽表冗余

  • 复杂关联 → 业务端处理

相关推荐
阿里云大数据AI技术10 小时前
云上AI推理平台全掌握 (5):大模型异步推理服务
大数据·人工智能·llm
杨超越luckly11 小时前
HTML应用指南:利用GET请求获取全国奈雪的茶门店位置信息
大数据·前端·python·arcgis·信息可视化·html
李李不躺平11 小时前
数学基础弱能学好大数据技术吗?
大数据
isNotNullX13 小时前
数据集成难在哪?制造企业该怎么做?
大数据·数据库·数据仓库·人工智能·制造
阿里云大数据AI技术13 小时前
【新模型速递】PAI-Model Gallery云上一键部署Qwen3-Coder模型
大数据·人工智能·llm
weixin_lynhgworld14 小时前
家政小程序系统开发:开启智慧家政新时代
大数据·小程序
潮湿的心情14 小时前
绿色转向的时代红利:创新新材如何以技术与标准主导全球铝业低碳重构
大数据·人工智能·重构
小森( ﹡ˆoˆ﹡ )15 小时前
LangChain聊天机器人教程
大数据·langchain·机器人
Data-Miner15 小时前
数据分析的尽头是什么?是洞察,而非数字!
大数据·数据挖掘·数据分析