ElasticSearch进阶-数据建模

10 篇文章 1 订阅
9 篇文章 2 订阅

一、nested object

1、案例:设计一个用户document数据类型,其中包含一个地址数据的数组

地址中的字段可能包含住址、公司地址等多个地址。

PUT /user_index
{
  "mappings": {
    "properties": {
      "login_name": {
        "type": "keyword"
      },
      "age": {
        "type": "short"
      },
      "address": {
        # 相当于地址中含有三个子属性:province、city、street
        "properties": {
          "province": {
            "type": "keyword"
          },
          "city": {
            "type": "keyword"
          },
          "street": {
            "type": "keyword"
          }
        }
      }
    }
  }
}

但是上述的数据建模有其明显的缺陷,就是针对地址数据做数据搜索的时候,经常会搜索出不必要的数据,如:在下述数据环境中,搜索一个province为北京,city为天津的用户。

准备数据:

PUT /user_index/_doc/1
{
  "login_name": "jack",
  "age": 25,
  "address": [
    {
      "province": "北京",
      "city": "北京",
      "street": "枫林三路"
    },
    {
      "province": "天津",
      "city": "天津",
      "street": "华夏路"
    }
  ]
}
PUT /user_index/_doc/2
{
  "login_name": "rose",
  "age": 21,
  "address": [
    {
      "province": "河北",
      "city": "廊坊",
      "street": "燕郊经济开发区"
    },
    {
      "province": "天津",
      "city": "天津",
      "street": "华夏路"
    }
  ]
}

我们使用语句进行查询:

GET /user_index/_search
{
  "query": {
    "bool": {
      "must": [
        {
          "match": {
            "address.province": "北京"
          }
        },
        {
          "match": {
            "address.city": "天津"
          }
        }
      ]
    }
  }
}

查询结果:

{
        "_index" : "user_index",
        "_type" : "_doc",
        "_id" : "1",
        "_score" : 0.7587298,
        "_source" : {
          "login_name" : "jack",
          "age" : 25,
          "address" : [
            {
              "province" : "北京",
              "city" : "北京",
              "street" : "枫林三路"
            },
            {
              "province" : "天津",
              "city" : "天津",
              "street" : "华夏路"
            }
          ]
        }
}

我们搜省份北京,城市天津,我们想看到的结果其实是不想让它搜出来的,因为不是靠单个地址匹配上的,通过数据可以看出这里只是匹配到了一个doc中的地址组中的两个地址,所以依然给我搜出来了,不符合我们的预期,我们是想实现通过单个地址匹配上才会返回doc的效果。这里这样举例可能不大合适,类似反向推导,但是表达出来了我们的意思。

这个时候就可以在声明映射信息的时候,使用内聚的方式声明address这个字段。

2.nested object

使用nested object作为地址数组的集体类型,可以解决上述问题,document模型如下:

PUT /user_index
{
  "mappings": {
    "properties": {
      "login_name": {
        "type": "keyword"
      },
      "age": {
        "type": "short"
      },
      "address": {
        # 指定nested类型
        "type": "nested",
        "properties": {
          "province": {
            "type": "keyword"
          },
          "city": {
            "type": "keyword"
          },
          "street": {
            "type": "keyword"
          }
        }
      }
    }
  }
}

这个时候就需要使用nested对应的搜索语法来执行搜索了,如果还是用原来的方式查询的话,就算你查询的城市和省份都是北京也查不出来。语法如下:

GET /user_index/_search
{
  "query": {
    "bool": {
      "must": [
        {
          "nested": {
            "path": "address",
            "query": {
              "bool": {
                "must": [
                  {
                    "match": {
                      "address.province": "北京"
                    }
                  },
                  {
                    "match": {
                      "address.city": "天津"
                    }
                  }
                ]
              }
            }
          }
        }
      ]
    }
  }
}

这个时候返回的搜索结果就是空了,也就是没有符合条件的doc,这种效果符合我们的预期。

分析原因:

