• 由于在ES中,所有单个文档的增删改都是原子性的操作,因此将相关的实体数据都储存在同一个文档是很好的,且由于所有信息都在一个文档中,因此当我们查询时就没有必要像mysql一样去关联很多张表,只要搜一遍文档就可以查出所有需要的数据,查询效率非常高

  • 因此除了基本数据类型之外,ES也支持使用複杂的数据类型,像是数组、内部对象,而要使用内部对象的话,需要使用nested来定义索引,使文档内可以包含一个内部对象

    • 为什麽不用object而要使用nested来定义索引的原因是,obejct类型会使得内部对象的关联性丢失

    • 这是因为Lucene底层其实没有内部对象的概念,所以ES会利用简单的列表储存字段名和值,将object类型的对象层次摊平,再传给Lucene

    • 假设user类型是object,当插入一笔新的数据时,ES会将他转换为下面的内部文档,其中可以看见alice和white的关联性丢失了

      PUT 127.0.0.1/mytest/doc/1
      {
            
          "group": "fans",
          "user": [
              { "first": "John", "last": "Smith" },
              { "first": "Alice", "last": "White" }
          ]
      }
      转换后的内部文档
      {
            
          "group": "fans",
          "user.first": [ "alice", "john" ],
          "user.last": [ "smith", "white" ]
      }
    • 理论上从插入的数据来看,应该搜索 "first为Alice且last为White" 时,这个文档才算符合条件被搜出来,其他的条件都不算符合,但是因为ES把object类型的对象摊平了,所以实际上如果搜索 "first为Alice且last为Smith",这个文档也会当作符合的文档被搜出来,但这样就违反我们的意愿了,我们希望内部对象自己的关联性还是存在的

    • 因此在使用内部对象时,要改使用nested类型来取代object类型 (因为nested类型不会被摊平,下面说明)

  • nested类型就是为了解决object类型在对象数组上丢失关联性的问题的,如果将字段设置为nested类型,那个每一个嵌套对象都会被索引为一个 "隐藏的独立文档"

    • 其本质上就是将数组中的每个对象作为分离出来的隐藏文档进行索引,因此这也意味著每个嵌套对象可以独立于其他对象被查询

    • 假设将上面的例子的user改为nested类型,经过ES转换后的文档如下

      //嵌套文档1
      {
            
          "user.first": [ alice ],
          "user.last": [ white ]
      }
      //嵌套文档2
      {
            
          "user.first": [ john ],
          "user.last": [ smith ]
      }
      //根文档,或者也可以称为父文档
      {
            
          "group": "fans"
      }
    • 在独立索引每一个嵌套对象后,对象中每个字段的相关性得以保留,因此我们查询时,也仅返回那些真正符合条件的文档

    • 不仅如此,由于嵌套文档直接储存在文档内部,因此查询时嵌套文档和根文档的联合成本很低,速度和单独储存几乎一样

    • 但是要注意,查询的时候返回的是整个文档,而不是嵌套文档本身,并且如果要增删改一个嵌套对象,必须把整个文档重新索引才可以

  • 具体实例

    • 索引准备

      • 定义一个nested类型的mapping,user是一个内部对象,裡面包含了first、last和age,因为user设置了nested类型,因此user对象会被索引在独立的嵌套文档中

        PUT 127.0.0.1/mytest
        {
                
            "mappings": {
                
             "doc": {
                
                 "properties": {
                
                        "group": { "type": "keyword" },
                     "user": {
                
                         "type": "nested",
                            "properties": {
                
                             "first": { "type": "keyword" },
                                "last": { "type": "keyword" },
                                "age": { "type": "integer" }
                            }
                        }
                    }
                }
            }
        }
      • 插入两笔实际数据,因此在ES中存在的文档如下

        "hits": [
            {
                
                "_source": {
                
                    "group": "fans",
                    "user": {
                
                        "first": "Amy",
                        "last": "White",
                        "age": 18
                    }
                }
            },
            {
                
                "_source": {
                
                    "group": "fans",
                    "user": {
                
                        "first": "John",
                        "last": "Smith",
                        "age": 22
                    }
                }
            }
        ]
    • 嵌套对象查询 nested

      • 由于嵌套对象被索引在独立的隐藏文档中,因此我们无法直接使用一般的query去查询他,我们必须改使用 "nested查询" 去查询他们

      • nestedt查询是一个叶子子句,因此外层需要使用query或是bool来包含他,且因为nested查询是一个叶子子句,所以他也可以像一般的叶子子句一样被bool层层嵌套

      • nested查询的内部必须要包含一个path参数,负责指定要用的是哪个nested类型的字段,且要包含一个query,负责进行此嵌套对象内的查询

        GET 127.0.0.1/mytest/doc/_search
        {
                
            "query": {
                
                "nested": {
                
                 "path": "user",
                    "query": {
                
                        "bool": {
                
                         "must": [
                                { "term": { "user.first": "Amy" } },
                                { "term": { "user.last": "White" } }
                            ]
                        }
                    }
                }
            }
        }
      • 和bool的其他叶子子句(term、range...)一起搭配使用的nested查询

        GET 127.0.0.1/mytest/doc/_search
        {
                
            "query": {
                
             "bool": {
                
                 "filter": [
                        {
                
                      "term": {
                
                          "group": "fans"
                         }
                     },
                        {
                
                            "nested": {
                
                             "path": "user",
                                "query": {
                
                                 "term": {
                
                                     "user.first": "Amy"
                                    }
                                }
                            }
                        }
                ]
                }
            }
        }
    • 嵌套对象的评分 score_mode

      • 假设nested类型的user,储存的是一个数组,那麽在进行嵌套查询时,可能会匹配到多个嵌套的文档,而每一个匹配的嵌套文档都有自己的相关度得分

        • 假设有一个文档如下,一个根文档内,包含了3个嵌套文档

        • 当查询 "user.first = July或user.last = Month" 时,第一个嵌套文档的分数最高,第二个嵌套文档次之,第三个嵌套文档的分数最低

          "hits": [
              {
                    
                  "_source": {
                    
                      "group": "fans",
                      "user": [
                          { "first": "July", "last": "Month", "age": 18 },
                          { "first": "Aug", "last": "Month", "age": 22 },
                          { "first": "Monday", "last": "Day", "age": 25 }
                      ]
                  }
              }
          ]
      • 为了汇集这众多的嵌套文档分数到根文档,就需要设置score_mode来指定怎样去计算这些嵌套文档的总分

        • 默认情况下,根文档的分数是这些嵌套文档分数的平均值,就是默认score_mode = avg

        • 可以透过设置score_mode为avg、max、sum、none (直接返回1.0常数值分数),来控制根文档的得分策略

        • 不过要注意,如果 nested 查询放在一个 filter 子句中,就算定义了 score_mode 也不会生效,因为filter不打分,所以score_mode 就没有任何意义

          GET 127.0.0.1/mytest/doc/_search
          {
                    
              "query": {
                    
                  "nested": {
                    
                   "path": "user",
                      "score_mode": "max", //返回最佳匹配嵌套文档的_score给根文档使用
                      "query": {
                    
                          "bool": {
                    
                           "should": [
                                  { "term": { "user.first": "July" } },
                                  { "term": { "user.last": "Month" } }
                              ]
                          }
                      }
                  }
              }
          }
    • 使用嵌套对象的字段来排序

      • 儘管嵌套对象储存于独立的隐藏文档中,但依然有方法按照嵌套字段的值排序

      • 假设我们想要查出user.first为Amy,且依照 user.age 这个内部对象的字段,由小到大进行排序,查询语句如下

        GET 127.0.0.1/mytest/doc/_search
        {
                
            "query": {
                
                "nested": {
                
                    "path": "user",
                    "query": {
                
                        "term": {
                
                            "user.first": "Amy"
                        }
                    }
                }
            },
            "sort": {
                
                "user.age": {
                
                    "nested": {
                
                        "path": "user"
                    },
                    "order": "asc"
                }
            }
        }