本篇文章主要讲解 ElasticSearch 中的数据建模内容,包括如何处理关联关系、索引重建和字段建模最佳实践等。
在 ElasticSearch 中处理关联关系
关系型数据库,一般会考虑 Normalize 数据;在 Elasticsearch,往往考虑 Denormalize 数据。
Denormalize 的好处:读的速度变快/无需表连接/无需行锁
Elasticsearch 并不擅长处理关联关系。我们一般采用以下四种方法处理关联:
- 对象类型
- 嵌套对象 (Nested Object)
- 父子关联关系 (Parent/Child)
- 应用端关联
特性 | 对象类型 | 嵌套对象 | 父子关联 | 应用端关联 |
---|---|---|---|---|
本质 | 默认处理 JSON 对象的方式 | 特殊的对象类型 | 同一索引内文档间的逻辑链接 | 由应用程序维护关联 |
存储结构 | 对象属性扁平化存储到父文档 | 作为父文档内部的独立隐藏文档 | 父子文档独立存储在同一分片 | 关联数据存储在不同文档或外部系统 |
数据边界 | 无边界,对象属性合并到父文档 | 有边界,每个嵌套对象独立存储 | 父子文档完全独立 | 完全独立,无 ES 内部关联 |
查询特点 | 可能匹配不同对象的字段组合 | 确保条件匹配同一嵌套对象内部 | 需用 has_child 、has_parent 等查询 |
需多次查询,先查主文档再查关联文档 |
更新特点 | 更新需重索引整个父文档 | 更新需重索引整个父文档 | 可单独更新子文档,不影响父文档 | 每个文档可单独更新 |
适用场景 | 简单键值对对象,无需独立查询 | 对象数组需独立查询和匹配 | 子文档频繁更新或数量大 | 关系简单、查询量小或数据分散 |
缺点 | 无法精确匹配数组内对象组合 | 更新成本高,嵌套数量有限制 | 查询性能低,内存消耗大 | 网络开销大,应用逻辑复杂 |
Nested Data Type
- Nested 数据类型:允许对象数组中的对象被独立索引。
- 使用 nested 和 properties 关键字,将所有数组索引到多个分隔的文档。
- 在内部,Nested 文档会被保存在两个 Lucene 文档中,在查询时做 Join 处理。
示例
json
PUT my_movies
{
"mappings": {
"properties": {
"actors": {
"type": "nested",
"properties": {
"first_name": {
"type": "keyword"
},
"last_name": {
"type": "keyword"
}
}
},
"title": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
}
}
}
}
}
若没有指定 "type": "nested"
时,数组数据会扁平化存储,搜索会出现不该出现的内容。
Parent / Child
对象和 Nested 对象每次更新都需要重新索引整个对象。
ES 提供了类似关系型数据库中 Join 的实现。可通过维护 Parent / Child 的关系,从而分离两个对象。
- 父文档和子文档是两个独立的文档。
- 更新父文档无需重新索引子文档。子文档被添加,更新或者删除也不会影响到父文档和其他的子文档。
示例
1、添加数据
json
PUT my_blogs
{
"settings": {
"number_of_shards": 2
},
"mappings": {
"properties": {
"blog_comments_relation": {
"type": "join",
"relations": {
"blog": "comment"
}
},
"content": {
"type": "text"
},
"title": {
"type": "keyword"
}
}
}
}
// 索引父文档
PUT my_blogs/_doc/blog2
{
"title":"Learning Hadoop",
"content":"learning Hadoop",
"blog_comments_relation":{
"name":"blog"
}
}
// 索引子文档
PUT my_blogs/_doc/comment1?routing=blog1
{
"comment":"I am learning ELK",
"username":"Jack",
"blog_comments_relation":{
"name":"comment",
"parent":"blog1"
}
}
// 索引子文档
PUT my_blogs/_doc/comment2?routing=blog2
{
"comment":"I like Hadoop!!!!!",
"username":"Jack",
"blog_comments_relation":{
"name":"comment",
"parent":"blog2"
}
}
// 索引子文档
PUT my_blogs/_doc/comment3?routing=blog2
{
"comment":"Hello Hadoop",
"username":"Bob",
"blog_comments_relation":{
"name":"comment",
"parent":"blog2"
}
}
2、父子关系查询
json
// 根据父文档ID查看
GET my_blogs/_doc/blog2
// 根据 Parent Id 查询
POST my_blogs/_search
{
"query": {
"parent_id": {
"type": "comment",
"id": "blog2"
}
}
}
// has_child 查询,返回父文档
POST my_blogs/_search
{
"query": {
"has_child": {
"type": "comment",
"query" : {
"match": {
"username" : "Jack"
}
}
}
}
}
// has_parent 查询,返回相关的子文档
POST my_blogs/_search
{
"query": {
"has_parent": {
"parent_type": "blog",
"query" : {
"match": {
"title" : "Learning Hadoop"
}
}
}
}
}
索引重建
需要索引重建的场景如下:
- 索引的 Mappings 发生变更:字段类型更改,分词器及字典更新。
- 索引的 Settings 发生变更:索引的主分片数发生改变。
- 集群内,集群间需要做数据迁移。
索引重建的方式:
- Update By Query:在现有索引上重建。
- Reindex:在其他索引上重建索引。
特性 | Update By Query | Reindex |
---|---|---|
核心操作 | 原地更新现有索引文档 | 复制数据到新索引 |
操作对象 | 单个索引 | 源索引和目标索引 |
数据迁移 | 不支持 | 支持 |
索引结构 | 不可更改 Mapping/Settings/分片 | 可更改 Mapping/Settings/分片 |
主要用途 | 批量修改文档内容或删除文档 | 重建索引结构/迁移数据 |
性能影响 | 对原索引读写压力大 | 源索引读压力,目标索引写压力 |
原子性 | 非原子操作,失败需手动处理 | 目标索引独立,失败可重试 |
适用场景 | 字段值更新、文档删除 | 更改字段类型、分片数或跨集群迁移等 |
字段建模
flowchart LR
字段类型["字段类型"] --> 是否要搜索及分词["是否要搜索及分词"]
是否要搜索及分词 --> a1["是否要聚合及排序"]
a1 --> 是否要额外的存储
Mapping 字段的相关设置:
- Enabled ------ 设置成 false,仅做存储,不支持搜索和聚合分析(数据保存在 _source 中)。
- Index ------ 是否构建倒排索引。设置成 false,无法被搜索,但还是支持 aggregation,并出现在 _source 中。
- Norms ------ 如果字段用来过滤和聚合分析,可以关闭,节约存储。
- Doc_values ------ 是否启用 doc_values,用于排序和聚合分析。
- Field_data ------ 如果要对 text 类型启用排序和聚合分析,fielddata 需要设置成 true。
- Store ------ 默认不存储,数据默认存储在 _source。
- Coerce ------ 默认开启,是否开启数据类型的自动转换(例如,字符串转数字)。
- Multifields 多字段特性。
- Dynamic ------ true/false/strict 控制 Mapping 的自动更新。
写在最后
这是该系列的第八篇,主要讲解 ElasticSearch 中的数据建模内容,包括如何处理关联关系、索引重建和字段建模最佳实践。可以自己去到 Kibana 的 Dev Tool 实战操作,未来会持续更新该系列,欢迎关注👏🏻。
同时欢迎关注公众号:LanTech指南。不定时分享职场思考、独立开发日志、大厂方法论和后端经验❤️