實現GraphX與GraphSON格式相互轉換

摘要

轉換器實現了:1.根據用戶輸入的SparkContext,和文件路徑,讀取GraphSON格式文件,轉換爲GraphX所接受的graphRDD;2.用戶輸入GraphX的graphRDD,在指定文件路徑輸出GraphSON格式文件。

相關版本

Apache TinkerPop 3.3.3
scala 2.11.8
spark-graphx 2.11

提示

假設讀者較爲熟悉GraphX中RDD相關操作,TinkerPop Gremlin圖的遍歷操作

背景

TinkerPop是一種圖計算框架,用於圖數據庫和圖分析處理。工業界已普遍採用TinkerPop進行圖的存儲,但TinkerPop實現的圖計算的接口很少,不便於直接進行圖分析處理。Spark GraphX已經實現了很多圖計算的算法接口,但TinkerPop中無法直接使用。

如果在GraphX和TinkerPop之間存在橋樑,把TinkerPop中存儲的圖轉換成GraphX圖的格式,就可以利用GraphX豐富的圖計算進行處理,最後將處理後的圖轉換回TinkerPop中進行存儲。TinkerPop也沒有直接轉換爲GraphX所需圖格式的接口,如何自行搭建橋樑?我們利用TinkerPop輸出讀入文件的接口,將文件作爲中轉站,通過解析或構造文件內容,和GraphX的圖進行相互轉換。TinkerPop支持的文件格式很多,有Gryo,GraphSON,Script。我們選擇GraphSON,是以json格式,文件便於我們直接查看,也便於程序解析。

現實中,普遍存在大量數據,如果數據量很大的情況下,如何確保數據轉換間的高效,讓數據在橋樑上快速往返。我們發現TinkerGraph中,提供了HDFS中的GraphSON文件到RDD之間轉換的接口,從而利用Spark,Hadoop進行大數據高效處理。

總的來說,我們的工作意義在於結合兩種系統各自的優點,TinkerPop圖數據庫存儲的廣泛性和GraphX圖計算接口的多樣性,從而滿足更廣泛的需求。

TinkerPop數據結構StarGraph

轉換器中涉及到的重要數據結構就是StarGraph,它本身是一個很小的圖,以單節點爲中心,包含它自身的點屬性,所有的鄰接邊(包括出邊,入邊)屬性,以及鄰接點的ID。每個點維護這樣一個StarGraph,所有的點就構成了TinkerPop完整的圖。
圖對應一個完整的json文件,其中每一行就恰好是一個StarGraph。官方指導手冊給出如下實例:
http://kelvinlawrence.net/book/Gremlin-Graph-Guide.html#_adjacency_list_format_graphson

{"id":0,"label":"airport","inE":{"route":[{"id":17,"outV":4}]}, ... }
{"id":2,"label":"airport","inE":{"route":[{"id":18,"outV":4}, ... ]}}
{"id":4,"label":"airport","inE":{"route":[{"id":15,"outV":2}]}, ... }
{"id":6,"label":"airport","inE":{"route":[{"id":16,"outV":4}, ... ]}}
{"id":8,"label":"airport","inE":{"route":[{"id":11,"outV":0}]}, ... }

其中每一行代表一個StarGraph,形如其名,包含中心點的id,標籤,點屬性,入邊,出邊,在轉換器中解析,構造GraphSON文件都以每一個StarGraph爲基本單位,展開如下:

{
    "id": 0,
    "label": "airport",
    "inE": {
        "route": [{
            "id": 17,
            "outV": 4
        }]
    },
    "outE": {
        "route": [{
            "id": 10,
            "inV": 2
        }, {
            "id": 11,
            "inV": 8
        }]
    },
    "properties": {
        "code": [{
            "id": 1,
            "value": "AUS"
        }]
    }
}

處理過程

從GraphSON轉換爲GraphX

大致上分爲兩步,第一步,系統有自帶讀取GraphSON文件的接口,得到以StarGraph爲核心的中間RDD;第二步,處理中間RDD,分別生成GraphX所需要的VertexRDD和RDD[Edge]

讀入GraphSON轉換爲JavaPairRDD

關鍵API
org.apache.tinkerpop.gremlin.spark.structure.io
Class InputFormatRDD

Modifier and Type Method and Description
<any> readGraphRDD(Configuration configuration, JavaSparkContext sparkContext)
Read the graphRDD from the underlying graph system.

輸入部分代碼如下,inputGraphFilePath爲用戶輸入的文件路徑,jsc爲用戶傳入的sparkContext。官方文檔中沒有給出返回類型的明確格式,查看源碼後得知,返回得到的vertexWritableJavaPairRDD中每一個元素的格式是 Tuple2[AnyRef, VertexWritable],其中VertexWritable通過get方法就可以返回Vertex類型

