Elasticsearch中如何处理关联关系
Elasticsearch多表关联的问题是讨论最多的问题之一。多表关联通常指一对多或者多对多的数据关
系,如博客及其评论的关系。
Elasticsearch并不擅长处理关联关系,一般会采用以下四种方法处理关联:
嵌套对象(Nested Object)
Nested类型适用于一对少量、子文档偶尔更新、查询频繁的场景。如果需要索引对象数组并保持
数组中每个对象的独立性,则应使用Nested数据类型而不是Object数据类型。
Nested类型的优点是Nested文档可以将父子关系的两部分数据关联起来(例如博客与评论),可以基于Nested类型做任何查询。其缺点则是查询相对较慢,更新子文档时需要更新整篇文档。
Join父子文档类型
Join类型用于在同一索引的文档中创建父子关系。Join类型适用于子文档数据量明显多于父文档的
数据量的场景,该场景存在一对多量的关系,子文档更新频繁。举例来说,一个产品和供应商之间就是一对多的关联关系。当使用父子文档时,使用has_child或者has_parent做父子关联查询。
Join类型的优点是父子文档可独立更新。缺点则是维护Join关系需要占据部分内存,查询较Nested类型更耗资源。
宽表冗余存储
宽表适用于一对多或者多对多的关联关系。
宽表的优点是速度快。缺点则是索引更新或删除数据时,应用程序不得不处理宽表的冗余数据;并
且由于冗余存储,某些搜索和聚合操作的结果可能不准确。
业务端关联
这是普遍使用的技术,即在应用接口层面处理关联关系。一般建议在存储层面使用两个独立索引存
储,在实际业务层面这将分为两次请求来完成。
业务端关联适用于数据量少的多表关联业务场景。数据量少时,用户体验好;而数据量多时,两次
查询耗时肯定会比较长,反而影响用户体验。
博客作者信息变更
PUT /blog
{
"settings":{
"number_of_shards": 1,
"number_of_replicas": 1
},
"mappings":{
"properties": {
"content":{
"type": "text"
},
"time":{
"type": "date"
},
"user":{
"properties": {
"city":{
"type": "text"
},
"userid":{
"type": "long"
},
"username":{
"type": "keyword"
}
}
}
}
}
}
插入一条 blog信息
PUT /blog/_doc/1
{
"content":"I like ElasticSearch",
"time":"2022-01-01T00:00:00",
"user":{
"userid":1,
"username":"Fox",
"city":"Changsha"
}
}
查询 blog信息
GET /blog/_search
{
"query": {
"bool": {
"must": [
{"match":{"content":"ElasticSearch"}},
{"match":{"user.username":"Fox"}}
]
}
}
}
包含对象数组的文档
PUT /my_movies
{
"settings":{
"number_of_shards": 1,
"number_of_replicas": 1
},
"mappings":{
"properties": {
"actors": {
"properties": {
"first_name": {
"type": "keyword"
},
"last_name": {
"type": "keyword"
}
}
},
"title": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
}
}
}
}
}
写入一条电影信息
POST /my_movies/_doc/1
{
"title":"Speed",
"actors":[
{
"first_name":"Keanu",
"last_name":"Reeves"
},
{
"first_name":"Dennis",
"last_name":"Hopper"
}
]
}
查询电影信息
POST /my_movies/_search
{
"query": {
"bool": {
"must": [
{"match":{"actors.first_name":"Keanu"}},
{"match":{"actors.last_name":"Hopper"}}
]
}
}
}
思考:为什么会搜到不需要的结果?
存储时,内部对象的边界并没有考虑在内,JSON格式被处理成扁平式键值对的结构。当对多个字段进行查询时,导致了意外的搜索结果。可以用Nested Data Type解决这个问题。
嵌套对象(Nested Object)
什么是Nested Data Type
Nested数据类型: 允许对象数组中的对象被独立索引
使用nested 和properties 关键字,将所有actors索引到多个分隔的文档
在内部, Nested文档会被保存在两个Lucene文档中,在查询时做Join处理
DELETE /my_movies
PUT /my_movies
{
"settings":{
"number_of_shards": 1,
"number_of_replicas": 1
},
"mappings":{
"properties": {
"actors": {
"type": "nested",
"properties": {
"first_name": {
"type": "keyword"
},
"last_name": {
"type": "keyword"
}
}
},
"title": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
}
}
}
}
}
POST /my_movies/_doc/1
{
"title":"Speed",
"actors":[
{
"first_name":"Keanu",
"last_name":"Reeves"
},
{
"first_name":"Dennis",
"last_name":"Hopper"
}
]
}
Nested 查询
POST /my_movies/_search
{
"query": {
"bool": {
"must": [
{
"match": {
"title": "Speed"
}
},
{
"nested": {
"path": "actors",
"query": {
"bool": {
"must": [
{
"match": {
"actors.first_name": "Keanu"
}
},
{
"match": {
"actors.last_name": "Reeves"
}
}
]
}
}
}
}
]
}
}
}
Join父子关联类型
对象和Nested对象的局限性: 每次更新,可能需要重新索引整个对象(包括根对象和嵌套对象)
ES提供了类似关系型数据库中Join 的实现。使用Join数据类型实现,可以通过维护Parent/ Child的关系,从而分离两个对象
父文档和子文档是两个独立的文档更新父文档无需重新索引子文档。子文档被添加,更新或者删除也不会影响到父文档和其他的子文档
设定 Parent/Child Mapping
PUT /csdn_blog
{
"settings":{
"number_of_shards": 2,
"number_of_replicas": 1
},
"mappings":{
"properties": {
"blog_comments_relation":{
"type": "join",
"relations":{
"blog":"comment"
}
},
"content":{
"type": "text"
},
"title":{
"type": "keyword"
},
"user":{
"properties": {
"city":{
"type": "text"
},
"userid":{
"type": "long"
},
"username":{
"type": "keyword"
}
}
}
}
}
}
索引父文档
PUT /csdn_blog/_doc/blog1
{
"title":"Learning Elasticsearch",
"content":"learning ELK",
"blog_comments_relation":{
"name":"blog"
},
"time":"2022-01-01T00:00:00",
"user":{
"userid":1,
"username":"Fox",
"city":"Changsha"
}
}
PUT /csdn_blog/_doc/blog2
{
"title":"Learning Hadoop",
"content":"learning Hadoop",
"blog_comments_relation":{
"name":"blog"
},
"time":"2022-01-01T00:00:00",
"user":{
"userid":1,
"username":"Fox",
"city":"Changsha"
}
}
索引子文档
PUT /csdn_blog/_doc/comment1?routing=blog1
{
"comment":"I am Learning ELK",
"username":"Jack",
"blog_comments_relation":{
"name":"comment",
"parent":"blog1"
}
}
PUT /csdn_blog/_doc/comment2?routing=blog2
{
"comment":"I like Hadoop",
"username":"Bob",
"blog_comments_relation":{
"name":"comment",
"parent":"blog2"
}
}
PUT /csdn_blog/_doc/comment3?routing=blog2
{
"comment":"I like Hadoop!!!!!!!!",
"username":"Bob",
"blog_comments_relation":{
"name":"comment",
"parent":"blog2"
}
}
注意:
父文档和子文档必须存在相同的分片上,能够确保查询join 的性能
当指定子文档时候,必须指定它的父文档ld。使用routing参数来保证,分配到相同的分片
查询
查询所有文档
POST /csdn_blog/_search
根据父文档ID查看
GET /csdn_blog/_doc/blog2
Parent Id 查询
POST /csdn_blog/_search
{
"query":{
"parent_id":{
"type":"comment",
"id":"blog2"
}
}
}
Has Child 查询,返回父文档
POST /csdn_blog/_search
{
"query": {
"has_child": {
"type": "comment",
"query": {
"match": {
"username": "Jack"
}
}
}
}
}
Has Parent 查询,返回相关的子文档
POST /csdn_blog/_search
{
"query": {
"has_parent": {
"parent_type": "blog",
"query": {
"match": {
"title": "Learning Hadoop"
}
}
}
}
}
多表关联方案对比
在Elasticsearch开发实战中,对于多表关联的设计要突破关系型数据库设计的思维定式。不建议在
Elasticsearch中做多表关联操作,尽量在设计时使用扁平的宽表文档模型,或者尽量将业务转化为没有关联关系的文档形式,在文档建模处多下功夫,以提升检索效率。

