ElasticSearch文件建模最佳实践

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"
        }
        
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值