ELK生態:ES中的數據關聯查詢

1、前言

對於solr來說是無法做兩個collection之間的關聯的,es是否可以做到類似於表的join關聯那,這就是本篇需要研究的內容,

主要參考內容是官方文檔。

先說下結論,如果不做特殊處理,es是無法完成類似與表Join的關聯查詢的

 

2、ES如何做關聯

官網裏面有幾種支持關聯查詢的辦法:

2.1 應用程序做關聯

  這個沒有什麼好說的,其實不算真正的關聯,需要先查詢一個索引,得到值構造出條件再去查詢另外一個索引。

  缺點也很明顯,就是需要查詢多次。

 

2.2 冗餘數據

   簡單的來說就是將需要查詢的數據保存在一起,舉個例子如下:

    假設有用戶索引和用戶發佈博客的索引,需要將用戶和博客信息關聯起來,對於用戶來說,我們只需要取得用戶的姓名信息即可,則可以這樣做:

 

可以看下map信息如下:

實際在es內,已經將user下面的id和name進行了扁平化處理,可以通過如下的方式查詢:

 

 

 

優點:查詢速度非常快,缺點是存在數據的冗餘。

 

2.3 嵌套對象

在ES中,對單個文檔的增刪改都是原子操作,有時候爲了方便我們將實體和它相關的明細是放在一個文檔中存儲的。比如論壇發的帖子和它的回覆信息。

其實和冗餘對象有點類似,但是如果只是做查詢會發現有問題,因爲es扁平處理之後:

{
  "title":            [ eggs, nest ],
  "body":             [ making, money, work, your ],
  "tags":             [ cash, shares ],
  "comments.name":    [ alice, john, smith, white ],
  "comments.comment": [ article, great, like, more, please, this ],
  "comments.age":     [ 28, 31 ],
  "comments.stars":   [ 4, 5 ],
  "comments.date":    [ 2014-09-01, 2014-10-22 ]
}

tilte、body、tags被稱爲父文檔或根文檔。

這樣數組內之間是沒有順序關係的,這就導致了後面的查詢仍然可以查到數據,嵌套對象是爲了解決這個問題的,先看下普通的對象:

 

 

{
  "query": {
    "bool": {
      "must": [
        { "match": { "comments.name": "Alice" }},
        { "match": { "comments.age":  28      }} ,
        { "match": { "comments.comment.keyword":  "Great article"     }}
      ]
    }
  }
}

 

嵌套對象,上面的例子是沒有定義map的情況直接發送數據,comments被定義爲object,失去了數組內的順序關係,如果先定義了nested對象,則如下:

 

 

PUT my_index2
{
  "mappings": {
    "blogpost2": {
      "properties": {
        "comments": {
          "type": "nested",
          "properties": {
            "name":    { "type": "text"  },
            "comment": { "type": "text"  },
            "age":     { "type": "short"   },
            "stars":   { "type": "short"   },
            "date":    { "type": "date"    }
          }
        }
      }
    }
  }
}

再次發送相同的數據:

PUT /my_index2/blogpost2/10
{
  "title": "Nest eggs",
  "body":  "Making your money work...",
  "tags":  [ "cash", "shares" ],
  "comments": [
    {
      "name":    "John Smith",
      "comment": "Great article",
      "age":     28,
      "stars":   4,
      "date":    "2014-09-01"
    },
    {
      "name":    "Alice White",
      "comment": "More like this please",
      "age":     31,
      "stars":   5,
      "date":    "2014-10-22"
    }
  ]
}

再次發起查詢:

 

爲什麼查不到,是因爲nested對象有自己特定的語法如下:

 

{
  "query": {
    "nested": {
      "path": "comments",
       "score_mode": "max",
 
      "query": {
        "bool": {
          "must": [
            {
              "term": {
                "comments.age": 31
              }
            },
            {
              "term": {
                "comments.name": "alice"
             }
            }
          ]
        }
      }
    }
  }
}

score_mode:表示嵌套文檔的最高得分納入到根文檔的計算之中。

嵌套模型的缺點如下:

當對嵌套文檔做增加、修改或者刪除時,整個文檔都要重新被索引。嵌套文檔越多,這帶來的成本就越大。
查詢結果返回的是整個文檔,而不僅僅是匹配的嵌套文檔。儘管目前有計劃支持只返回根文檔中最佳匹配的嵌套文檔,但目前還不支持。

 

2.4 父子對象

父子對象是最類似與表join的對象,父子關係的對象分別位於不同的文檔中,做到了很好的隔離。
 