普通的数组数据在ES中会被扁平化处理,处理方式如下:(如果字段需要分词,会将分词数据保存在对应的字段位置,当然应该是一个倒排索引,这里只是一个做一个直观的展示)

{
  "login_name" : "jack",
  "address.province" : [ "北京", "天津" ],
  "address.city" : [ "北京", "天津" ]
  "address.street" : [ "枫林三路", "华夏路" ]
}

那么nested object数据类型ES在保存的时候不会有扁平化处理,保存方式如下:所以在搜索的时候一定会有需要的搜索结果。

{
  "login_name" : "jack"
}
{
  "address.province" : "北京",
  "address.city" : "北京",
  "address.street" : "枫林三路"
}
{
  "address.province" : "天津",
  "address.city" : "天津",
  "address.street" : "华夏路",
}

二、父子关系数据建模

1.nested object缺点

nested object的建模,有个不好的地方,就是采取的是类似冗余数据的方式,将多个数据都放在一起了,维护成本就比较高。每次更新,需要重新索引整个对象(包括跟对象和嵌套对象)。

总结nested object缺点:

  • 数据冗余
  • 每次用户信息数据变更,因为会重新索引整个对象,包括嵌套的对象,即用户信息会重新索引,地址信息也会重新索引,是很麻烦的,实际开发中不是很好用,不推荐。

2.父子关系数据建模分析

如果一个用户有多个地址信息,按照mysql的思路分析,这是一对多的关系,那么把用户信息和地址信息放在一个表里就不是很合理了,那就是用户一张表,地址单独一张表。那么拿到es中来,ES 提供了类似关系型数据库中 Join 的实现。使用 Join 数据类型实现,可以通过 Parent / Child 的关系,从而分离两个对象。

父文档和子文档是两个独立的文档,但还是在同一个索引库中;更新父文档无需重新索引整个子文档。子文档被新增,更改和删除也不会影响到父文档和其他子文档。

要点:父子关系元数据映射,用于确保查询时候的高性能,但是有一个限制,就是父子数据必须存在于一个shard中。

父子关系数据存在一个shard中,而且还有映射其关联关系的元数据,那么搜索父子关系数据的时候,不用跨分片,一个分片本地自己就搞定了,性能当然高

3.如果保证父子关系文档在同一个分片中

在插入子文档时的url后面拼上"?routing=父文档id"

4.父子关系文档定义

1)定义父子关系步骤

  • 设置索引的 Mapping
  • 索引父文档
  • 索引子文档
  • 按需查询文档

2)设置索引mapping

# 设定 Parent/Child Mapping
PUT my_blogs
{
  "mappings": {
    "properties": {
      "blog_comments_relation": {
        "type": "join",
        "relations": {
          "blog": "comment"
        }
      },
      "content": {
        "type": "text"
      },
      "title": {
        "type": "keyword"
      }
    }
  }
}

3)索引父文档

PUT my_blogs/_doc/blog1
{
  "title": "Learning Elasticsearch",
  "content": "learning ELK is happy",
  # 声明文档的关系类型,这里的blog不是随便写的,是上面映射中声明好的
  "blog_comments_relation": {
    "name": "blog"
  }
}

PUT my_blogs/_doc/blog2
{
  "title": "Learning Hadoop",
  "content": "learning Hadoop",
  "blog_comments_relation": {
    "name": "blog"
  }
}

4)索引子文档

  • 父文档和子文档必须存在相同的分片上
    • 确保查询 join 的性能
  • 当指定文档时候,必须指定它的父文档 ID
    • 使用 route 参数来保证,分配到相同的分片
# routing用来保证父子文档在同一个分片中
PUT my_blogs/_doc/comment2?routing=blog2
{
  "comment": "I like Hadoop!!!!!",
  "username": "Jack",
  # 声明文档关系类型
  "blog_comments_relation": {
    # 这里的name中的comment也不是随便指定的,是上面映射中声明好的
    "name": "comment",
    # 父文档id
    "parent": "blog2"
  }
}