val inputGraphConf = new BaseConfiguration
inputGraphConf.setProperty("gremlin.graph", classOf[HadoopGraph].getName)
inputGraphConf.setProperty(Constants.GREMLIN_HADOOP_GRAPH_READER, classOf[GraphSONInputFormat].getName)
inputGraphConf.setProperty(Constants.GREMLIN_HADOOP_INPUT_LOCATION, inputGraphFilePath)
inputGraphConf.setProperty(Constants.MAPREDUCE_INPUT_FILEINPUTFORMAT_INPUTDIR, inputGraphFilePath)
val jsc = JavaSparkContext.fromSparkContext(sc)
val graphRDDInput = new InputFormatRDD
val vertexWritableJavaPairRDD = graphRDDInput.readGraphRDD(inputGraphConf, jsc)

解析JavaPairRDD

從JavaPairRDD中得到的Vertex類型,每一個Vertex可以視作一個StarGraph,正如上面所提到的,從StarGraph中利用遍歷對象可以獲取到中心點ID,屬性,所有邊屬性,鄰接點ID,這些信息足以構建GraphX所需的VertexRDD和RDD[Edge]。

涉及到StarGraph構建API
org.apache.tinkerpop.gremlin.structure.util.star.StarGraph
Class StarGraph

Modifier and Type Method and Description
static StarGraph of(Vertex vertex)
Creates a new StarGraph from a Vertex.

構造VertexRDD的過程如下:

val vertexRDD:RDD[(Long,HashMap[String,java.io.Serializable])] = vertexWritableJavaPairRDD.rdd.map((tuple2: Tuple2[AnyRef, VertexWritable]) => {
 
      // Get the center vertex
      val v = tuple2._2.get
      val g = StarGraph.of(v)
      // In case the vertex id in TinkerGraph is not long type
      val vid = convertStringIDToLongID(v.id().toString)
 
      // Pass the vertex properties to GraphX vertex value map and remain the original vertex id
      var graphxValueMap : HashMap[String,java.io.Serializable] = new HashMap[String,java.io.Serializable]()
      graphxValueMap.put("originalID",v.id().toString)
      graphxValueMap.putAll(g.traversal.V(v.id).valueMap().next(1).get(0))
      (vid,graphxValueMap)
    })

注:
1.JavaPairRDD轉換到rdd.RDD可以使用自帶的rdd方法;
2.TinkerGraph中點ID屬性類型可以是整形,也可以是字符串,在這裏統一按字符串處理,使用Hashing工具轉換爲GraphX VertexID接收的Long型,避免信息丟失,原有TinkerGraph ID作爲點屬性以“originalID”爲鍵存儲在HashMap中。


構造RDD[Edge]的過程如下

val edge = vertexWritableJavaPairRDD.rdd.flatMap((tuple2: Tuple2[AnyRef, VertexWritable]) => {
      val v = tuple2._2.get
      val g = StarGraph.of(v)
      val edgelist:util.List[Edge] = g.traversal.V(v.id).outE().toList
 
      // Put all edges of the center vertex into the list
      val list = new collection.mutable.ArrayBuffer[graphx.Edge[util.HashMap[String,java.io.Serializable]]]()
      var x = 0
      for(x <- 0 until edgelist.size()){
        var srcId = edgelist.get(x).inVertex.id().toString
        var dstId = edgelist.get(x).outVertex.id().toString
        val md1 = convertStringIDToLongID(srcId)
        val md2 = convertStringIDToLongID(dstId)
        // Get the properties of the edge
        var edgeAttr = new util.HashMap[String,java.io.Serializable]()
        edgelist.get(x).properties().asScala.foreach((pro:Property[Nothing])=>
        {edgeAttr.put(pro.key(),pro.value().toString)})
        list.append(graphx.Edge(md1,md2,edgeAttr))
      }
      list
    })
val edgeRDD = edge.distinct()

注:
1.一箇中心點可能有多個鄰接邊,利用ArrayBuffer將每個邊按照Edge格式存儲到list,flatMap將每一個list展開;
2.爲了區別TinkerPop的Edge和GraphX的Edge,將GraphX的Edge格式用graphx.Edge表示。


構造GraphX graphRDD

已經具備了VertexRDD和RDD[Edge],構造GraphX輕而易舉

graphx.Graph[util.HashMap[String,java.io.Serializable],
      HashMap[String,java.io.Serializable]](vertexRDD,edgeRDD,new HashMap[String,java.io.Serializable]())

