ElasticSearch - 嵌套對象 nested

古古

2018/06/22


  • 由於在 ES 中,所有單個文檔的增刪改都是原子性的操作,因此將相關的實體數據都儲存在同一個文檔是很好的,且由於所有信息都在一個文檔中,因此當我們查詢時就沒有必要像 mysql 一樣去關聯很多張表,只要搜一遍文檔就可以查出所有需要的數據,查詢效率非常高

  • 因此除了基本數據類型之外,ES 也支持使用複雜的數據類型,像是數組、內部對象,而要使用內部對象的話,需要使用 nested 來定義索引,使文檔內可以包含一個內部對象

    • 為什麼不用 object 而要使用 nested 來定義索引的原因是,object 類型會使得內部對象的關聯性丟失

    • 這是因為 Lucene 底層其實沒有內部對象的概念,所以 ES 會利用簡單的列表儲存字段名和值,將 object 類型的對象層次攤平,再傳給 Lucene

    • 假設 user 類型是 object,當插入一筆新的數據時,ES 會將他轉換為下面的內部文檔,其中可以看見 alice 和 white 的關聯性丟失了

      PUT 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 mytest
        {
            "mappings": {
            	"doc": {
                	"properties": {
                        //group是正常的keyword字段
                        "group": { "type": "keyword" },
                        //user是一個nested類型,表示他底下還會包含子對象
                    	"user": {
                        	"type": "nested",
                            "properties": {
                            	"first": {
                                    "type": "keyword"
                                },
                                "last": {
                                    "type": "keyword"
                                },
                                "age": {
                                    "type": "integer"
                                }
                            }
                        }
                    }
                }
            }
        }
        
      • 插入兩筆數據

        POST mytest/doc
        {
            "group": "fans",
            "user": {
                "first": "Taylor",
                "last": "Swift",
                "age": 30
            }
        }
        
        POST mytest/doc
        {
            "group": "fans",
            "user": [
                {
                    "first": "Amy",
                    "last": "White",
                    "age": 18
                },
                {
                    "first": "John",
                    "last": "Smith",
                    "age": 22
                }
            ]    
        }
        
      • 因此在ES中存在的文檔如下

        "hits": [
            {
                "_source": {
                    "group": "fans",
                    "user": {
                        "first": "Taylor",
                        "last": "Swift",
                        "age": 30
                    }
                }
            },
            {
                "_source": {
                    "group": "fans",
                    "user": [
                        {
                            "first": "Amy",
                            "last": "White",
                            "age": 18
                        },
                        {
                            "first": "John",
                            "last": "Smith",
                            "age": 22
                        }
                    ]
                }
            }
        ]
        
  • 嵌套對象查詢 nested

    • 由於嵌套對象被索引在獨立的隱藏文檔中,因此我們無法直接使用一般的 query 去查詢他,我們必須改使用 “nested查詢” 去查詢他們

      • nested 查詢是一個葉子子句,因此外層需要使用 query 或是 bool 來包含他,且因為 nested 查詢是一個葉子子句,所以他也可以像一般的葉子子句一樣被 bool 層層嵌套
      • nested 查詢的內部必須要包含一個 path 參數,負責指定要用的是哪個 nested 類型的字段,且要包含一個 query,負責進行此嵌套對象內的查詢
    • 如果是將 bool 子句寫在 nested 裡面,表示要查詢某個子對象中,必須同時包含這兩個條件

      • 也就是說,下面的查詢實際上是查找必須存在一個子對象,然後此對象必須同時滿足 user.first 為 Taylor,且 user.last 為 Swift,所以能夠找到一個存在的文檔

        GET mytest/doc/_search
        {
            "query": {
                "nested": {
                	"path": "user",
                    "query": {
                        "bool": {
                        	"must": [
                                { "term": { "user.first": "Taylor" } },
                                { "term": { "user.last": "Swift" } }
                            ]
                        }
                    }
                }
            }
        }
        
        "hits": [
            {
                "_source": {
                    "group": "fans",
                    "user": {
                        "first": "Taylor",
                        "last": "Swift",
                        "age": 30
                    }
                }
            }
        ]
        
      • 但將查詢改寫成下面這樣的話,則搜索不到任何結果,因為沒有一個子對象同時滿足 user.first 為 Taylor 且 user.last 為xxx

        GET mytest/doc/_search
        {
            "query": {
                "nested": {
                	"path": "user",
                    "query": {
                        "bool": {
                        	"must": [
                                { "term": { "user.first": "Taylor" } },
                                { "term": { "user.last": "xxx" } }
                            ]
                        }
                    }
                }
            }
        }
        
    • 而如果是將 bool 子句寫在 nested 的外面,表示要查詢此 nested 對象兩次,而這兩次是分別使用不同的條件,因此不需要一定要同一個子對象中同時滿足這兩個條件

      • 下面的查詢表示只要 user 中包含 user.first 為 Amy,且也包含 user.last 為 Smith 就行了,但不是要求一定要有一個子對象是 user.first 為 Amy 且他的 user.last 剛好也是 Smith,所以結果才能搜出一個文檔

      • 如果這裡將 bool 改寫在 nested 裡面,那就搜不出任何文檔了

        GET mytest/doc/_search
        {
            "query": {
                "bool": {
                    "filter": [
                        {
                            "nested": {
                                "path": "user",
                                "query": {
                                    "term": {
                                        "user.first": "Amy"
                                    }
                                }
                            }
                        },
                        {
                            "nested": {
                                "path": "user",
                                "query": {
                                    "term": {
                                        "user.last": "Smith"
                                    }
                                }
                            }
                        }
                    ]
                }
            }
        }
        
        "hits": [
            {
                "_source": {
                    "group": "fans",
                    "user": [
                        {
                            "first": "Amy",
                            "last": "White",
                            "age": 18
                        },
                        {
                            "first": "John",
                            "last": "Smith",
                            "age": 22
                        }
                    ]
                }
            }
        ]
        
    • 和 bool 的其他葉子子句(term、range…)一起搭配使用的 nested 查詢

      GET 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 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 mytest/doc/_search
      {
          "query": {
              "nested": {
                  "path": "user",
                  "query": {
                      "term": {
                          "user.first": "Amy"
                      }
                  }
              }
          },
          "sort": {
              "user.age": {
                  "nested": {
                      "path": "user"
                  },
                  "order": "asc"
              }
          }
      }