有以下優點:
1)更新父文檔或子文檔時候,另一方不受影響。
2)創建和刪除子文檔,父文檔不受到影響。
3)子文檔可以作爲獨立的結果單獨返回。

缺點是:
1)父文檔和子文檔必須存在同一個shard中。
2)貌似只能是同一個index的兩個type(對於es6.x版本只能支持一個type,如何處理,目前還未看到)

原理:

Elasticsearch 維護了一個父文檔和子文檔的映射關係,得益於這個映射,父-子文檔關聯查詢操作非常快。
但是這個映射也對父-子文檔關係有個限制條件:父文檔和其所有子文檔,都必須要存儲在同一個分片中。
父-子文檔ID映射存儲在 Doc Values 中。當映射完全在內存中時, Doc Values 提供對映射的快速處理能力,
另一方面當映射非常大時,可以通過溢出到磁盤提供足夠的擴展能力
 

如何建立父子映射:
建立父-子文檔映射關係時只需要指定某一個文檔 type 是另一個文檔 type 的父親。 該關係可以在如下兩個時間點設置:

1)創建索引時;

2)在子文檔 type 創建之前更新父文檔的 mapping。
舉例來說,對於公司和員工之間存在着類似的關係,即可以將公司信息看成員工信息的父文檔。

如下:

PUT /company
{
  "mappings": {
    "branch": {}, //公司type
    "employee": {
      "_parent": {
        "type": "branch" //employee 文檔 是 branch 文檔的子文檔。
      }
    }
  }
}

 

父子文檔的創建
1)對於父對象來說,它是不知道有多少個子對象的,所以按照一般的對象創建方法即可。
2)子對象創建方法:

PUT /company/employee/1?parent=london
{
  "name":  "Alice Smith",
  "dob":   "1970-10-24",
  "hobby": "hiking"
}

父文檔 ID 有兩個作用:創建了父文檔和子文檔之間的關係,並且保證了父文檔和子文檔都在同一個分片上。

這裏面的父ID london 會作爲路由的依據,這樣子對象就會路由到父文檔同一個shard上。
在執行單文檔的請求時需要指定父文檔的 ID,單文檔請求包括:通過 GET 請求獲取一個子文檔;創建、更新或刪除一個子文檔。
而執行搜索請求時是不需要指定父文檔的ID,這是因爲搜索請求是向一個索引中的所有分片發起請求,而單文檔的操作是隻會向存儲該文檔的分片發送請求。
因此,如果操作單個子文檔時不指定父文檔的 ID,那麼很有可能會把請求發送到錯誤的分片上。

父文檔的 ID 應該在 bulk API 中指定

POST /company/employee/_bulk
{ "index": { "_id": 2, "parent": "london" }}
{ "name": "Mark Thomas", "dob": "1982-05-16", "hobby": "diving" }
{ "index": { "_id": 3, "parent": "liverpool" }}
{ "name": "Barry Smith", "dob": "1979-04-01", "hobby": "hiking" }
{ "index": { "_id": 4, "parent": "paris" }}
{ "name": "Adrien Grand", "dob": "1987-05-11", "hobby": "horses" }

通過子文檔查詢父文檔
查詢80後所在的公司信息:

GET /company/branch/_search
{
  "query": {
    "has_child": {
      "type": "employee",
      "query": {
        "range": {
          "dob": {
            "gte": "1980-01-01"
          }
        }
      }
    }
  }
}

 

查詢至少兩個員工的公司:

GET /company/branch/_search
{
  "query": {
    "has_child": {
      "type":         "employee",
      "min_children": 2,
      "query": {
        "match_all": {}
      }
    }
  }
}

 

通過父文檔查詢子文檔

GET /company/employee/_search
{
  "query": {
    "has_parent": {
      "type": "branch",
      "query": {
        "match": {
          "country": "UK"
        }
      }
    }
  }
}
 
查詢公司在UK的所有員工。

 

每個子文檔都保存了父文檔的ID。

 


當你考慮父子關係是否適合你現有關係模型時,請考慮下面這些建議:

  • 儘量少地使用父子關係,僅在子文檔遠多於父文檔時使用。
  • 避免在一個查詢中使用多個父子聯合語句。
  • 在 has_child 查詢中使用 filter 上下文,或者設置 score_mode 爲 none 來避免計算文檔得分。
  • 保證父 IDs 儘量短,以便在 doc values 中更好地壓縮,被臨時載入時佔用更少的內存。
  • 父子關聯查詢比普通的查詢慢5-10倍

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章