PUT my_blogs/_doc/comment3?routing=blog2
{
  "comment": "Hello Hadoop",
  "username": "Bob",
  "blog_comments_relation": {
    "name": "comment",
    "parent": "blog2"
  }
}

5.父子文档查询

Parent / Child 所支持的查询

  • 查询所有文档
  • Parent Id 查询
  • Has Child 查询
  • Has Parent 查询

1)查询所有文档、根据父文档id查询

# 查询所有文档
POST my_blogs/_search


#根据父文档ID查看,和普通的根据id查询没有区别
GET my_blogs/_doc/blog2

2)Has Child查询

  • 返回父文档
  • 通过对子文档进行查询
    • 返回具体相关子文档的父文档
    • 父子文档在相同的分片上,因此 Join 效率高
# Has Child 查询,返回父文档
# 查询哪个父文档中包含评论,什么评论呢?username包含Jack的评论
POST my_blogs/_search
{
  "query": {
    # 查询是否有子文档,什么的子文档呢
    "has_child": {
      # 类型是comment
      "type": "comment",
      # 评论的用户包含Jack的子文档
      "query": {
        "match": {
          "username": "Jack"
        }
      }
    }
  }
}

3)Has Parent查询

  • 返回相关性的子文档
  • 通过对父文档进行查询
    • 返回相关的子文档
# Has Parent 查询,返回相关的子文档
# 查询父文档类型是blog的,并且父文档的title包含"Learning Hadoop"的评论
POST my_blogs/_search
{
  "query": {
    "has_parent": {
      # Parent Relation Name
      "parent_type": "blog",
      # 标题是含有对应内容的父文档
      "query": {
        "match": {
          "title": "Learning Hadoop"
        }
      }
    }
  }
}

4)使用 parent_id 查询

  • 返回所有相关子文档
  • 通过对父文档 Id 进行查询
    • 返回所有相关的子文档
# Parent Id 查询
POST my_blogs/_search
{
  "query": {
    # Parent Id查询关键字
    "parent_id": {
      # Child Relation Name
      "type": "comment",
      # 父文档id
      "id": "blog2"
    }
  }
}

5)访问子文档

# 方式一:就是普通的根据id获取doc信息
GET my_blogs/_doc/comment3

# 方式二:多了个routing
GET my_blogs/_doc/comment2?routing=blog2

6)更新子文档

  • 更新子文档不会影响到父文档
# 字段内容和关系信息(类型(comment、blog)、归属的父文档id)都可以改
POST my_blogs/_doc/comment3?routing=blog2
{
  "comment": "Hello Hadoop?",
  "username": "Bob",
  "blog_comments_relation": {
    "name": "comment",
    "parent": "blog1"
  }
}

6.嵌套对象(Nested Object)和父子文档(Parent/Child)对比

Nested Object

优点:文档存储在一起,读取性能高

缺点:更新嵌套的子文档时,需要更新整个文档

适用场景:子文档偶尔更新,以查询为主

Parent / Child

优点:父子文档可以独立更新

缺点:需要额外的内存去维护关系,读取性能相对差

适用场景子:子文档更新频繁

三、文件系统数据建模

思考一下,github中可以使用代码片段来实现数据搜索。这是如何实现的?

在github中也使用了ES来实现数据的全文搜索。其ES中有一个记录代码内容的索引,大致数据内容如下:

{
  "fileName" : "HelloWorld.java",
  "authName" : "zzh",
  "authID" : 110,
  "productName" : "first-java",
  "path" : "/com/zzh/first",
  "content" : "package com.zzh.first; public class HelloWorld { //code... }"
}

我们可以在github中通过代码的片段来实现数据的搜索。也可以使用其他条件实现数据搜索。但是,如果需要使用文件路径搜索内容应该如何实现?这个时候需要为其中的字段path定义一个特殊的分词器。具体如下:

PUT /codes
{
  "settings": {
    "analysis": {
      "analyzer": {
        # 自定义分词器,分词器名称设置为path_analyzer
        "path_analyzer": {
          # es自带的,专门针对路径的分词策略
          "tokenizer": "path_hierarchy"
        }
      }
    }
  },
  "mappings": {
    "properties": {
      "fileName": {
        "type": "keyword"
      },
      "authName": {
        "type": "text",
        "analyzer": "standard",
        # 相当于子字段,子字段名称设置为keyword
        "fields": {
          "keyword": {
            "type": "keyword"
          }
        }
      },
      "authID": {
        "type": "long"
      },
      "productName": {
        "type": "text",
        "analyzer": "standard",
        "fields": {
          "keyword": {
            "type": "keyword"
          }
        }
      },
      "path": {
        "type": "text",
        # 路径字段设置使用之前声明好的自定义分词器path_analyzer
        "analyzer": "path_analyzer",
        "fields": {
          "keyword": {
            "type": "keyword"
          }
        }
      },
      "content": {
        "type": "text",
        "analyzer": "standard"
      }
    }
  }
}

我们通过分词分析的api验证一下:

GET /codes/_analyze
{
  "text": "/a/b/c/d",
  "field": "path"
}

结果:

{
  "tokens" : [
    {
      "token" : "/a",
      "start_offset" : 0,
      "end_offset" : 2,
      "type" : "word",
      "position" : 0
    },
    {
      "token" : "/a/b",
      "start_offset" : 0,
      "end_offset" : 4,
      "type" : "word",
      "position" : 0
    },
    {
      "token" : "/a/b/c",
      "start_offset" : 0,
      "end_offset" : 6,
      "type" : "word",
      "position" : 0
    },
    {
      "token" : "/a/b/c/d",
      "start_offset" : 0,
      "end_offset" : 8,
      "type" : "word",
      "position" : 0
    }
  ]
}

可以发现,通过path_hierarchy这种策略可以实现路径拆分,但是只会分成/a、/a/b、/a/b/c、/a/b/c/d,会发现没有/b/c、/b、/c这种,那是因么这个分词策略就是这样的,如果说我们就想实现用户输入/b、/c、/b/c也可以查到,因为我们不记得前面的路径是什么了,直接查询某个中间路径,也可以实现,那我们需要修改一下策略:同样给path定义一个子字段,但是类型是text,分词器使用standard。如下:

PUT /codes
{
  "settings": {
    "analysis": {
      "analyzer": {
        "path_analyzer": {
          "tokenizer": "path_hierarchy"
        }
      }
    }
  },
  "mappings": {
    "properties": {
      "fileName": {
        "type": "keyword"
      },
      "authName": {
        "type": "text",
        "analyzer": "standard",
        "fields": {
          "keyword": {
            "type": "keyword"
          }
        }
      },
      "authID": {
        "type": "long"
      },
      "productName": {
        "type": "text",
        "analyzer": "standard",
        "fields": {
          "keyword": {
            "type": "keyword"
          }
        }
      },
      "path": {
        "type": "text",
        "analyzer": "path_analyzer",
        "fields": {
          # 给path定义一个子字段,名称为keyword,类型为text
          "keyword": {
            "type": "text",
            "analyzer": "standard"
          }
        }
      },
      "content": {
        "type": "text",
        "analyzer": "standard"
      }
    }
  }
}

我们通过分词分析的api验证一下:

GET /codes/_analyze
{
  "text": "/a/b/c/d",
  "field": "path.keyword"
}

结果:

{
  "tokens" : [
    {
      "token" : "a",
      "start_offset" : 1,
      "end_offset" : 2,
      "type" : "<ALPHANUM>",
      "position" : 0
    },
    {
      "token" : "b",
      "start_offset" : 3,
      "end_offset" : 4,
      "type" : "<ALPHANUM>",
      "position" : 1
    },
    {
      "token" : "c",
      "start_offset" : 5,
      "end_offset" : 6,
      "type" : "<ALPHANUM>",
      "position" : 2
    },
    {
      "token" : "d",
      "start_offset" : 7,
      "end_offset" : 8,
      "type" : "<ALPHANUM>",
      "position" : 3
    }
  ]
}