注:
這裏需要顯式指定點屬性,邊屬性的類型,就是GraphX官方文檔中的VD和ED,我們轉換器中,屬性都使用util.HashMap[String,java.io.Serializable]類型來存儲。

至此,我們從TinkerPop出發,順利到達GraphX,之後可以利用豐富的圖計算算子進行圖分析處理,可以將得到的結果作爲新的屬性添加到點或邊屬性中,從而生成新的GraphX graphRDD。在GraphX玩久了突然想家,如何找到回家的路,轉換回GraphSON,且聽下節分解。

從GraphX轉換爲GraphSON

往返路線具有對稱性,樞紐也是TinkerPop的StraGraph,解析GraphX的graphRDD,把元素構造成StarGraph的形式,再利用TinkerPop寫文件的接口。

解析GraphX graphRDD

爲了生成StarGraph,一定需要按每個點ID進行join的操作,從而生成中心點及其鄰接邊,鄰接點的結構。
關鍵API
org.apache.spark.graphx
abstract class Graph[VD, ED] extends Serializable

Modifier and Type Method and Description
Graph[VD2, ED] outerJoinVertices[U, VD2](other: RDD[(VertexId, U)])(mapFunc: (VertexId, VD, Option[U]) ⇒ VD2)(implicit arg0:ClassTag[U], arg1: ClassTag[VD2], eq: =:=[VD, VD2] = null)

Joins the vertices with entries in the table RDD and merges the results using mapFunc. The input table should contain at most one entry for each vertex. If no entry in other is provided for a particular vertex in the graph, the map function receives None.

class GraphOps[VD, ED] extends Serializable

Modifier and Type Method and Description
VertexRDD[Array[Edge[ED]]] collectEdges(edgeDirection: EdgeDirection)

Returns an RDD that contains for each vertex v its local edges, i.e., the edges that are incident on v, in the user-specified direction.

collectEdges將一個點所有的出邊都加入到Array中,作爲一個點的新屬性,但點的原本屬性被丟棄。我們於是使用outerJoinVertices,把點屬性和邊Array信息聚合構建StarGraph,並將之存儲在每個點的屬性中。

    // Tuple2 of the src vertex id and the array of all its out Edge
    val vertexRDDWithEdgeProperties = graphRDD.collectEdges(EdgeDirection.Out)
 
    // Join the vertex id ,vertex attribute and the array of all its out edges(as adjacent edges)
    val tinkerPopVertexRDD = graphRDD.outerJoinVertices(vertexRDDWithEdgeProperties) {
      case (centerVertexID, centerVertexAttr, adjs) => {
        // Create the StarGraph and its center
        val graph = StarGraph.open
        val cache = new util.HashMap[Long, Vertex]
        val centerVertex:Vertex = getOrCreateVertexForStarGraph(graph,cache,
                                                                centerVertexID,true,centerVertexAttr)
        // Add adjacent edges
        adjs.get.map(edge => {
 
          // Create the adjacent vertex
          val anotherVertexID = edge.dstId
          val edgeProperties = edge.attr
          val srcV = centerVertex
          val dstV :Vertex = getOrCreateVertexForStarGraph(graph,cache,anotherVertexID,false, null)
 
          // For both direction, add an edge between the both vertices
          val outedgeID:lang.Long = hashEdgeID(edge.srcId.toString,edge.dstId.toString)
          val outedge = srcV.addEdge(DEFAULT_EDGE_LABEL,dstV,T.id,outedgeID)
          if (outedge != null && edgeProperties.size > 0) addProperties(outedge, edgeProperties)
 
          val inedgeID:lang.Long = hashEdgeID(edge.dstId.toString,edge.srcId.toString)
          val inedge = dstV.addEdge(DEFAULT_EDGE_LABEL,srcV,T.id,inedgeID)
          if (inedge != null && edgeProperties.size > 0) addProperties(inedge, edgeProperties)
        })
 
        // Return the center vertex
        graph.getStarVertex
      }
    }.vertices.map {case(vid, vertex) => vertex}
 

注:
1.添加鄰接邊的過程中,需要指定邊ID值,不同的邊ID不同,一條邊需要對稱地添加在起點和終點,保證相同的ID值,纔可以被TinkerPop Gremlin讀入識別(寫入GraphSON文件時,不會檢查ID唯一性問題,在讀取文件,創建Graph時候,會進行識別,如果有重複點ID或者邊ID出現,會報錯)。
2.對於無向圖,兩個點之間視爲有兩條方向不同的有向邊,這兩條邊的ID生成策略如下,設兩個點的ID分別是A,B,轉換爲字符串後,A到B的ID值爲“A”拼接"B"的哈希值,B到A的ID值爲“B”拼接“A”的哈希值。


