数据建模
嵌套类型:Nested
nested属于object类型的一种,是Elasticsearch中用于复杂类型对象数组的索引操作。Elasticsearch没有内部对象的概念,因此,ES在存储复杂类型的时候会把对象的复杂层次结果扁平化为一个键值对列表。
比如:
PUT my-index-000001/_doc/1 { "group" : "fans", "user" : [ { "first" : "John", "last" : "Smith" }, { "first" : "Alice", "last" : "White" } ] }
上面的文档被创建之后,user数组中的每个json对象会以下面的形式存储
{ "group" : "fans", "user.first" : [ "alice", "john" ], "user.last" : [ "smith", "white" ] }
user.first
和user.last
字段被扁平化为多值字段,first
和last
之间的关联丢失。
使用nested为复杂类型创建mapping:
PUT <index_name> { "mappings": { "properties": { "<nested_field_name>": { "type": "nested" } } } }
查询:
GET <index_name>/_search { "query": { "nested": { "path": "<nested_field_name>", "query": { ... } } } }
Optins:
- path:nested对象的查询深度
- score_mode:评分计算方式
- avg (默认):使用所有匹配的子对象的平均相关性得分。
- max:使用所有匹配的子对象中的最高相关性得分。
- min:使用所有匹配的子对象中最低的相关性得分。
- none:不要使用匹配的子对象的相关性分数。该查询为父文档分配得分为0。
- sum:将所有匹配的子对象的相关性得分相加。
父子级关系:Join
连接数据类型是一个特殊字段,它在同一索引的文档中创建父/子关系。关系部分在文档中定义了一组可能的关系,每个关系是一个父名和一个子名。父/子关系可以定义如下
PUT <index_name> { "mappings": { "properties": { "<join_field_name>": { "type": "join", "relations": { "<parent_name>": "<child_name>" } } } } }
使用场景
join
类型不能像关系数据库中的表链接那样去用,不论是has_child
或者是has_parent
查询都会对索引的查询性能有严重的负面影响。并且会触发global ordinals
join
唯一合适应用场景是:当索引数据包含一对多的关系,并且其中一个实体的数量远远超过另一个的时候。比如:老师
有一万个学生
注意
- 在索引父子级关系数据的时候必须传入routing参数,即指定把数据存入哪个分片,因为父文档和子文档必须在同一个分片上,因此,在获取、删除或更新子文档时需要提供相同的路由值。
- 每个索引只允许有一个
join
类型的字段映射 - 一个元素可以有多个子元素但只有一个父元素
- 可以向现有连接字段添加新关系
- 也可以向现有元素添加子元素,但前提是该元素已经是父元素
数据建模
概念
数据模型是描述现实世界某种现象或者状态的物理抽象,比如我们之前用FSA
来描述周老师的一天
这种现象,就是把现实世界抽象成某种模型。现实世界有很多重要的关联关系:博客帖子有一些评论,银行账户有多次交易记录,客户有多个银行账户,订单有多个订单明细,文件目录有多个文件和子目录。
关系型数据库关联关系:
- 每个实体(或 行 ,在关系世界中)可以被
主键
唯一标识。 - 实体
规范化
(范式)。唯一实体的数据只存储一次,而相关实体只存储它的主键。只能在一个具体位置修改这个实体的数据。 - 实体可以进行关联查询,可以跨实体搜索。
- 单个实体的变化是
原子性
,一致性
,隔离性
, 和持久性
。 (可以在 ACID Transactions 中查看更多细节。) - 大多数关系数据库支持跨多个实体的 ACID 事务。
但是关系型数据库有其局限性,包括对全文检索有限的支持能力。 实体关联查询时间消耗是很昂贵的,关联的越多,消耗就越昂贵。特别是跨服务器进行实体关联时成本极其昂贵,基本不可用。 但单个的服务器上又存在数据量的限制。
Elasticsearch ,和大多数 NoSQL 数据库类似,是扁平化的。索引是独立文档的集合体。 文档是否匹配搜索请求取决于它是否包含所有的所需信息。
Elasticsearch 中单个文档的数据变更是 ACIDic 的, 而涉及多个文档的事务则不是。当一个事务部分失败时,无法回滚索引数据到前一个状态。
扁平化有以下优势:
- 索引过程是快速和无锁的。
- 搜索过程是快速和无锁的。
- 因为每个文档相互都是独立的,大规模数据可以在多个节点上进行分布。
但关联关系仍然非常重要。某些时候,我们需要缩小扁平化和现实世界关系模型的差异。以下四种常用的方法,用来在 Elasticsearch 中进行关系型数据的管理:
对象和实体
对象和实体的关系就是现实世界和数据模型的映射,我们在做Java开发的时候经常用到的POJO的领域模型就是这种关系:
分层领域模型规约:
- DO( Data Object):与数据库表结构一一对应,通过DAO层向上传输数据源对象。
- DTO( Data Transfer Object):数据传输对象,Service或Manager向外传输的对象。
- BO( Business Object):业务对象。 由Service层输出的封装业务逻辑的对象。
- AO( Application Object):应用对象。 在Web层与Service层之间抽象的复用对象模型,极为贴近展示层,复用度不高。
- VO( View Object):显示层对象,通常是Web向模板渲染引擎层传输的对象。
- POJO( Plain Ordinary Java Object):在本手册中, POJO专指只有setter/getter/toString的简单类,包括DO/DTO/BO/VO等。
- Query:数据查询对象,各层接收上层的查询请求。 注意超过2个参数的查询封装,禁止使用Map类来传输。
领域模型命名规约:
- 数据对象:xxxDO,xxx即为数据表名。
- 数据传输对象:xxxDTO,xxx为业务领域相关的名称。
- 展示对象:xxxVO,xxx一般为网页名称。
- POJO是DO/DTO/BO/VO的统称,禁止命名成xxxPOJO。
数据建模的过程
- 概念:需求 => 抽象。即把实际的用户需求抽象为某种数据模型,比如我们在存储
倒排表
的时候,就是把”储存倒排表“这个需求抽象成FST
的这种抽象的数据模型。 - 逻辑:抽象 => 具体。仍然以”存储倒排表“为例,FST模型构建完成之后,我们需要把其抽象变成具体的代码和对象,把实现变为肉眼可见的东西。
- 物理:具体 => 落地。同上,当我们有了逻辑之后,就可以通过具体的对象、属性编程实实在在的数据文件,保存在你的磁盘里。
意义
我个人总结如下,但其实不仅限于以下几点:
- 开发:简化开发流程,从而提高效率
- 产品:提升数据的存储效率,提升查询性能
- 管理:前期准备充分,降低后期出现问题的可能性
- 成本:综合各个因素,降低整体的运营和管理成本
数据建模的包含的内容
-
关联关系处理(index relations):
-
数据模型的关联:我们通过在我们的应用程序中实现联接可以(部分)模拟关系数据库。应用层联接的主要优点是可以对数据进行标准化处理。只能在
user
文档中修改用户的名称。缺点是,为了在搜索时联接文档,必须运行额外的查询
-
-
- 非规范化数据:使用 Elasticsearch 得到最好的搜索性能的方法是有目的的通过在索引时进行非规范化 denormalizing。对每个文档保持一定数量的冗余副本可以在需要访问时避免进行关联
- 稀疏字段:避免稀疏字段文档
- 并发问题:全局锁、文档锁、树锁(独占锁、共享锁)、乐观锁、悲观锁
-
Object类型:通俗点就是通过字段冗余,以一张大宽表来实现粗粒度的index,这样可以充分发挥扁平化的优势。但是这是以牺牲索引性能及灵活度为代价的。使用的前提:冗余的字段应该是很少改变的;比较适合与一对少量关系的处理。当业务数据库并非采用非规范化设计时,这时要将数据同步到作为二级索引库的ES中,就很难使用上述增量同步方案,必须进行定制化开发,基于特定业务进行应用开发来处理join关联和实体拼接
-
嵌套对象:索引性能和查询性能二者不可兼得,必须进行取舍。嵌套文档将实体关系嵌套组合在单文档内部(类似与json的一对多层级结构),这种方式牺牲索引性能(文档内任一属性变化都需要重新索引该文档)来换取查询性能,可以同时返回关系实体,比较适合于一对少量的关系处理。当使用嵌套文档时,使用通用的查询方式是无法访问到的,必须使用合适的查询方式(nested query、nested filter、nested facet等),很多场景下,使用嵌套文档的复杂度在于索引阶段对关联关系的组织拼装
-
父子级关系:父子文档牺牲了一定的查询性能来换取索引性能,适用于一对多的关系处理。其通过两种type的文档来表示父子实体,父子文档的索引是独立的。父-子文档ID映射存储在 Doc Values 中。当映射完全在内存中时, Doc Values 提供对映射的快速处理能力,另一方面当映射非常大时,可以通过溢出到磁盘提供足够的扩展能力。 在查询parent-child替代方案时,发现了一种filter-terms的语法,要求某一字段里有关联实体的ID列表。基本的原理是在terms的时候,对于多项取值,如果在另外的index或者type里已知主键id的情况下,某一字段有这些值,可以直接嵌套查询。具体可参考官方文档的示例:通过用户里的粉丝关系,微博和用户的关系,来查询某个用户的粉丝发表的微博列表。
扩展性:
- 分片分配感知
- 索引模板
- 索引生命周期
- 冷热架构
- 分片管理和规划
- 滚动索引和别名
- 跨集群搜索
DELETE order PUT order { "mappings": { "properties": { "goods_list": { "properties": { "name": { "type": "text", "analyzer": "ik_max_word", "fields": { "keyword": { "type": "keyword", "ignore_above": 256 } } } } } } } } GET order/_mapping PUT /order/_doc/1 { "order_name": "小米10 Pro订单", "desc": "shouji zhong de zhandouji", "goods_count": 3, "total_price": 12699, "goods_list": [ { "name": "小米10 PRO MAX 5G", "price": 4999 }, { "name": "钢化膜", "price": 19 }, { "name": "手机壳", "price": 1999 } ] } PUT /order/_doc/2 { "order_name": "扫地机器人订单", "desc": "shouji zhong de zhandouji", "goods_count": 2, "total_price": 12699, "goods_list": [ { "name": "小米扫地机器人儿", "price": 1999 }, { "name": "洗碗机", "price": 4999 } ] } GET _analyze { "analyzer": "ik_max_word", "text": ["小米10"] } GET order/_search { "query": { "bool": { "must": [ { "match": { "goods_list.name": "小米10" } }, { "match": { "goods_list.price": "1999" } } ] } } } GET order/_search { "query": { "bool": { "must": [ { "match": { "goods_list.name": "洗碗机" } }, { "match": { "goods_list.price": "1999" } } ] } } } GET product_en/_search DELETE order PUT order { "mappings": { "properties": { "goods_list": { "type": "nested", "properties": { "name": { "type": "text", "analyzer": "ik_max_word", "fields": { "keyword": { "type": "keyword", "ignore_above": 256 } } } } } } } } PUT /order/_doc/1 { "order_name": "小米10 Pro订单", "desc": "shouji zhong de zhandouji", "goods_count": 3, "total_price": 12699, "goods_list": [ { "name": "小米10 PRO MAX 5G", "price": 4999 }, { "name": "钢化膜", "price": 19 }, { "name": "手机壳", "price": 199 } ] } PUT /order/_doc/2 { "order_name": "扫地机器人订单", "desc": "shouji zhong de zhandouji", "goods_count": 2, "total_price": 12699, "goods_list": [ { "name": "小米扫地机器热儿", "price": 1999 }, { "name": "洗碗机", "price": 4999 } ] } GET /order/_search { "query": { "nested": { "path": "goods_list", "query": { "bool": { "must": [ { "match": { "goods_list.name": "小米10" } }, { "match": { "goods_list.price": 4999 } } ] } } } } } GET /order/_search { "query": { "nested": { "path": "goods_list", "query": { "bool": { "must": [ { "match": { "goods_list.name": "洗碗机" } }, { "match": { "goods_list.price": "1999" } } ] } } } } } # score_mode GET /order/_search { "query": { "nested": { "path": "goods_list", "query": { "bool": { "must": [ { "match": { "goods_list.name": "小米10" } }, { "match": { "goods_list.price": 4999 } } ] } }, "score_mode" : "max" } } } PUT /area { "mappings": { "properties": { "province": { "type": "nested", "properties": { "name": { "type": "text", "analyzer": "ik_max_word" }, "cities": { "type": "nested", "properties": { "name": { "type": "text", "analyzer": "ik_max_word" }, "district": { "type": "nested", "properties": { "name": { "type": "text", "analyzer": "ik_max_word" } } } } } } } } } } GET area/_mapping GET area/_search PUT /area/_doc/1 { "province": { "name": "北京", "cities": [ { "name": "北京市", "district": [ {"name":"丰台区"}, {"name":"海淀区"}, {"name":"朝阳区"}, {"name":"东城区"}, {"name":"西城区"}, {"name":"昌平区"} ] } ] } } PUT /area/_doc/2 { "province": { "name": "河南省", "cities": [ { "name": "郑州市", "district": [ { "name": "金水区" }, { "name": "高新区" }, { "name": "郑东新区" }, { "name": "二七区" }, { "name": "中原区" }, { "name": "惠济区" } ] }, { "name": "鹤壁市", "district": [ { "name": "山城区" }, { "name": "淇滨区" }, { "name": "鹤山区" }, { "name": "朝歌" }, { "name": "浚县" } ] } ] } } PUT /area/_doc/3 { "province": { "name": "台湾省", "cities": [ { "name": "台北市", "district": [ { "name": "中正区" }, { "name": "大同区" }, { "name": "中山区" }, { "name": "万华区" }, { "name": "信义区" }, { "name": "松山区" } ] }, { "name": "高雄", "district": [ { "name": "小港区" }, { "name": "鼓山区" }, { "name": "三民区" } ] } ] } } #city为包含北京市 或者 包含淇滨区的 省份信息 GET /area/_search { "query": { "nested": { "path": "province", "query": { "nested": { "path": "province.cities", "query": { "bool": { "should": [ { "match": { "province.cities.name": "北京市" } }, { "nested": { "path": "province.cities.district", "query": { "bool": { "must": [ { "match": { "province.cities.district.name": "淇滨区" } } ] } } } } ] } } } } } } }
# 父子级关系 DELETE msb_depart PUT msb_depart { "mappings": { "properties": { "msb_join_field": { "type": "join", "relations": { "depart": "employee" } }, "my_id": { "type": "keyword" } } } } GET msb_depart/_mapping #部门 PUT msb_depart/_doc/1 { "my_id": 1, "name":"教学部", "msb_join_field":{ "name":"depart" } } PUT msb_depart/_doc/2 { "my_id": 2, "name":"咨询部", "msb_join_field":{ "name":"depart" } } # 老师 # 路由值是强制性的,因为父文档和子文档必须在同一个分片上建立索引 PUT msb_depart/_doc/3?routing=1&refresh { "my_id": 3, "name":"马老师", "msb_join_field":{ "name":"employee", "parent":1 } } PUT msb_depart/_doc/4?routing=1&refresh { "my_id": 4, "name":"周老师", "msb_join_field":{ "name":"employee", "parent":1 } } # 咨询 PUT msb_depart/_doc/5?routing=1&refresh { "my_id": 5, "name":"静静", "msb_join_field":{ "name":"employee", "parent":2 } } PUT msb_depart/_doc/6?routing=1&refresh { "my_id": 6, "name":"球球", "msb_join_field":{ "name":"employee", "parent":2 } } PUT msb_depart/_doc/7?routing=1&refresh { "my_id": 7, "name":"琪琪", "msb_join_field":{ "name":"employee", "parent":2 } } # 搜索所有部门 GET msb_depart/_search { "query": { "has_child": { "type": "employee", "query": { "match_all": {} } } } } # 搜索周老师所在部门 GET msb_depart/_search { "query": { "has_child": { "type": "employee", "query": { "match": { "name.keyword": "周老师" } } } } } # 搜索咨询部所有老师 GET msb_depart/_search { "query": { "has_parent": { "parent_type": "depart", "query": { "match": { "name.keyword": "咨询部" } } } } } # 搜索部门id为2的部门员工 GET msb_depart/_search { "query": { "parent_id":{ "type":"employee", "id":2 } } }
本文来自博客园,作者:孙龙-程序员,转载请注明原文链接:https://www.cnblogs.com/sunlong88/p/17399607.html