圖解Spark Graphx實現頂點關聯鄰接頂點的collectNeighbors函數原理

image

一、場景案例

在一張社區網絡裏,可能需要查詢出各個頂點鄰接關聯的頂點集合,類似查詢某個人關係比較近的都有哪些人的場景。

在用Spark graphx中,通過函數collectNeighbors便可以獲取到源頂點鄰接頂點的數據。

下面以一個例子來說明,首先,先基於頂點集和邊來創建一個Graph圖。
image

該圖的頂點集合爲——

(1L, "Alice"),
(2L, "Bob"),
(3L, "Charlie"),
(4L, "David"),
(5L, "Eve"),
(6L, "Frank"),
(7L, "Grace"),
(8L, "Henry"),
(9L, "Ivy")

邊的集合爲——

Edge(1L, 2L, "friend"),
Edge(1L, 5L, "friend"),
Edge(2L, 3L, "friend"),
Edge(2L, 4L, "friend"),
Edge(3L, 4L, "friend"),
Edge(4L, 6L, "friend"),
Edge(5L, 7L, "friend"),
Edge(5L, 8L, "friend"),
Edge(6L, 9L, "friend"),
Edge(7L, 8L, "friend"),
Edge(8L, 9L, "friend")

基於以上頂點和邊,分別建立一個頂點RDD 和邊RDD,然後通過Graph(vertices, edges, defaultVertex)創建一個Graph圖,代碼如下——

val conf = new SparkConf().setMaster("local[*]").setAppName("graphx")
val ss = SparkSession.builder().config(conf).getOrCreate()

// 創建頂點RDD
val vertices = ss.sparkContext.parallelize(Seq(
  (1L, "Alice"),
  (2L, "Bob"),
  (3L, "Charlie"),
  (4L, "David"),
  (5L, "Eve"),
  (6L, "Frank"),
  (7L, "Grace"),
  (8L, "Henry"),
  (9L, "Ivy")
))


// 創建邊RDD
val edges = ss.sparkContext.parallelize(Seq(
  Edge(1L, 2L, "friend"),
  Edge(1L, 5L, "friend"),
  Edge(2L, 3L, "friend"),
  Edge(2L, 4L, "friend"),
  Edge(3L, 4L, "friend"),
  Edge(4L, 6L, "friend"),
  Edge(5L, 7L, "friend"),
  Edge(5L, 8L, "friend"),
  Edge(6L, 9L, "friend"),
  Edge(7L, 8L, "friend"),
  Edge(8L, 9L, "friend")
))

val graph = Graph(vertices, edges, null)

在成功創建圖之後,就可以基於已有的圖,通過collectNeighbors方法,分別得到每個頂點關聯鄰接頂點的數據——

val neighborVertexs = graph.mapVertices{
  case (id,(label)) => (label)
}.collectNeighbors(EdgeDirection.Either)

最終得到的neighborVertexs是一個VertexRDD[Array[(VertexId, VD)]]類型的RDD,可以通過neighborVertexs.foreach(println)打印觀察一下,發現數據裏,是每一個【頂點,元組】的結構,注意看,大概就能猜出來,通過neighborVertexs得到的RDD其實就是每個頂點關聯了鄰接頂點集合元組的數據——