因为keyword这个子字段使用了standard,所以可以看到把每个路径都拆开了,这样我们搜索/b/c、/b/d等都可以搜到了。如果path数据结构的字段使用了如上定义,我们做查询的时候可以这样写:使用组合查询,这样就能保证用户输入什么路径组合都可以被搜索到。

GET /codes/_search
{
  "query": {
    "bool": {
      "should": [
        {
          "match": {
            "path": "/zzh/first"
          }
        },
        {
          "match": {
            "path.keyword": "/zzh/first"
          }
        }
      ]
    }
  }
}

思考:以上示例是为了演示文档建模中路径结构的数据分词过程,如果想使用path_hierarchy这种分词策略,则可以配合设置子字段的方式进行使用。个人感觉不如把路径结构的数据直接设置成类型是text,并且使用standard进行分词就可以了,就不用再进行设置子字段额外维护了。

注意:在定义字段的时候也发现了这样一个结构:

"authName": {
        "type": "text",
        "analyzer": "standard",
        # 相当于子字段,子字段名称设置为keyword
        "fields": {
          "keyword": {
            "type": "keyword"
          }
        }
      },

设置一个类似于子字段的作用是做聚合使用的,我想根据作者的名字坐聚合分析,但是类型是text,因为text类型是不能做聚合分析的,如果我们就想根据text类型的字段做聚合分析,ES5.0之后,如果如上指定,那么分词器也会把authName完整的保存一份到倒排索引中,检索的时候就得authName.keyword(相当于authName的子字段)这样使用,如果使用这种格式作为查询条件时,ES不会将查询的内容做分词查询,只会看作一个整体去倒排索引中查询。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
1、Elasticsearch 7.X 进阶实战大纲10个大选题来源于实战业务场景的提炼、总结。Elasticsearch 全貌认知Elasticsearch 索引创建和搜索原理Elasticsearch 集群规划及节点角色规划最佳实践Elasticsearch 集群性能调优及原理Elasticsearch 数据建模实例讲解与实战技巧Elasticsearch 冷温热架构讲解与实战Elasticsearch ILM 索引生命周期管理讲解与实战Elasticsearch CCS 跨集群搜索讲解与实战Elasticsearch 分片分配策略讲解与实战Elasticsearch 安全特性讲解与使用2、课程特色来源于项目实战、应用于实战项目。实战项目经验总结,属于基础后的进阶系列。基于 7.13 版本讲解,市面上教程都没有完整体现过该版本。近 10 个小时的视频,力求相对完整、体系、通透。 3、讲解方式干货凝练总结、侧重原理、深入浅出。每一讲脚本都可以提供下载,跟着学、学的会。视频共11大讲,每讲可以独立成课,方便大家地铁、公交车学习。录播方式,非直播。4、讲师:铭毅天下介绍Elastic认证工程师、Elastic中国合作培训讲师(有世界500强企业用户企业内训经验);阿里云MVP;死磕Elasticsearch知识星球发起人,全球付费用户1200人+(含中国台湾、美国硅谷、加拿大球友),已带领 60人+ 通过Elasticsearch 认证考试(中国仅通过100人左右);可能是中国最大Elastic技术公众号——铭毅天下Elasticsearch作者。CSDN博客专家、CSDN2020年度优秀创作者、CSDN2016年、2013年博客征文大赛特等奖得主;CSDN博客地址:elastic.blog.csdn.net;CSDN博客排名:近前150,阅读量近500,0000+;CSDN持续写作近十年(几乎每月都有输出,几乎从未间断),累计使用 Elasticsearch 超过 10000小时;Elastic中文社区日报责任编辑,2018年Elastic中文社区杰出贡献者、社区排名 TOP5;高级工程师、计算机应用技术硕士、近十年工作经验;理想主义者、终身学习者、终身成长者;笃信坚持、积累的力量;自1997年——至今20年+持续思考、积累、总结,从未间断;个人信条:自由不是你想干什么就干什么,而是你不想干什么就有能力不干什么!人因为梦想而伟大,机遇永远属于那些有准备、立即行动并能坚持到底的人!

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值