在解析過程中,生成StarGraph是很重要的一部分
org.apache.tinkerpop.gremlin.structure.util.star.StarGraph
Class StarGraph

Modifier and Type Method and Description
static StarGraph open()
Creates an empty StarGraph.
StarGraph.StarVertex getStarVertex()
Gets the Vertex representative of the StarGraph.
Vertex addVertex(Object... keyValues)
Add a Vertex to the graph given an optional series of key/value pairs.

解析過程中創建StrarGraph的getOrCreateVertexForStarGraph函數如下

  def getOrCreateVertexForStarGraph(graph:StarGraph, cache:util.HashMap[Long, Vertex],
                      name: Long,isCenter: Boolean,
                      properties :util.HashMap[String, java.io.Serializable]):Vertex = {
 
    // Get the vertex contained in the cache or create one
    // Return the vertex
    if (cache.containsKey(name) && !isCenter) cache.get(name)
    else if (!cache.containsKey(name) && !isCenter) {
      val v = graph.addVertex(T.id, name:lang.Long, T.label, DEFAULT_VERTEX_LABEL)
      cache.put(name, v)
      v
    } else if (cache.containsKey(name) && isCenter) {
      val v = cache.get(name)
 
      // Add the properties only if the vertex is center vertex
      properties.asScala.foreach(pro => {
        v.property(pro._1, pro._2)
      })
      cache.replace(name, v)
      v
    } else {
      val v = graph.addVertex(T.id, name:lang.Long, T.label, DEFAULT_VERTEX_LABEL)
      properties.asScala.foreach(pro => {
        v.property(pro._1, pro._2)
      })
      cache.put(name, v)
      v
    }
  }

注:
點屬性以HashMap形式保存,引入了cache,避免重複創建。中心點的鄰接點(isCenter = false)創建時,不需要寫入點屬性,每個點都會被遍歷爲中心點,避免重複寫入點屬性。


寫入GraphSON

和讀入過程對稱,需要構造Tuple2[AnyRef, VertexWritable]的JavaPairRDD。

org.apache.tinkerpop.gremlin.spark.structure.io
Class OutputFormatRDD

Modifier and Type Method and Description
void writeGraphRDD(Configuration configuration, <any> graphRDD)
Write the graphRDD to an output location.

根據vertex:VertexRDD構建元素是Tuple2[AnyRef, VertexWritable]的JavaPairRDD,輸出到指定文件路徑,完成最後的收尾工作。

// Change the form for adapting to the java interface
val tinkergraphRDD = tinkerPopVertexRDD.map(vertex => (AnyRef :AnyRef, new VertexWritable(vertex))).toJavaRDD()
 
///////// Output the VertexRDD
val outputConf = new BaseConfiguration
val tmpOutputPath = outputFilePath + "~"
val hadoopConf = new Configuration
val path = URI.create(outputFilePath)
outputConf.setProperty(Constants.GREMLIN_HADOOP_GRAPH_WRITER, classOf[GraphSONOutputFormat].getName)
outputConf.setProperty(Constants.GREMLIN_HADOOP_OUTPUT_LOCATION, tmpOutputPath)
FileSystem.get(path,hadoopConf).delete(new Path(tmpOutputPath), true)
FileSystem.get(path,hadoopConf).delete(new Path(outputFilePath), true)
FileSystem.get(path,hadoopConf).deleteOnExit(new Path(tmpOutputPath))
val formatRDD = new OutputFormatRDD
formatRDD.writeGraphRDD(outputConf, JavaPairRDD.fromJavaRDD(tinkergraphRDD))
sc.stop()
FileSystem.get(path,hadoopConf).rename(new Path(tmpOutputPath, "~g"), new Path(outputFilePath))
FileSystem.get(path,hadoopConf).delete(new Path(tmpOutputPath), true)

注:
1.從rdd.RDD到JavaPairRDD,需要經過JavaRDD的中轉,利用JavaPairRDD自帶的fromJavaRDD方法;
2.writeGraphRDD的過程最後產生的文件名和用戶提供的有微小差別,需要特別處理。

尾聲

至此,我們已經完成了從GraphSON到GraphX之間圖數據的旅程,意味着我們實現了通過GraphSON作爲中轉站,兩個圖框架的對接。下一步,我們的目標是直接從圖數據庫到GraphX的轉換,希望可以繞開中轉站,實現無縫對接。



作者:ljh_77ef
鏈接:https://www.jianshu.com/p/110615738d23

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