(5,[Lscala.Tuple2;@bb793d7)
(8,[Lscala.Tuple2;@6d5786e6)
(1,[Lscala.Tuple2;@398cb9ea)
(9,[Lscala.Tuple2;@61c4eeb2)
(2,[Lscala.Tuple2;@d7d0256)
(6,[Lscala.Tuple2;@538f0156)
(7,[Lscala.Tuple2;@77a17e3d)
(3,[Lscala.Tuple2;@1be2a4fb)
(4,[Lscala.Tuple2;@1e0153f9)

可以進一步驗證,將元組裏的數據進行展開打印,通過以下代碼進行驗證——先通過coalesce(1)將分區設置爲一個分區,多個分區打印難以確定打印順序。然後再通過foreach遍歷RDD裏每一個元素,這裏的元素結構如(5,[Lscala.Tuple2;@bb793d7),x._1表示是頂點5,x._2表示[Lscala.Tuple2;@bb793d7,既然是元組,那就可以進一步進行遍歷打印,即 x._2.foreach(y => {...})——

neighborVertexs.coalesce(1).foreach(x => {
    print("頂點:" + x._1 + "關聯的鄰居頂點集合->{" )
    var str = "";
    x._2.foreach(y => {
      str += y + ","})
    print(str.substring(0, str.length - 1 ) +"}")
    println()
})

可以觀察一下最後打印結果——

頂點:8關聯的鄰居頂點集合->{(5,Eve),(7,Grace),(9,Ivy)}
頂點:1關聯的鄰居頂點集合->{(2,Bob),(5,Eve)}
頂點:9關聯的鄰居頂點集合->{(6,Frank),(8,Henry)}
頂點:2關聯的鄰居頂點集合->{(1,Alice),(3,Charlie),(4,David)}
頂點:3關聯的鄰居頂點集合->{(2,Bob),(4,David)}
頂點:4關聯的鄰居頂點集合->{(2,Bob),(3,Charlie),(6,Frank)}
頂點:5關聯的鄰居頂點集合->{(1,Alice),(7,Grace),(8,Henry)}
頂點:6關聯的鄰居頂點集合->{(4,David),(9,Ivy)}
頂點:7關聯的鄰居頂點集合->{(5,Eve),(8,Henry)}

結合文章開始的那一個圖驗證一下,頂點1關聯的鄰接頂點是(2,Bob),(5,Eve),正確;頂點8關聯的鄰接頂點是(5,Eve),(7,Grace),(9,Ivy),正確。其他驗證都與下圖情況符合。可見,通過collectNeighbors(EdgeDirection.Either)確實可以獲取網絡裏每個頂點關聯鄰接頂點的數據。
image

二、函數代碼原理解析

以上就是頂點關聯鄰接頂點的用法案例,接下來,讓我們分析一下collectNeighbors(EdgeDirection.Either)源碼,該函數實現了收集頂點鄰居頂點的信息——

def collectNeighbors(edgeDirection: EdgeDirection): VertexRDD[Array[(VertexId, VD)]] = {
  val nbrs = edgeDirection match {
    //聚合本頂點出度指向的鄰居頂點和入度指向本頂點的鄰居頂點
    case EdgeDirection.Either =>
      graph.aggregateMessages[Array[(VertexId, VD)]](
        ctx => {
          ctx.sendToSrc(Array((ctx.dstId, ctx.dstAttr)))
          ctx.sendToDst(Array((ctx.srcId, ctx.srcAttr)))
        },
        (a, b) => a ++ b, TripletFields.All)
    //聚合本頂點出度指向的鄰居頂點
    case EdgeDirection.In =>
      graph.aggregateMessages[Array[(VertexId, VD)]](
        ctx => ctx.sendToDst(Array((ctx.srcId, ctx.srcAttr))),
        (a, b) => a ++ b, TripletFields.Src)
    //聚合入度指向本頂點的鄰居頂點
    case EdgeDirection.Out =>
      graph.aggregateMessages[Array[(VertexId, VD)]](
        ctx => ctx.sendToSrc(Array((ctx.dstId, ctx.dstAttr))),
        (a, b) => a ++ b, TripletFields.Dst)
    case EdgeDirection.Both =>
      throw new SparkException("collectEdges does not support EdgeDirection.Both. Use" +
        "EdgeDirection.Either instead.")
  }
  graph.vertices.leftJoin(nbrs) { (vid, vdata, nbrsOpt) =>
    nbrsOpt.getOrElse(Array.empty[(VertexId, VD)])
  }
} // end of collectNeighbor

該函數用match做了一個類似Java的switch匹配,匹配有四種結果,其中,最後一種EdgeDirection.Both已經不支持,故而這裏就不解讀了,只講仍然有用的三種。

用一個圖來說明吧,假如有以下邊指向的圖——

Edge(2L, 1L),
Edge(2L, 4L),
Edge(3L, 2L),
Edge(2L, 5L),

image

  • EdgeDirection.Either表示本頂點的出度鄰居和入度鄰居。若本頂點爲2,那麼它得到鄰居頂點包括(1,4,3,5),該參數表示只要與頂點2一度邊關聯的,都會聚集成鄰居頂點。

  • EdgeDirection.In表示指向本頂點的鄰居,即本頂點的入度鄰居。若本頂點爲2,圖裏鄰居頂點只有3是指向2的,那麼頂點2得到鄰居頂點包括(3)。

  • EdgeDirection.Out表示本頂點的出度指向的鄰居頂點。若本頂點爲2,圖裏從頂點2指向鄰居頂點的,將得到(1,4,5)。

由此可知,頂點關聯鄰居頂點的函數collectNeighbors(EdgeDirection.Either)裏面的參數,就是可以基於該參數得到不同情況的鄰居頂點。

這裏以collectNeighbors(EdgeDirection.Either)說明函數核心邏輯——

 graph.aggregateMessages[Array[(VertexId, VD)]](
        ctx => {
          ctx.sendToSrc(Array((ctx.dstId, ctx.dstAttr)))
          ctx.sendToDst(Array((ctx.srcId, ctx.srcAttr)))
        },
        (a, b) => a ++ b, TripletFields.All)

該代碼做了聚合,表示會對圖裏的所有邊做處理。

圖裏有一種邊結構,叫三元組(Triplet),這種結構由以下三個部分組成——

  1. 源頂點(Source Vertex):圖中的一條邊的起始點或源節點。
  2. 目標頂點(Destination Vertex):圖中的一條邊的結束點或目標節點。
  3. 邊屬性(Edge Attribute):連接源頂點和目標頂點之間的邊上的屬性值。

在graph.aggregateMessages[Array[(VertexId, VD)]]( ctx => {......})聚合函數裏,就是基於三元組去做聚合統計的。

該聚合函數有兩個參數,第一個參數是一個函數(ctx) => { ... },裏面定義了每個頂點如何發送消息給鄰居頂點。

注意看,這裏的ctx正是一個三元組對象,基於該對象,可以獲取一下信息——

  • ctx.srcId:獲取源頂點的ID。

  • ctx.srcAttr:獲取源頂點的屬性。

  • ctx.dstId:獲取目標頂點的ID。

  • ctx.dstAttr:獲取目標頂點的屬性。

ctx作爲一個知道源頂點、目標頂點的三元組對象,就像一個郵差一樣,負責給兩邊頂點發送消息。

1、ctx.sendToSrc(Array((ctx.dstId, ctx.dstAttr)))函數,這裏頂點A是作爲目標頂點,鄰居節點B是源頂點,ctx對象就會將目標頂點B的頂點ID和屬性組成的元組(ctx.dstId, ctx.dstAttr)當作消息傳給源頂點A,A會將收到的消息保存下來,這樣就知道EdgeDirection.Either無向邊情況下,它有一個鄰居B了。
image

2、 ctx.sendToDst(Array((ctx.srcId, ctx.srcAttr)))函數,這時A成爲了源頂點,C成爲了目標頂點,ctx對象就會將源頂點A的頂點ID和屬性組成的元組(ctx.dstId, ctx.dstAttr)當作消息傳給源頂點B。B會將收到的消息以數組格式Array((ctx.dstId, ctx.dstAttr))保存下來,這樣B以後就知道EdgeDirection.Either無向邊情況下,它有一個鄰居A了。

image

這裏ctx.sendToDst()用Array((ctx.dstId, ctx.dstAttr))數組形式發送,是方便後面的(a, b) => a ++ b 合併函數操作,最後每個頂點可以將它收到的鄰居頂點數組合併到一個大的數組,即所有鄰居頂點聚集到一個數組裏返回。

還有一個TripletFields枚舉需要了解下——

TripletFields.All表示本頂點將聚合包括源頂點以及目標頂點發送頂點消息。

TripletFields.Src表示本頂點只聚合源頂點發送過來的頂點消息。

TripletFields.Dst表示本頂點只聚合目標頂點發送過來的頂點消息。

EdgeDirection.Either參數對應的是TripletFields.All,表示需要將本頂點接收到的所有源頂點以及目標頂點發送的頂點消息進行聚合。

接下來,就是做聚合了——

整個圖裏會有許多類似郵差角色的ctx對象,只需要處理完這些對象,那麼,每個頂點就會收到通過ctx對象傳送過來的鄰居頂點信息。

例如,A收到的ctx對象發過來的鄰居消息如下——

Array((B,屬性))

Array((C,屬性))

Array((D,屬性))

......

這時,就可以基於頂點A作爲分組key,將組內的Array((B,屬性))、Array((C,屬性))、Array((D,屬性))都合併到一個組裏,即通過(a, b) => a ++ b將分組各個數據合併成一個大數組{(B,屬性),(C,屬性),(D,屬性)},這個分組group的key是收到各個ctx對象發送鄰居消息過來的頂點A。

各個頂點聚合完後,返回一個nbrs,該RDD的每一個元素,即(頂點,頂點屬性,Array(鄰居頂點))——

val nbrs = edgeDirection match {
  case EdgeDirection.Either =>
    graph.aggregateMessages[Array[(VertexId, VD)]](
      ctx => {
        ctx.sendToSrc(Array((ctx.dstId, ctx.dstAttr)))
        ctx.sendToDst(Array((ctx.srcId, ctx.srcAttr)))
      },
      (a, b) => a ++ b, TripletFields.All)
		......  
}

接着將原圖graph的頂點vertices的rdd與聚合結果nbrs做左連接,返回一個新的 VertexRDD 對象,其中每個頂點都附帶了它的鄰居信息。如果某個頂點沒有鄰居信息(在 nbrs 中不存在對應的條目),則使用空數組來表示它的鄰居。

graph.vertices.leftJoin(nbrs) { (vid, vdata, nbrsOpt) =>
  nbrsOpt.getOrElse(Array.empty[(VertexId, VD)])
}

最後,得到的頂點關聯鄰居頂點的RDD情況,就如前文打印的那樣——

(5,[Lscala.Tuple2;@bb793d7) 頂點5展開鄰居頂點=> 頂點:5關聯的鄰居頂點集合->{(1,Alice),(7,Grace),(8,Henry)}
(8,[Lscala.Tuple2;@6d5786e6) 頂點8展開鄰居頂點=> 頂點:8關聯的鄰居頂點集合->{(5,Eve),(7,Grace),(9,Ivy)}
(1,[Lscala.Tuple2;@398cb9ea) 頂點1展開鄰居頂點=> 頂點:1關聯的鄰居頂點集合->{(2,Bob),(5,Eve)}
(9,[Lscala.Tuple2;@61c4eeb2) 頂點9展開鄰居頂點=> 頂點:9關聯的鄰居頂點集合->{(6,Frank),(8,Henry)}
(2,[Lscala.Tuple2;@d7d0256) 頂點2展開鄰居頂點=> 頂點:2關聯的鄰居頂點集合->{(1,Alice),(3,Charlie),(4,David)}
(6,[Lscala.Tuple2;@538f0156) 頂點6展開鄰居頂點=> 頂點:6關聯的鄰居頂點集合->{(4,David),(9,Ivy)}
(7,[Lscala.Tuple2;@77a17e3d) 頂點7展開鄰居頂點=> 頂點:7關聯的鄰居頂點集合->{(5,Eve),(8,Henry)}
(3,[Lscala.Tuple2;@1be2a4fb) 頂點3展開鄰居頂點=> 頂點:3關聯的鄰居頂點集合->{(2,Bob),(4,David)}
(4,[Lscala.Tuple2;@1e0153f9) 頂點4展開鄰居頂點=> 頂點:4關聯的鄰居頂點集合->{(2,Bob),(3,Charlie),(6,Frank)}

image

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