ElasticSearch文档建模的最佳实践
如何处理关联关系
Object: 优先考虑反范式(Denormalization)
Nested: 当数据包含多数值对象,同时有查询需求
Child/Parent:关联文档更新非常频繁时
避免过多字段
一个文档中,最好避免大量的字段
过多的字段数不容易维护
Mapping 信息保存在Cluster State 中,数据量过大,对集群性能会有影响
删除或者修改数据需要reindex
默认最大字段数是1000,可以设置index.mapping.total_fields.limit限定最大字段数。·
思考:什么原因会导致文档中有成百上千的字段?
生产环境中,尽量不要打开 Dynamic,可以使用Strict控制新增字段的加入
true :未知字段会被自动加入,默认值
false :新字段不会被索引,但是会保存在_source
strict :新增字段不会被索引,文档写入失败
PUT /user/_doc/1
{
"name":"fox",
"address":{
"province":"hunan",
"city":"changsha"
}
}
PUT /user/_doc/2
{
"name":"fox",
"sex":1,
"address":{
"province":"hunan",
"city":"changsha"
}
}
PUT /user
{
"mappings": {
"dynamic":"strict",
"properties": {
"name":{
"type": "text"
},
"address":{
"type":"object",
"dynamic":"true"
}
}
}
}
插入文档报错,原因为age为新增字段,会抛出异常
对于多属性的字段,比如cookie,商品属性,可以考虑使用Nested
避免正则,通配符,前缀查询
正则,通配符查询,前缀查询属于Term查询,但是性能不够好。特别是将通配符放在开头,会导致性能的灾难
案例:针对版本号的搜索
PUT /softwares
{
"mappings": {
"properties": {
"version":{
"properties": {
"display_name":{
"type": "keyword"
},
"hot_fix":{
"type":"byte"
},
"marjor":{
"type":"byte"
},
"minor":{
"type":"byte"
}
}
}
}
}
}
PUT /softwares/_doc/1
{
"version":{
"display_name":"7.1.0",
"major":7,
"minor":1,
"hot_fix":0
}
}
PUT /softwares/_doc/2
{
"version":{
"display_name":"7.2.0",
"major":7,
"minor":2,
"hot_fix":0
}
}
PUT /softwares/_doc/3
{
"version":{
"display_name":"7.2.1",
"major":7,
"minor":2,
"hot_fix":1
}
}
通过 bool 查询
避免空值引起的聚合不准
Not Null 解决聚合的问题
PUT /score
{
"mappings": {
"properties": {
"score":{
"type":"float",
"null_value": "0"
}
}
}
}
PUT /score/_doc/1
{
"score":100
}
PUT /score/_doc/2
{
"score":null
}
GET /score/_search
GET /score/_search
{
"size": 0,
"aggs": {
"avg": {
"avg": {
"field": "score"
}
}
}
}
为索引的Mapping加入Meta 信息
Mappings设置非常重要,需要从两个维度进行考虑
功能︰搜索,聚合,排序
性能︰存储的开销; 内存的开销; 搜索的性能
Mappings设置是一个迭代的过程
加入新的字段很容易(必要时需要update_by_query)
更新删除字段不允许(需要Reindex重建数据)
最好能对Mappings 加入Meta 信息,更好的进行版本管理
可以考虑将Mapping文件上传git进行管理
PUT /score
{
"mappings": {
"_meta":{
"index_version_mapping":"1.1"
}
}
}
5670

被折叠的 条评论
为什么被折叠?



