• Elasticsearch7.17 六 :ES高级功能和数据建模


    ES高级功能和数据建模

    文档关联关系处理

    关系型数据库范式化(Normalize)设计的主要目标是减少不必要的更新,往往会带来一些副作用:

    • 一个完全范式化设计的数据库会经常面临“查询缓慢”的问题。数据库越范式化,就需要Join越多的表;
    • 范式化节省了存储空间,但是存储空间已经变得越来越便宜;
    • 范式化简化了更新,但是数据读取操作可能更多。

    反范式化(Denormalize)的设计不使用关联关系,而是在文档中保存冗余的数据拷贝。

    • 优点: 无需处理Join操作,数据读取性能好。Elasticsearch可以通过压缩_source字段,减少磁盘空间的开销
    • 缺点: 不适合在数据频繁修改的场景。 一条数据的改动,可能会引起很多数据的更新

    关系型数据库,一般会考虑Normalize 数据;在Elasticsearch,往往考虑Denormalize 数据。
    Elasticsearch并不擅长处理关联关系,一般会采用以下四种方法处理关联:

    • 对象类型
    • 嵌套对象(Nested Object)
    • 父子关联关系(Parent / Child )
    • 应用端关联

    对象类型

    案例1: 博客作者信息
    当我们查询博客信息的同时需要知道作者信息,那么把作者信息当成一个对象放到博客信息当中

    • 在每一博客的文档中都保留作者的信息
    • 如果作者信息发生变化,需要修改相关的博客文档
    PUT /blog
    {
      "mappings": {
        "dynamic":"false",
        "properties": {
          "content":{
            "type": "text"
          },
          "time":{
            "type": "date"
          },
          "user":{
            "properties": {
              "city":{
                "type": "keyword"
              },
              "userid":{
                "type":"long"
              },
              "username":{
                "type":"keyword"
              }
            }
          }
        }
      },
      "settings" : {
            "index" : {
                "analysis.analyzer.default.type": "ik_max_word"
            },
            "number_of_replicas": 1,
            "number_of_shards": 3
        }
    }
    PUT /blog/_doc/1
    {
      "content":"I like Elasticsearch",
      "time":"2022-06-23T17:20:20",
      "user":{
        "userid":1,
        "username":"lsx",
        "city":"厦门"
      }
    }
    # 查询对应作者的文档
    GET blog/_search
    {
      "query": {
        "term": {
          "user.username": {
            "value": "lsx"
          }
        }
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55

    案例2:包含对象数组的文档

    PUT /my_movies
    {
          "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"
        }
      ]
    }
    # 根据first_name 和 last_name 查询全名是Keanu Hopper
    GET /my_movies/_search
    {
      "query": {
        "bool": {
          "must": [
            {"term": { "actors.first_name": "Keanu"}},
            {"term": {"actors.last_name": "Hopper"}}
          ]
        }
      }  
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54

    在这里插入图片描述
    文档里面没有全名是Keanu Hopper 这个人,但是依然查了出来。查询出了不需要的数据。为什么会这样?
    存储时,内部对象的边界并没有考虑在内,JSON格式被处理成扁平式键值对的结构。当对多个字段进行查询时,导致了意外的搜索结果。可以用Nested Data Type解决这个问题。

    嵌套对象(Nested Object)

    什么是Nested Data Type

    • Nested数据类型: 允许对象数组中的对象被独立索引
    • 使用nested 和properties 关键字,将所有actors索引到多个分隔的文档
    • 在内部, Nested文档会被保存在两个Lucene文档中,在查询时做Join处理
    DELETE /my_movies
    # 创建 Nested 对象 Mapping
    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}}
            }
          }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    Nested 查询和普通查询不一样,通过上面的查询是查不出结果的

    GET /my_movies/_search
    {
      "query": {
        "bool": {
          "must": [
            {
              "nested": {
                "path": "actors",
                "query": {
                  "bool": {
                    "must": [
                      {"term": {"actors.first_name": "Keanu"}},
                      {"term": {"actors.last_name": "Reeves"}}
                    ]
                  }
                }
              }
              
            }
          ]
        }
      } 
    }
    # nested聚合
    GET /my_movies/_search
    {
      "size": 0,
      "aggs": {
        "actors": {
          "nested": {
            "path": "actors"
          },
          "aggs": {
            "actor_name": {
              "terms": {
                "field": "actors.first_name",
                "size": 10
              }
            }
          }
        }
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43

    父子关联关系(Parent / Child )

    • 对象和Nested对象的局限性: 每次更新,可能需要重新索引整个对象(包括根对象和嵌套对象)
    • ES提供了类似关系型数据库中Join 的实现。使用Join数据类型实现,可以通过维护Parent/ Child的关系,从而分离两个对象
      父文档和子文档是两个独立的文档;
      更新父文档无需重新索引子文档。子文档被添加,更新或者删除也不会影响到父文档和其他的子文档。

    设定 Parent/Child Mapping

    PUT /my_blogs
    {
      "settings": {
        "number_of_shards": 2
      },
      "mappings": {
        "properties": {
          "blog_comments_relation": {
            "type": "join",
            "relations": {
              "blog": "comment"
            }
          },
          "content": {
            "type": "text"
          },
          "title": {
            "type": "keyword"
          }
        }
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    在这里插入图片描述
    索引父文档

    #索引父文档
    PUT /my_blogs/_doc/blog1
    {
      "title":"Learning Elasticsearch",
      "content":"learning ELK ",
      "blog_comments_relation":{
        "name":"blog"
      }
    }
    
    #索引父文档
    PUT /my_blogs/_doc/blog2
    {
      "title":"Learning Hadoop",
      "content":"learning Hadoop",
      "blog_comments_relation":{
        "name":"blog"
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    在这里插入图片描述
    索引子文档

    #索引子文档
    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"
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32

    在这里插入图片描述
    注意:
    父文档和子文档必须存在相同的分片上,能够确保查询join 的性能
    当指定子文档时候,必须指定它的父文档ld。使用routing参数来保证,分配到相同的分片

    查询

    # 查询所有文档
    GET /my_blogs/_search
    
    #根据父文档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" : {
                    "term": {
                        "username.keyword": "Jack"
                    }
                }
        }
      }
    }
    # Has Parent 查询,返回相关的子文档
    POST /my_blogs/_search
    {
      "query": {
        "has_parent": {
          "parent_type": "blog",
          "query" : {
                    "match": {
                        "title" : "Learning Hadoop"
                    }
                }
        }
      }
    }
    
    #通过ID ,访问子文档
    GET /my_blogs/_doc/comment3
    #通过ID和routing ,访问子文档
    GET /my_blogs/_doc/comment3?routing=blog2
    
    #更新子文档
    PUT /my_blogs/_doc/comment3?routing=blog2
    {
        "comment": "Hello Hadoop??",
        "blog_comments_relation": {
          "name": "comment",
          "parent": "blog2"
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60

    嵌套文档 VS 父子文档

    Nested ObjectParent / Child
    优点文档存储在一起,读取性能高父子文档可以独立更新
    缺点更新嵌套的子文档时,需要更新整个文档需要额外的内存维护关系。读取性能相对差
    适用场景子文档偶尔更新,以查询为主子文档更新频繁

    Ingest Pipeline & Painless Script

    应用场景: 修复与增强写入数据

    Ingest Pipeline

    Ingest Node
    Elasticsearch 5.0后,引入的一种新的节点类型。默认配置下,每个节点都是Ingest Node:

    • 具有预处理数据的能力,可拦截lndex或 Bulk API的请求
    • 对数据进行转换,并重新返回给Index或 Bulk APl

    无需Logstash,就可以进行数据的预处理,例如

    • 为某个字段设置默认值;重命名某个字段的字段名;对字段值进行Split 操作
    • 支持设置Painless脚本,对数据进行更加复杂的加工

    Pipeline & Processor

    • Pipeline ——管道会对通过的数据(文档),按照顺序进行加工
    • Processor——Elasticsearch 对一些加工的行为进行了抽象包装
    • Elasticsearch 有很多内置的Processors,也支持通过插件的方式,实现自己的Processor

    一些内置的Processors:https://www.elastic.co/guide/en/elasticsearch/reference/7.17/ingest-processors.html

    Split Processor : 将给定字段值分成一个数组
    Remove / Rename Processor :移除一个重命名字段
    Append : 为商品增加一个新的标签
    Convert:将商品价格,从字符串转换成float 类型
    Date / JSON:日期格式转换,字符串转JSON对象
    Date lndex Name Processor︰将通过该处理器的文档,分配到指定时间格式的索引中
    Fail Processor︰一旦出现异常,该Pipeline 指定的错误信息能返回给用户
    Foreach Process︰数组字段,数组的每个元素都会使用到一个相同的处理器
    Grok Processor︰日志的日期格式切割)
    Gsub / Join / Split︰字符串替换│数组转字符串/字符串转数组
    Lowercase / upcase︰大小写转换

    在这里插入图片描述
    测试数据

    PUT tech_blogs/_doc/1
    {
      "title":"Introducing big data......111",
      "tags":"hadoop,elasticsearch,spark",
      "content":"You konw, for big data"
    }
    
    PUT tech_blogs/_doc/2
    {
      "title":"Introducing big data......222",
      "tags":"java,php,js",
      "content":"You konw, for big data"
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    创建Pipeline
    Pipeline的功能,文档中的tags标签,通过,转成数组对象,并且增加views字段

    PUT _ingest/pipeline/testPipeline
    {
      "description": "test",
      # 定义管道处理器 
      "processors": [
        {
          # 字符串分割处理器
          "split": {
            "field": "tags",
            "separator": ","
          }
        },
        {
          # 字符串分割处理器
          "set": {
            "field": "views",
            "value": "0"
          }
        }
      ]
    }
    #查看所有管道
    GET _ingest/pipeline
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    索引文档时,通过定义的管道加工数据

    PUT tech_blogs/_doc/3?pipeline=testPipeline
    {
      "title":"Introducing big data......3333",
      "tags":"mysql,redis,kafka",
      "content":"You konw, for big data"
    }
    
    PUT tech_blogs/_doc/4?pipeline=testPipeline
    {
      "title":"Introducing big data......4444",
      "tags":"test,text,tttt",
      "content":"You konw, for big data"
    }
    GET tech_blogs/_doc/3
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    在这里插入图片描述
    借助update_by_query更新已存在的文档

    #增加update_by_query的条件
    POST tech_blogs/_update_by_query?pipeline=blog_pipeline
    {
        "query": {
            "bool": {
                "must_not": {
                    "exists": {
                        "field": "views"
                    }
                }
            }
        }
    }
    
    GET tech_blogs/_search
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    Ingest Node VS Logstash

    LogstashIngest Node
    数据输入与输出支持从不同的数据源读取,并写入不同的数据源支持从ES REST API获取数据,并且写入Elasticsearch
    数据缓冲实现了简单的数据队列,支持重写不支持缓冲
    数据处理支持大量的插件,也支持定制开发内置的插件,可以开发Plugin进行扩展(Plugin更新需要重启)
    配置和使用增加了一定的架构复杂度无需额外部署

    Painless Script

    自Elasticsearch 5.x后引入,专门为Elasticsearch 设计,扩展了Java的语法。6.0开始,ES只支持 Painless。Groovy,JavaScript和 Python 都不再支持。Painless支持所有Java 的数据类型及Java API子集。
    Painless Script具备以下特性:

    • 高性能/安全
    • 支持显示类型或者动态定义类型

    Painless的用途:

    • 可以对文档字段进行加工处理
      更新或删除字段,处理数据聚合操作
      Script Field:对返回的字段提前进行计算
      Function Score:对文档的算分进行处理
    • 在lngest Pipeline中执行脚本
    • 在Reindex APl,Update By Query时,对数据进行处理

    通过Painless脚本访问字段

    上下文语法
    Ingestionctx.field_name
    Updatectx._source.field_name
    Search & Aggregationdoc[“field_name”]

    管道中定义脚本代码

    PUT _ingest/pipeline/test2PP
    {
      "description": "test2",
      "processors": [
        {
          "split": {
            "field": "tags",
            "separator": ","
          }
        },
        { # java 脚本代码 计算content长度
          "script": {
             "source": """
              if(ctx.containsKey("content")){
                ctx.content_length = ctx.content.length();
              }else{
                ctx.content_length=0;
              }
              """
          }
        },
        {
          "set": {
            "field": "views",
            "value": "0"
          }
        }
      ]
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    索引文档的时通过管道执行脚本

    PUT tech_blogs/_doc/5?pipeline=test2PP
    {
      "title":"Introducing big data......555",
      "tags":"mysql,redis,kafka",
      "content":"You konw, for big data"
    }
    
    GET tech_blogs/_doc/5
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    在这里插入图片描述
    更新时执行脚本代码

    POST tech_blogs/_update/4
    {
      "script": {
        "source": "ctx._source.views += params.new_views",
        "params": {
          "new_views":100
        }
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    定义脚本代码

    POST _scripts/testScripts
    {
      "script":{
        "lang": "painless",
        "source": "ctx._source.views += params.new_views"
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    执行定义的脚本代码

    POST tech_blogs/_update/4
    {
      "script": {
        "id": "testScripts",
        "params": {
          "new_views":1000
        }
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    查询中执行脚本代码

    DELETE tech_blogs
    PUT tech_blogs/_doc/1
    {
      "title":"Introducing big data......",
      "tags":"hadoop,elasticsearch,spark",
      "content":"You konw, for big data",
      "views":0
    }
    GET tech_blogs/_search
    {
      "script_fields": {
        "rnd_views": {
          "script": {
            "lang": "painless",
            "source": """
              java.util.Random rnd = new Random();
              doc['views'].value+rnd.nextInt(1000);
            """
          }
        }
      },
      "query": {
        "match_all": {}
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    脚本编译的开销较大,Elasticsearch会将脚本编译后缓存在Cache 中
    Inline scripts和 Stored Scripts都会被缓存,默认缓存100个脚本

    参数说明
    script.cache.max_size设置最大缓存数
    script.cache.expire设置缓存超时
    script.max_compilations_rate默认5分钟最多75次编译(75/5m)

    数据建模建议

    建模建议1:如何处理关联关系

    • Object: 优先考虑反范式(Denormalization)
    • Nested: 当数据包含多数值对象,同时有查询需求
    • Child/Parent:关联文档更新非常频繁时

    建模建议2: 避免过多字段
    一个文档中,最好避免大量的字段

    • 过多的字段数不容易维护
    • Mapping 信息保存在Cluster State 中,数据量过大,对集群性能会有影响
    • 删除或者修改数据需要reindex

    默认最大字段数是1000,可以设置index.mapping.total_fields.limit限定最大字段数。

    建模建议3︰避免正则,通配符,前缀查询
    正则,通配符查询,前缀查询属于Term查询,但是性能不够好。特别是将通配符放在开头,会导致性能的灾难
    建模建议4︰避免空值引起的聚合不准

    # Not Null 解决聚合的问题
    DELETE /scores
    PUT /scores
    {
      "mappings": {
          "properties": {
            "score": {
              "type": "float",
              "null_value": 0
            }
          }
        }
    }
    
    PUT /scores/_doc/1
    {
     "score": 100
    }
    PUT /scores/_doc/2
    {
     "score": null
    }
    
    POST /scores/_search
    {
      "size": 0,
      "aggs": {
        "avg": {
          "avg": {
            "field": "score"
          }
        }
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34

    如果不在mapping 中加上 “null_value”: 0 那么聚合会出现错误
    建模建议5: 为索引的Mapping加入Meta 信息
    Mappings设置非常重要,需要从两个维度进行考虑

    • 功能︰搜索,聚合,排序
    • 性能︰存储的开销; 内存的开销; 搜索的性能

    Mappings设置是一个迭代的过程

    • 加入新的字段很容易(必要时需要update_by_query)
    • 更新删除字段不允许(需要Reindex重建数据)
    • 最好能对Mappings 加入Meta 信息,更好的进行版本管理
    • 可以考虑将Mapping文件上传git进行管理
  • 相关阅读:
    java面试基础(三)
    SwiftUI中EnvironmentObject使用中,直接修改数据源的原值的方法
    如何进行MDM的产品测试
    如何安装与配置Node.js
    【django问题集】django.db.utils.OperationalError: (1040, ‘Too many connections‘)
    第四十六章 命名空间和数据库 - 系统提供的数据库
    Flask 使用 JWT(三)flask-jwt-extended
    网络安全(黑客)-小白自学
    函数空间的数学理论指导深度学习模型的设计和训练
    开源开放 | DeepOnto: 基于深度学习和语言模型的本体工程Python软件包
  • 原文地址:https://blog.csdn.net/admin522043032/article/details/125430856