---
abstract: 'The work on large-scale graph analytics to date has largely focused on the study of static properties of graph snapshots. However, a static view of interactions between entities is often an oversimplification of several complex phenomena like the *spread of epidemics*, *information diffusion*, *formation of online communities*, and so on. Being able to find temporal interaction patterns, visualize the evolution of graph properties, or even simply compare them across time, adds significant value in reasoning over graphs. However, because of lack of underlying data management support, an analyst today has to manually navigate the added temporal complexity of dealing with large evolving graphs. In this paper, we present a system, called *Historical Graph Store*, that enables users to store large volumes of historical graph data and to express and run complex temporal graph analytical tasks against that data. It consists of two key components: a [*Temporal Graph Index*]{} (TGI), that compactly stores large volumes of historical graph evolution data in a partitioned and distributed fashion; it provides support for retrieving snapshots of the graph as of any timepoint in the past or evolution histories of individual nodes or neighborhoods; and a Spark-based [*Temporal Graph Analysis Framework*]{} (TAF), for expressing complex temporal analytical tasks and for executing them in an efficient and scalable manner. Our experiments demonstrate our system’s efficient storage, retrieval and analytics across a wide variety of queries on large volumes of historical graph data.'
author:
- |
    Udayan Khurana\
    \
    Amol Deshpande\
    \
bibliography:
- 'TGIdraft-extended.bib'
title:
- Historical Graph Data Management
- Storing and Analyzing Historical Graph Data at Scale
---

Introduction
============

Graphs are useful in capturing behavior involving interactions between entities. Several processes are naturally represented as graphs – social interactions between people, financial transactions, biological interactions among proteins, geospatial proximity of infected livestock, and so on. Many problems based on such graph models can be solved using well-studied algorithms from graph theory or network science. Examples include finding driving routes by computing shortest paths on a network of roads, finding user communities through dense subgraph identification in a social network, and many others. Numerous graph data management systems have been developed over the last decade, including specialized graph database systems like Neo4j, Titan, etc., and large-scale graph processing frameworks such as Pregel [@pregel], Giraph, GraphLab [@distgraphlab], GraphX [@graphx], GraphChi [@kyrola2012graphchi], etc. However much of the work to date, especially on cloud-scale graph data management systems, focuses on managing and analyzing a single (typically, current) static snapshot of the data. In the real world, however, interactions are a dynamic affair and any graph that abstracts a real-world process changes over time. For instance, in online social media, the friendship network on Facebook or the “follows” network on Twitter change steadily over time, whereas the “mentions” or the “retweet” networks change much more rapidly. Dynamic cellular networks in biology, evolving citation networks in publications, dynamic financial transactional networks, are few other examples of such data. Lately, we have seen an increasing merit in dynamic modeling and analysis of network data to obtain crucial insights in several domains such as cancer prediction [@taylor2009dynamic], epidemiology [@gross2006epidemic], organizational sociology [@gulati1999interorganizational], molecular biology [@eisenberg2000protein], information spread on social networks [@lerman2010information] amongst others.

In this work, our focus is on providing the ability to analyze and to reason over the entire history of the changes to a graph. There are many different types of analyses of interest. For example, an analyst may wish to study the evolution of well-studied static graph properties such as centrality measures, density, conductance, etc., over time. Another approach is through the search and discovery of temporal patterns, where the events that constitute the pattern are spread out over time. Comparative analysis, such as juxtaposition of a statistic over time, or perhaps, computing aggregates such as *max* or *mean* over time, possibly gives another style of knowledge discovery into temporal graphs. Most of all, a primitive notion of just being able to access past states of the graphs and performing simple static graph analysis, empowers a data scientist with the capacity to perform analysis in arbitrary and unconventional patterns.

Supporting such a diverse set of temporal analytics and querying over large volumes of historical graph data requires addressing several data management challenges. Specifically, there is a want of techniques for storing the historical information in a compact manner, while allowing a user to retrieve graph snapshots as of any time point in the past or the evolution history of a specific node or a specific neighborhood. Further the data must be stored and queried in a distributed fashion to handle the increasing scale of the data. We must also develop an expressive, high-level, easy-to-use programming framework that will allow users to specify complex temporal graph analysis tasks, while ensuring that the specified tasks can be executed efficiently in a data-parallel fashion across a cluster.

In this paper, we present a graph data management system, called [*Historical Graph Store (HGS)*]{}, that provides an ecosystem for managing and analyzing large historical traces of graphs. HGS consists of two key distinct components. First, the [*Temporal Graph Index (TGI)*]{}, is an index that compactly stores the entire history of a graph by appropriately partitioning and encoding the differences over time (called [*deltas*]{}). These deltas are organized to optimize the retrieval of several temporal graph primitives such as neighborhood versions, node histories, and graph snapshots. TGI is designed to use a distributed key-value store to store the partitioned deltas, and can thus leverage the scalability afforded by those systems (our implementation uses Apache Cassandra[^1] key-value store). TGI is a tunable index structure, and we investigate the impact of tuning the different parameters through an extensive empirical evaluation. TGI builds upon our prior work on DeltaGraph [@icdepaper], where the focus was on retrieving individual snapshots efficiently; we discuss the differences between the two in more detail in Section \[sec:tgi\].

The second component of HGS is a *Temporal Graph Analysis Framework (TAF)*, which provides an expressive library to specify a wide range of temporal graph analysis tasks and to execute them at scale in a cluster environment. The library is based on a novel set of *temporal graph operators* that enable a user to analyze the history of a graph in a variety of manners. The execution engine itself is based on Apache Spark [@zaharia2010spark], a large-scale in-memory cluster computing framework.

[ ]{} The rest of the paper is organized as follows. In Section \[sec:related\], we survey the related work on graph data stores, temporal indexing, and other topics relevant to the scope of the paper. In Section \[sec:overview\], we provide a sketch of the overall system, including key aspects of the underlying components. We then present the Temporal Graph Index and the Temporal Graph Analytics Framework in detail in Section \[sec:tgi\] and Section \[sec:taf\], respectively. In Section \[sec:experiments\], we provide an empirical evaluation of the various system components such as the graph retrieval, scalability of temporal analytics, etc. We conclude with a summary and a list of future directions in Section \[sec:conclusion\].

Related Work {#sec:related}
============

In the recent years, there has been much work on graph storage and graph processing systems and numerous systems have been designed to address various aspects of graph data management. Some examples include Neo4J, AllegroGraph [@aasman2006allegro], Titan[^2], GBase [@kang2011gbase], Pregel [@pregel], Giraph, GraphChi [@kyrola2012graphchi], GraphX [@graphx], GraphLab [@distgraphlab], and Trinity [@shao2013trinity]. These systems use a variety of different models for representation, storage, and querying, and there is a lack of standardized or widely accepted models for the same. Most graph querying happens through programmatic access to graphs in languages such as Java, Python or C++. Graph libraries such as Blueprints[^3] provide a rich set of implementations for graph theoretic algorithms. SPARQL [@perez2006semantics] is a language used to search patterns in linked data. It works on an underlying RDF representation of graphs. T-SPARQL [@grandi2010t] is a temporal extension of SPARQL. He et al. [@he:sigmod08], provide a language for finding sub-graph patterns using a graph as a query primitive. Gremlin[^4] is a graph traversal language over the property graph data model, and has been adopted by several open-source systems. For large-scale graph analysis, perhaps the most popular framework is the vertex-centric programming framework, adopted by Giraph, GraphLab, GraphX, and several other systems; there have also been several proposals for richer and more expressive programming frameworks in recent years. However, most of these prior systems largely focus on analyzing a single snapshot of the graph data, with very little support for handling dynamic graphs, if any.

A few recent papers address the issues of storage and retrieval in dynamic graphs. In our prior work, we proposed DeltaGraph [@icdepaper], an index data structure that compactly stores the history of all changes in a dynamic graph and provides efficient snapshot reconstruction. G\* [@gstar] stores multiple snapshots compactly by utilizing commonalities. Chronos [@hant2014chronos; @immortalgraph] is an in-memory system for processing dynamic graphs, with objective of shared storage and computation for overlapping snapshots. Ghrab et al. [@ghrab2013analytics] provide a system of network analytics through labeling graph components. Gedik et al. [@6702469], describe a block-oriented and cache-enabled system to exploit spatio-temporal locality for solving temporal neighborhood queries. Koloniari et al. also utilize caching to fetch selective portions of temporal graphs they refer to as partial views [@koloniari2013partial]. LLAMA [@llama] uses multiversioned arrays to represent a mutating graph, but their focus is primarily on in-memory representation. There is also recent work on streaming analytics over dynamic graph data [@kineograph; @graphinc], but it typically focuses on analyzing only the recent activity in the network (typically over a sliding window). Our work in this paper focuses on techniques for a wide variety of temporal graph retrieval and analysis on entire graph histories.

Temporal graph analytics is an area of growing interest. Evolution of shortest paths in dynamic graphs has been studies by Huo et al. [@huo2014efficient], Ren et al. [@RenEvolvGraph11], and Xuan et al. [@xuan2003computing]. Evolution of community structures in graphs has been of interest as well [@asur2009event; @berger2006framework; @greene2010tracking; @tang2008community]. Change in page rank with evolving graphs [@desikan2005incremental; @bahmani2010fast], and the study of change in centrality of vertices, path lengths of vertex pairs, etc. [@pan2011path], also lie under the larger umbrella of temporal graph analysis. Ahn et al. [@ahn2014task] provide a taxonomy of analytical tasks over evolving graphs. Barrat et al. [@barrat2008dynamical], provide a good reference for studying several dynamic processes modeled over graphs. Our system significantly reduces the effort involved in building and deploying such analytics over large volumes of graph data.

Temporal data management for relational databases was a topic of active research in the 80s and early 90s. Snapshot index [@Tsotras1995] is an I/O optimal solution to the problem of snapshot retrieval for transaction-time databases. Salzberg and Tsotras [@Salzberg1999] present a comprehensive survey of temporal data indexing techinques, and discuss two extreme approaches to supporting snapshot retrieval queries, referred to as the *Copy* and *Log* approaches. While the copy approach relies on storing new copies of a snapshot upon every point of change in the database, the log approach relies on storing everything through changes. Their hybrid is often referred to as the *Copy+Log* approach. We omit a detailed discussion of the work on temporal databases, and refer the interested reader to a representative set of references [@Bolour92; @DBLP:conf/sigmod/SnodgrassA85; @Ozsoyoglu1995; @Tansel1993; @date2002temporal; @tsql2; @Salzberg1999]. Other data structures, such as Interval Trees [@Arge1996] and Segment trees [@Blankenagel1994] can also be used for storing temporal information. Temporal aggregation in scientific array databases [@soroush2013time] is another related topic of interest, but the challenges there are significantly different.

Overview {#sec:overview}
========

In this section, we introduce key aspects related to HGS. We begin with the data model, followed by the key challenges and concluding with an overview of the system.

Data Model
----------

Under a discreet notion of time, a time-evolving graph $G^T=(V^T,E^T)$ may be expressed as a collection of graph *snapshots* over different time points, $\{G^0 = (V^0, E^0), G^1, \dots, G^t \}$. The vertex set $V^i$ for a snapshot consists of a set of vertices (nodes), each of which has a unique identifier, and an arbitrary number of key-value attribute pairs. The edge sets $E^i$ consist of edges that each contain references to two valid nodes in the corresponding vertex set $V^i$, information about the direction of the edge, and an arbitrary list of key-value attribute pairs. A temporal graph can also be equivalently described by a set of changes to the graph over time. We call an atomic change at a specific timepoint in the graph an *event*. The changes could be structural, such as the addition or the deletion of nodes or edges, or be related to attributes such as an addition or a deletion or a change in the value of a node or an edge attribute. These approaches as well as certain hybrids have been used in the past for the physical and logical modeling of temporal data. Our approach to temporal processing in this paper is best described using a *node-centric* logical model, i.e., the historical graph is seen as a collection of evolving vertices over time; the edges are considered as attributes of the nodes.

![The scope of temporal graph analytics can be represented across two different dimensions - time and entity. The chart lists retrieval tasks (black), graph operations (red), example queries (magenta) at different granularities of time and entity size.[]{data-label="fig:entity-time"}](entity-time1.pdf){width="\linewidth"}

Challenges
----------

The nature of data management tasks in historical graph analytics can be categorized based on the scope of analysis using the dual dimensions of *time* and *entity* as illustrated with examples in Figure \[fig:entity-time\]. The temporal scope of an analysis task can range from a single point in time to a long interval; the entity scope can range from a single node to the entire graph. While the diversity of analytical tasks provides a potential for a rich set of insights from historical graphs, it also poses several challenges in constructing a system that can perform those tasks. To the best of our knowledge, none of the existing systems address a majority of those challenges that are described below:

[ ]{} An natural tradeoff between index size and access latencies can be seen in the Log and Copy approaches for snapshot retrieval. Log requires minimal information for encoding the graph’s history, but incurs large reconstruction costs. Copy, on the other hand, provides direct access, but at the cost of excessive storage. The desirable index should consume space of the order of Log index but provide near direct access like Copy.

[ ]{} For [*point*]{} access such as past snapshot retrieval, a time-centric indexing such as DeltaGraph or Copy+Log is suitable. However, for version retrieval tasks such as retrieving a [*node’s history*]{}, entity-centric indexing is the correct choice. Neither of the indexing approaches, however, are feasible in the opposite scenarios. Given the diversity of access needs, we require an index that works well with both styles of lookup at the same time.

[ ]{} Query latencies for a graph also depends on the size of chunks in which the data is indexed. While larger granularities of storage incur wasteful data read for “node retrieval”, a finely chunked graph storage would mean higher number of lookups and aggregation for a 2-hop neighborhood lookup. The physical and logical arrangement of data should take care of access needs of queries of all granularities.

[ ]{} It is evident that graph partitioning is inevitable in the storage and processing of large graphs. However, finding the appropriate strategy to maintain workable partitioning on a constantly [*changing*]{} graph is another challenge while designing a historical graph index.

[ ]{} A platform for expressing a wide variety of historical graph analytics requires an appropriate amalgam of temporal logic and graph theory. Additionally, utilizing a vast body of existing tools in network science is an engineering challenge and opportunity.

[ ]{} Parallelization is the key to scale up analytics for large network datasets. It is essential that the underlying data-representations and operators in the analytical platform be designed for parallel computing.

System Overview
---------------

Figure \[fig:overview\] shows the architecture of our proposed Historical Graph Store. It consists of two main components:

[ ]{} records the entire history of a graph compactly while enabling efficient retrieval of several temporal graph primitives. It encodes various forms of differences (called *deltas*) in the graph, such as atomic events, changes in subgraphs over intervals of time, etc. It uses specific choices of graph partitioning, data replication, temporal compression and data placement to optimize the graph retrieval performance. TGI uses the Apache Cassandra distributed key-value store as the backend to store the deltas. In Section \[sec:tgi\], we describe the design details of TGI and the access algorithms. [ ]{} provides a *temporal node*-centric abstraction for specifying and executing complex temporal network analysis tasks. We provide a Java and Python based library to specify the retrieval, computation and analysis on a *set of (temporal) nodes (SoN)*. Computational scalability is achieved by distributing tasks by node and time. TAF is built on top of Apache Spark for supporting scalable, in-memory, cluster computation and provides an option to utilize GraphX for static graph computation. In Section \[sec:taf\], we describe the details of the library, query processing, parallel data fetch aspects of the system, along with a few examples of analytics.

![System Overview[]{data-label="fig:overview"}](TGIFoverview.pdf){width="\linewidth"}

Temporal Graph Index {#sec:tgi}
====================

In this section, we investigate the issue of indexing temporal graphs. First, we introduce a *delta framework*[^5] to define any temporal index as a set of different changes or *deltas*. Using this framework, we are able to qualitatively compare the access costs and sizes of different alternatives for temporal graph indexing, including our proposed approach. We then present the Temporal Graph Index (TGI), that stores the entire history of a large evolving network in the cloud, and facilitates efficient parallel reconstruction for different graph primitives. TGI is a generalization of both entity and time-based indexing approaches and can be tuned to suit specific workload needs. We claim that TGI is the minimal index that provides efficient access to a variety of primitives on a historical graph, ranging from past snapshots to versions of a node or neighborhood. We also describe the key partitioning strategies instrumental in scaling to large datasets across a cloud storage.

Preliminaries
-------------

We start with a few preliminary definitions that help us formalize the notion of the delta framework.

A *static node* refers to the state of a vertex in a network at a specific time, and is defined as a *set* containing: (a) *node-id*, denoted *I* (an integer), (b) an edge-list, denoted *E* (captured as a set of node-ids), (c) attributes, denoted *A*, a map of key-value pairs.

A *static edge* is defined analogously, and contains the node-ids for the two endpoints and the edge direction in addition to a map of key-value pairs. Finally, a *static graph component* refers to either a static edge or a static node.

\[Delta\] A Delta ($\Delta$) refers to either: (a) a static graph component (including the empty set), or (b) a difference, sum, union or intersection of two deltas.

\[Cardinality and Size\] The cardinality\
and the size of a $\Delta$ are the unique and total number of static node or edge descriptions within it, respectively.

\[$\Delta$ Sum\] A sum (+) over two deltas, $\Delta_1$ and $\Delta_2$, i.e., $\Delta_s = \Delta_1 + \Delta_2$ is defined over graph components in the two deltas as follows: (1) $\forall  gc_1 \in \Delta_1$, if $\exists gc_2 \in \Delta_2\ s.t.\ gc_1.I = gc_2.I$, then we add $gc_2$ to $\Delta_s$, (2) $\forall gc_1 \in \Delta_1\ s.t.\ \nexists gc_2 \in \Delta_2\ s.t.\ gc_1.I = gc_2.I$, we add $gc_1$ to $\Delta_s$, and (3) analogously the components present only in $\Delta_2$ are added to $\Delta_s$.

Note that: $\Delta_1 + \Delta_2 = \Delta_2 +\Delta_1$ is not necessarily true due the order of changes. We also note that: $\Delta_1 + \emptyset = \Delta_1$, and $(\Delta_1 + \Delta_2) + \Delta_3 = \Delta_1 + (\Delta_2 + \Delta_3)$. Analogously, difference(-) is defined as a set difference over different components of the two deltas. $\Delta_1 - \phi = \Delta_1$ and $\Delta_1 - \Delta_1 = \phi$, are true, while, $\Delta_1 - \Delta_2 = \Delta_2 - \Delta_1$, does not necessarily hold.

\[$\Delta$ Intersection\] An intersection of two $\Delta$s is defined as a set intersection over the the components of two deltas. $\Delta_1 \cap \phi = \phi$, is true for any delta. Similarly, union of two deltas $\Delta_{\cup} = \Delta_1 \cup \Delta_2$, consists of all elements from $\Delta_1$ and $\Delta_2$. The following is true for any delta: $\Delta_1 \cup \phi = \Delta_1$.

Next we discuss and define some specific types of $\Delta$s:

An *event* is the smallest change that happens to a graph, i.e., addition or deletion of a node or an edge, or a change in an attribute value. An event is described around one time point. As a $\Delta$, an event concerning a graph component $c$, at time point $t_e$, is defined as the difference of state of $c$ at and before $t_e$, i.e., $\Delta_{event}(c,t_e) =  c(t_e) - c(t_e-1)$.

An *eventlist* delta is a chronologically sorted set of event deltas. An eventlist’s scope may be defined by the time duration, $(t_s,t_e]$, during which it defines all the changes that happened to the graph.

An *partitioned eventlist* delta is an eventlist constrained by the scope of a set of nodes (say a set of nodes, $\mathcal N = \{N_1, N_2, \dots\})$ apart from the time range constraint $(t_s,t_e]$.

\[Snapshot\] A snapshot, $\mathcal G^{t_a}$ is the state of a graph $\mathcal G$ at a time point $t_a$. As a $\Delta$, it is defined as the difference of the state of the graph at $t_a$ from an empty set, $\Delta_{snapshot}({\mathcal G}, t_a)= G(t_a)-G(-\infty)$.

\[Partitioned Snapshot\] A partitioned snapshot is a subset of a snapshot. It is identified by a subset of all nodes, $\mathcal P$ in graph, $\mathcal G$ at time, $t_a$. It consists of the state of all nodes at time $t_a$ and all the edges whose at least one of the end points lies in $\mathcal P$ at time, $t_a$.

Prior Techniques
----------------

The prior techniques for temporal graph indexing use changes or differences in various forms to encode time-evolving datasets. We can express them in the $\Delta$ framework as follows. The [***Log***]{} index is equivalent to a set of all [*event*]{} deltas (equivalently, a single [*eventlist*]{} delta encompassing the entire history). The [***Copy+Log***]{} index can be represented as combination of: (a) a finite number of distinct [*snapshot*]{} deltas, and (b) [*eventlist*]{} deltas to capture the change between successive snapshots. Although we are not aware of a specific proposal for a [***vertex-centric***]{} index, however, a natural approach would be to maintain a set of [*partitioned eventlist*]{} deltas, one for each node (with edge information replicated with the endpoints). The [***DeltaGraph***]{} index, proposed in our prior work, is a tunable index with several parameters. For a typical setting of parameters, it can be seen as equivalent to taking a Copy+Log index, and replacing the [*snapshot*]{} deltas in it with another set of deltas constructed hierarchically as follows: for every $k$ successive [*snapshot*]{} deltas, replace them with a single delta that is the intersection of those deltas and a set of difference deltas from the intersection to the original snapshots, and recursively apply this till you are left with a single delta.

Table \[tab:deltacompare\] estimates the cost of fetching different graph primitives as the number and the cumulative size of deltas that need to be fetched for the different indexes. The first column shows an estimate of the total storage space, which varies considerably across the techniques.

  -------------- --------------------- --------------------------- --------------------- --------------------------------- --------------------- --------------------------- --------------------- --------------------------- --------------------- --------------------------- ---------------------
                         Index                                                                                                                                                                                                                                                   
                         Size           $\sum_{\Delta}{|\Delta|}$   $\sum_{\Delta}{1} $      $\sum_{\Delta}{|\Delta|}$      $\sum_{\Delta}{1} $   $\sum_{\Delta}{|\Delta|}$   $\sum_{\Delta}{1} $   $\sum_{\Delta}{|\Delta|}$   $\sum_{\Delta}{1} $   $\sum_{\Delta}{|\Delta|}$   $\sum_{\Delta}{1} $
       Log               $|G|$                    $|G|$               $|G| \over |E|$                  $|G|$                  $|G| \over |E|$               $|G|$               $|G| \over |E|$               $|G|$               $|G| \over |E|$               $|G|$               $|G| \over |E|$
       Copy             $|G|^2$                   $|S|$                     $1$                        $|S|$                        $1$                   $|S||G|$                   $|G|$                    $|S|$                     $1$                   $|S||G|$                   $|G|$
     Copy+Log     ${|G|^2 \over |E|}$           $|S|+|E|$                   $2$                      $|S|+|E|$                       2                      $|G|$               $|G| \over |E|$             $|S|+|E|$                    2                      $|G|$               $|G| \over |E|$
   Node Centric         $2|G|$                   $2.|G|$                   $|N|$                       $|C|$                        $1$                     $|C|$                      1                    $|R|.|V|$                  $|R|$                  $|R|.|V|$                  $|R|$
    DeltaGraph          $X_1^*$                h.$|S|+|E|$                 $2h$                     $h.|S|+|E|$                    $2h$                     $|G|$               $|G| \over |E|$           $h.(|S|+|E|)$                 2h                      $|G|$               $|G| \over |E|$
       TGI            $X_2^{**}$               h.$|S|+|E|$                 $2h$           ${h.|S| \over p}+{|E| \over p}$          $2h$            $|V|(1+{|S| \over p})$           $|V|+1$          ${h.(|S|+|E|) \over p}$            2h             $|V|(1+{|S| \over p})$           $|V|+1$
  -------------- --------------------- --------------------------- --------------------- --------------------------------- --------------------- --------------------------- --------------------- --------------------------- --------------------- --------------------------- ---------------------

Temporal Graph Index: Definition
--------------------------------

Given the above formalism, a Temporal Graph Index for a graph $\mathcal G$ over a time period $T=[0, t_c]$ is described by a collection of different $\Delta$s as follows:

-   Partitioned Eventlists: A set of partitioned eventlist $\Delta$s, $\{E_{tp}\}$, where $E_{tp}$ captures the changes during the time interval $t$ belonging to partition $p$.

-   Derived Partitioned Snapshots: Consider $r$ distinct time points, $t_i$, where $1 \le i \le r$, $t_i \in T$, For each $t_i$, we consider $l$ partition $\Delta$s, $P_{j}^i$, $1< j <l$, such that $\cup_{j} P_{j}^i = {\mathcal G^{t_i}}$. There exists a function that maps any node-id(I) in $\mathcal G^{t_i}$ to a unique partition-id($P_j^i$), $f_i: I \rightarrow P_j^{i}$. With a collection of $P_j^{i}$ over $T$ as leaf nodes, we construct a hierarchical tree structure where a parent is the intersection of children deltas. The difference of each parent from its child delta is called as a [*derived partitioned snapshot*]{} and is explicitly stored. Note that $P_j^{i}$’s are not explicitly stored. This is the same as DeltaGraph, with the exception of partitioning.

-   Version Chain: For all nodes $\mathcal N$ in the graph $\mathcal G$, we maintain a chronologically sorted list of pointers to all the references for that node in the delta sets described above (a and b). For a node $I$, this is called a *version chain*($VC_I$).

In short, the TGI stores *deltas* or *changes* in three different forms, as follows. The first one is the atomic changes in a chronological order through partitioned eventlists. This facilitates direct access to the changes that happened to a part or whole of the graph at specified points in time. Secondly, the state of nodes at different points in time is stored indirectly in form of the derived partitioned snapshot deltas. This facilitates direct access to the state of a neighborhood or the entire graph at a given time. Thirdly, a meta index stores node-wise pointers to the list of chronological changes for each node. This gives us a direct access to the changes occurring to individual nodes. Figure \[fig:tgi-stuff\](a) shows the arrangement of eventlist, snapshot and derived snapshot partitioned deltas. Figure \[fig:tgi-stuff\](b) shows a sample version chain.

TGI utilizes the concept of temporal consistency which was optimally utilized by DeltaGraph. However, it differs from DeltaGraph in two major ways. First, it uses a partitioning for eventlists, snapshots or deltas instead of a large monolithic chunks. Additionally, it maintains a list of version chain pointers for each node. The combination of these two novelties along with DeltaGraph’s temporal compression generalizes the notion of entity-centric and time-centric indexing approaches in an efficient way. This can be seen by the qualitative comparison shown in Table \[tab:deltacompare\] as well as empirical results in Section \[sec:experiments\].

\

TGI: Design and Architecture
----------------------------

In the previous subsection, we presented the logical description of TGI. We now describe the strategies for physical storage on a cloud which enables high scalability. In a distributed index, we desire that all graph retrieval calls achieve maximum parallelization through equitable distribution. A distribution strategy based on pure node-based key is good idea for snapshot style access, however, it is bad for a subgraph history style of access. A pure time-based key strategy on the other hand, has complementary qualities and drawbacks. An important related challenge for scalability is dealing with two different skews in a temporal graph dataset – temporal and topological. They refer to the uneven density of graph activity over time and the uneven edge density across regions of the graph, respectively. Another important aspect to note is that for a retrieval task, it is desirable that all the required micro-deltas on a particular machine be proximally located to minimize latency of lookups[^6].

Based on the above constraints and desired properties, we describe the physical layout of TGI as follows:

1.  The entire history of the graph is divided into *time spans*, keeping the number of changes to the graph consistent across different time spans, $f_t: e.time \rightarrow tsid$, where $e$ is the event and $tsid$ is the unique identifier for the time span. This is illustrated in Figure \[fig:timespans\].

2.  A graph at any point is horizontally partitioned into a fixed number of *horizontal partitions* based upon a random function of the node-id, $f_h: nid \rightarrow sid$, where $nid$ is the node-id and $sid$ is unique identifier of for the horizontal partition.

3.  The micro-deltas (including eventlists) are stored as a key-value pairs, where the delta-key is composed of\
    $\{tsid, sid, did, pid\}$, where $did$ is a delta-id, and $pid$ is the partition-id of the micro-delta.

4.  The placement-key is defined as a subset of the composite deltas key described above, as $\{tsid, sid\}$, which defines the chunks in which data is placed across a set of machines on a cluster. A combination of the $tsid$ and $sid$ ensure that a large fetch task, whether snapshot or version oriented, seeks data distributed across the cluster and not just one machine.

5.  The micro-deltas are clustered by the delta key. The given order of the delta-key besides the placement-key elements, means that all the micro-partitions of a delta are stored contiguously, which makes it efficient to scan and read all micro-partitions belonging to a delta in a snapshot query. On the other hand, if the order of $did$ and $pid$ is reversed, it makes fetching a micro-partition across different deltas more efficient.

![The graph history is divided into non-overlapping periods of time. Such division is based on time intervals after which the locality-based graph partitioning is updated. It is also used as a partial key for data chunking and placement.[]{data-label="fig:timespans"}](TimeSpans.pdf){width="\linewidth"}

Irrespective of a temporal or a topological skew in the graph, the index is spread out across a cluster in a balanced manner. This also makes it possible to fetch the graph primitives of large sizes in a naturally parallel manner. For instance, a snapshot query would demand all micro-partitions for a specific set of deltas in a particular timespan across all horizontal partitions. Given an equitable distribution of the deltas across all machines of a cluster, we retrieve the data in parallel on each storage machine, without a considerable skew.

[ ]{} TGI uses Cassandra for its delta storage. There are 5 tables that contain TGI data and metadata:\
(1) `Deltas(tsid, sid, did, pid, dval)` table stores the deltas as described above, where `dval` contains serialized value of the micro-delta as a binary string.\
(2) `Versions(nid, vchain)` consists of each node’s version chain as a hash-table with keys for each timespan.\
(3) `Timespans(tsid, start, end, checkpts, k, df)` stores, for each timespan, start and end times, a list of snapshot checkpoints, and arity.\
(4) `Graph(start, end, events, tscount, gtype)` contains global information about the graph and TGI.\
(5) `Micropartitions(nid, tsid, pid)` contains micro-delta partitioning information about nodes. It is not utilized in case of random partitioning.\
The graph construction and fetching modules are written in Python, using Pickle and Twisted libraries for serialization and communication.

[ ]{} TGI architecture can be seen in Figure \[fig:tgi-arch\], where *Query Mananger (QM)* is responsible for planning, dividing and delegating the query to one or more *Query Processors (QP)*. The QPs query the datastore in parallel and process the raw deltas into the required result. Depending on the query specification, the distributed result is either aggregated at a particular QP (the QM) or returned to the client which made the request without aggregation. The *Index Manager* is responsible for the construction and maintenance activities of the index. The cloud represents the distributed datastore.

[ ]{} The construction process involves three different stages. First, we analyze the input data using the index construction parameters including the timespan length (ts), number of horizontal partitions (ns), number of likely datastore nodes (m), eventlist size (l), and micro-delta partition size (psize). In the second stage, the input data is split into horizontal partitions. In the third stage, parallel construction workers of the IM work on separate horizontal partitions, and build the index, a time span at a time. The process of construction of each timespan is similar to that of DeltaGraph, albeit more fine-grained due to delta partitioning and version chain construction as well. The TGI accepts updates of events in batches of timespan length. The update process involves creating an independent TGI with the new events, and merging it with the original TGI. The merger of TGIs involves updates of corresponding deltas, VC index and the metadata.

Dynamic Graph Partitioning
--------------------------

Partitioning deltas into micro-deltas is an essential aspect of TGI and provides cheaper access to subgraph elements when compared to DeltaGraph or similar indexes. In a time-evolving graph, however, the size and topology of the graph change with time. The key is to keep the size of each micro-delta (and each micro-eventlist) about the same and bounded by a number that dictates the latency for fetching a node or neighborhood. The two traditional approaches to partitioning a static graph are random (node-id hash-based) or locality-based (min-cut max-flow) partitioning. Random partitioning is simpler and involves minimal bookkeeping. However, since it loses locality, it is unsuitable for neighborhood-level granularity access. Locality-aware partitioning, on the other hand, preserves locality but incurs extra bookkeeping in form of a {node-id:partition-id} map. TGI is designed to work with either configuration as desired, as well as different partition size specifications. TGI also supports replication of edge-cuts for further speed up of 1-hop neighborhoods. It uses a separate [*auxiliary micro-delta*]{} besides each micro-delta to store the replication, thereby preventing extra read cost for snapshot or node centric queries. This is illustrated in Figure \[fig:micrdeltasaux\].

Locality-aware partitioning, however, faces an additional challenge with time-evolving graphs. With the change in size and topology of a graph, a partitioning deemed good (with respect to locality) at an instant may cease to be good at a later time. A probable approach of frequent repartitioning over time would maintain partitioning quality, but leads to excessive amounts of bookkeeping, which in turn leads to degradation of performance while accessing different node or neighborhood versions. Maintaining and looking up that map as frequently as the changes in the graph is highly inefficient. Hence, we divide the history of the graph into [*time spans*]{}, where we keep the partitioning consistent within each time span, but perform it afresh it at the beginning of each new time span. This gives rise to two problems, described briefly as follows. Firstly, given a graph over time span, $T \in [t_s, t_e)$, find the graph partitioning that minimizes the edge cuts across all time points combined. Secondly, to determine the appropriate points for the end of a time span and the beginning of a new one, with respect to over all query performance. We discuss these problems below.

Static graph partitioning for an undirected and unweighted graph $G=(V, E)$ into $k$ partitions is defined as follows. Each node $v_i \in V$ is assigned a partition set $P_r$ such that $0 \le r < k$. The constraint is that $ {\left\lfloor |V| \over k \right\rfloor} \le |P_r| \le {\left\lceil |V| \over k \right\rceil}$, i.e., the partitions are more or less equal in size. The number of edge cuts across partitions are intended to be minimized, i.e., a count of all edges whose end points lie in different partitions. For a weighted graph, the edge cut cost is counted as the sum of the edge weights, which pushes stronger relationships (with higher edge weights) to be preferred for being in the same partition over the lesser weighted ones. Also, in case of a node weighted graph, the partition set count can be determined using the node weight. Different graph partitioning algorithms work under these constraints using one or the other heuristic, as described before.

For a dynamic graph partitioning, we consider an edge and node weighted, undirected time evolving graph, without the loss of generality. Consider the following: graph $G^T=(V^\tau, E^\tau, W_E^\tau, W_N^\tau)$ where, $\tau \in [t_s, t_e)$, is the time range for which we find a single partitioning; $V^\tau, E^\tau, W_E^\tau, W_N^\tau$ are the set of vertices, edges, edge weights, node weights over time $\tau$, respectively. Our partitioning strategy involves projecting the graph over time range $T$ to a single point in time using a *time collapsing* function $\Omega$, there by reducing the graph $G^\tau$ to a static graph, $G_\tau = \Omega(G^\tau)$. The constraint on function $\Omega$ is that $G_\tau$ must contain all the vertices that existed in $G^\tau$ at least once in $G^\tau$. Using $G_\tau$, we can employ static graph partitioning to find a suitable partitioning technique in the following manner.

The choice of $\Omega$ function determines how well the $G_T$ is a representation of $G^\tau$. Let us consider a few different options. (1) Median: consider the time point $t$ which is the median of the end points of $\tau$. The edges and weights in $G_\tau$ are the edge weights in $G^t$. (2) Union-Max: for an edge that existed at any time in $G^\tau$, we include it in $G_\tau$ such that its weight is the maximum value from all time points in $G^T$. (3) Union-Mean: for an edge that existed at any time in $G^\tau$, we include it in $G_\tau$, where its weight is the weighted average (time fraction) of the edge weights in $G^\tau$. Non existence of an edge during a time period counts as a $0$ contribution for the respective time period. (4). For any of the cases above, the node weight, $w_n$, can be defined independently of the edge set and edge weights. We consider three options as follows. (1) $w_n = 1$ for each nodes $n$ in $G_\tau$; (2) $w_n=degree(n)$ for each node $n$ in $G_T$; (3) $w_n={\bar{degree(n)}}$, i.e., average degree over $\tau$.

Given these different heuristic combinations, we plan to study their empirical behavior and use the apparently most suitable one for TGI partitioning. The default TGI partitioning uses Union-Max for edge weights and uniform node weights.

We argue that this style of partitioning that involves first projecting a temporal graph to a static one, followed by conventional forms of static graph partitioning, is better than other conceivable alternatives. One such alternative way of doing it is to determine the partitioning at different time points in $\tau$, say $P^\tau$ and then reducing $P^\tau$ to $P_\tau$, a single partitioning scheme. This approach has the following major disadvantages. Firstly, the output partitions from a a static graph partitioning algorithm for two versions of graph $G^\tau$, say $G^1$ and $G^2$ are not aligned, even when the two snapshots are similar to a large extent. This is attributed to some degree of randomness associated with graph partitioning algorithms. This makes it infeasible to combine $P^1$ and $P^2$ in to a single result. Secondly, this approach is much more expensive compared to our approach, because it involves computing $\tau$ orders of partitions. Another alternative approach is to use one of the *online graph partitioning* algorithms, which updates a partition set for a graph upon a small change in the graph. However, the output of such an approach only gives us partitioning schemes at different time points. The partitions across time are better aligned to each other than the previous approach, but we would still need to compute a combined partitioning from all available partitions, and the notion of time collapsing is inevitable. Secondly, the partitioning results from incremental graph partitioning are often inferior compared to the batch mode of partitioning for obvious reasons.

Determining the appropriate number and the exact boundaries of time-spans is another important issue. The need for creating higher number of time-spans and hence reducing the duration of a time-span is to maintain healthier partitioning. Let the hit taken on query latencies (assuming a certain query load Q) due to a subpar snapshot partitioning be, $f(T)$. This hit is generally incurred on k-hop queries, without replication, due to higher number of micro-delta seeks. In case of replication across partitions, the degree of replication increases with inferior partitioning, and leads to indirect impact on query latencies. On the other hand, there is need to create longer time-spans because the version queries require multiple micro-deltas, at different time points. Higher the changing number of partitions over query’s time interval, say $t$, higher the query latency. Let us say that for an average query time interval (again, as per a specific query load), the gain due to longer time spans is, $g(T)$. The appropriate length of a average time-span hence is the solution of the maxima of $g(T) - f(T)$. In practice, uniform time-span length in numbers of the number of events is perhaps the most convenient. While the models of $f$ and $g$ are complex, a good number for size of $T$ can be observed empirically.

\

Fetching Graph Primitives
-------------------------

We briefly describe access methods for different graph primitives. The algorithms provided here use primitive TGI fetch methods whose description should be self-explanatory from their nomenclature. [ ]{} In snapshot retrieval, the state of a graph at a time point is retrieved. Given a time $t_s$, the query manager locates the appropriate time span $T$ such that $t_s \in T$, within which, it figures out the path from the root of the TGI to the leaf closest to the given time point. All the snapshot deltas, $\Delta_{s1}, \Delta_{s2}, \dots, \Delta_{sm}$, (i.e., all their micro-partitions) along that path and the eventlists from the leaf node to the time point, $\Delta_{e1}, \Delta_{e2}, \dots, \Delta_{en}$ are fetched and merged appropriately as: $\sum_{i=1}^m{\Delta_{si}} + \sum_{i=1}^n{\Delta_{ei}}$ (notice the order). This is performed across different query processors covering the entire set of horizontal partitions. The procedure for snapshot retrieval is specified in Algorithm \[algo:sr\].

$t'\gets {\sf GetNearestPartTime}(t)$ $K\gets {\sf GetNearestPartKeys}(t)$ $D \gets {\sf GetDeltas}(K)$ $g \gets \emptyset$ $g \gets g + d$ $ B \gets {\sf GetEventLists}(t',t)$ $b \gets {\sf FilterByTime}(b, t',t)$ $g \gets g +b$ **return** $g$

[ ]{} Retrieving a node’s history during time interval, $[t_s, te)$ involves finding the state of the graph at point $t_s$, and all changes during the time range $(t_s, t_e)$. The first one is done in a similar manner to snapshot retrieval except the fact that we look up only a specific micro-partition in a specific horizontal partition, that the node belongs to. The second part happens through fetching the node’s version chain to determine its points of changes during the given range. The respective eventlists are fetched and filtered for the given node. The procedure for node-history retrieval is specified in Algorithm \[algo:nh\].

$C\gets {\sf GetVC}(I)$ $ C \gets {\sf FilterByTime}(C, t_s, t_e)$ $D \gets {\sf GetDeltas}(C)$

$I_N \gets \emptyset$ $D \gets {\sf FilterByTime}(D,t_s,t_e)$ $D \gets {\sf FilterById}(D,I)$ $I_N \gets I_N \cup d$ **return** $I_N$

[ ]{} In order to retrieve the k-hop neighborhood of a node, we can proceed in two possible ways. One of them is to fetch the whole graph snapshot and filter the required subgraph. The other is to fetch the given node, and then determine its neighbors, fetch them, and recurse. It is easy to see that the performance of the second method will deteriorate fast with growing $k$. However for lower values, typically $k \le 2$, the latter is faster or at least as good, especially if we are using neighborhood replication as discussed in a previous subsection. In case of a neighborhood fetch, the query manager automatically fetches the auxiliary portions of deltas (if they exist), and if the required nodes are found, further lookup is terminated. Two different procedures for fetching a k-hop neighborhood are specified in Algorithm \[algo:khop1\] and Algorithm \[algo:khop2\], respectively.

$g \gets {\sf GetSnapshot}(t)$ $ C \gets \{I\}$ $ R \gets \{I\}$

$C \gets C \cup N$ $S \gets S \cup N$ $g' \gets {\sf FilterByID}(g, C)$ **return** $g'$

$N \gets  {\sf GetNode}(I,t)$ $M \gets { \sf GetNeighbors}(N)$ $G \gets \emptyset$ $L \gets \emptyset$ $N \gets {\sf GetNode(m)}$ $G \gets G + R$ $L \gets L \cup  {\sf GetNeighbors}(m)$

**return** $G$

[ ]{} Neighborhood evolution queries can be posed in two different ways. First, requesting all changes for a described neighborhood, in which case the query manager fetches the initial state of the neighborhood followed by the events indicating the change. Second, requesting the state of the neighborhood at multiple specific time points. This translates to the retrieval of multiple single neighborhoods fetch tasks. Algorithm \[algo:1hophist\] specifies the procedure to fetch one hop neighborhood history. The general k-hop evolution process can be seen as a combination of the 1-hop evolution procedure along with the k-hop (static) neighborhood retrieval.

**return** $G$

Analytics Framework {#sec:taf}
===================

In this section, we describe the *Temporal Graph Analysis Framework (TAF)*, that enables programmers to express and execute complex analytical tasks on time-evolving graphs. We present details of the novel model of computation, including a library of temporal graph operators and operands (exposed through Python and Java APIs); we also present the details of implementation on top of Apache Spark, which enables scalable, parallel, in-memory execution. Finally, we describe TAF’s coordination with TGI to provide a complete ecosystem for historical graph management and analysis.

Temporal Graph Analysis Library {#subsec:tafcore}
-------------------------------

In this section, we describe a set of operators for analyzing large historical graphs. At the heart of this library is a data model where we view the historical graph as a *set of nodes or subgraphs evolving over time*. The choice of temporal nodes as a primitive helps us describe a wide range of fetch and compute operations in an intuitive manner. More importantly, it provides us an abstraction to parallelize computation. The *temporal nodes* and *set of temporal nodes* bear a correspondence to *tuples* and *tables* of the relational algebra, as the basic unit of data and the prime operand, respectively. [ ]{} The two central data types are defined below:

A *temporal node* (NodeT), $ N^T$, is defined as a sequence of all and only the states of a node $ N$ over a time range, $T=[t_s, t_e)$. All the $k$ states of the node must have a valid time duration $T_i$, such that $\cup_i^k T_i = T$ and $\cap_i^k T_i = \phi$.

A *SoN*, is defined as a set of $r$ temporal nodes $\{ N_1^T, N_2^T \dots N_r^T \}$ over a time range, $T=[t_s, t_e)$, as depicted in Figure \[fig:son\].

The [*NodeT*]{} class provides a range of methods to access the state of the node at various time points, including: [getVersions()]{} which returns the different versions of the node as a list of static nodes (NodeS), [getVersionAt()]{} which finds a specific version of the node given a timepoint, [getNeighborIDsAt()]{} which returns IDs of the neighbors at the specified time point, and so on.

A [*Temporal Subgraph (SubgraphT)*]{} generalizes NodeT and captures a sequence of the states of a subgraph (i.e., a set of nodes and edges among them) over a period of time. Typically the subgraphs correspond to $k$-hop neighborhoods around a set of nodes in the graph. An analogous [getVersionAt()]{} function can be used to retrieve the state of the subgraph as of a specific time point as an in-memory [Graph]{} object (the user program must ensure that any graph object so created can fit in the memory of a single machine). A Set of Temporal Subgraphs (SoTS) is defined analogously to SoN as a set of temporal subgraphs.

[ ]{} Below we discuss the important temporal graph algebra operators supported by our system.\

1.  [***Selection***]{} accepts an SoN or an SoTS along with a boolean function on the nodes or the subgraphs, and returns an SoN or SoTS. Selection performs *entity-centric filtering* on the operand, and does not alter temporal or attribute dimensions of the data.\

2.  [***Timeslicing***]{} accepts an SoN or an SoTS along with a timepoint (or time interval) $t$, finds the state of each of individual nodes or subgraphs in the operand as of $t$, and returns it as another SoN or SoTS, respectively (SoN/SoTS can represent sets of static nodes or subgraphs as a well). The operator can accept a list of timepoints as input and return a list.\

3.  [***Graph***]{} accepts an SoN and returns an in-memory Graph object containing the nodes in the SoN (with only the edges whose both endpoints are in the SoN). An optional parameter, $t_p$, may be specified to get a GraphS valid at time $t_p$.\

4.  [***NodeCompute***]{} is analogous to a [*map*]{} operation; it takes as input an SoN (or an SoTS) and a function, and applies the function to all the individual nodes (subgraphs) and returns the results as a set.\

5.  [***NodeComputeTemporal***]{}. Unlike [NodeCompute]{}, this operator takes as input a function that operates on a static node (or subgraph) in addition to an SoN (or an SoTS); for each node (subgraph), it returns a sequence of outputs, one for each different state (version) of that node (or subgraph). Optionally, the user may specify another function (NodeComputeDelta) that operates on the delta between two versions of a node (subgraph), which the system can use to compute the output more efficiently. An optional parameter is a method describing points of time at which computation needs to be performed; in the absence of it, the method will be evaluated at all the points of change.\

6.  [***NodeComputeDelta***]{} operator takes as input: (a) a function that operates on a static node (or subgraph) and produces an output quantity, (b) an SoN (or an SoTS) like\
    [NodeComputeTemporal]{}, (c) a function that operates on the following: a static node (or subgraph), some auxiliary information pertaining to that state of the node (or subgraph), the value of the quantity at that state, and an *update* (event) to it. This operator returns a sequence of outputs, one for each state of the node (or subgraph), similar to\
    [NodeComputeTemporal]{}. However, the method of computation in this method is different because it updates the computed quantity for each version incrementally instead of computing it afresh. An optional parameter is the method describing points of time at which to base the comparison. An optional parameter is a method describing points of time at which computation needs to be performed; in the absence of it, the method will be evaluated at all the points of change.\

7.  [***Compare***]{} operator takes as input two SoNs (or two SoTSs) and a scalar function (returning a single value), computes the function value over all the individual components, and returns the differences between the two as a set of [*(node-id, difference)*]{} pairs. This operator tries to abstract the common operation of comparing two different snapshots of a graph at different time points. A simple variation of this operator takes a single SoN (or SoTS) and two timepoints as input, and does the compare on the timeslices of the SoN as of those two timepoints. An optional parameter is the method describing points of time at which to base the comparison.\

8.  [***Evolution***]{} operator samples a specified quantity (provided as a function) over time to return evolution of the quantity over a period of time. An optional parameter is the method describing points of time at which to base the evolution.\

9.  [***TempAggregation***]{} abstractly represents a collection of temporal aggregation operators such as [Peak]{}, [Saturate]{}, [Max]{}, [Min]{}, and [Mean]{} over a scalar timeseries. The aggregation operations are used over the results of temporal evaluation of a given quantity over an SoN or SoTs. For instance, finding “times at which there was a [*peak*]{} in the network density” is used to find eventful timepoints of high interconnectivity such as conversations in a cellular network, or high transactional activity in a financial network.

System Implementation
---------------------

The library is implemented in Python and Java and is built on top of the Spark API. The choice of Spark provides us with an efficient in-memory cluster compute execution platform, circumventing dealing with the issues of data partitioning, communication, synchronization, and fault tolerance. We provide a GraphX integration for utilizing the capabilities of the Spark based graph processing system for static graphs.

The key abstraction in Spark is that of an RDD, which represents a collection of objects of the same type, stored across a cluster. SoN and SoTS are implemented as RDDs of NodeT and SubgraphT respectively (i.e., as [RDD&lt;NoteT&gt;]{} and [RDD&lt;SubgraphT&gt;]{}). The in-memory graph objects may be implemented using any popular graph representation, specially the ones that support useful libraries on top. We now describe in brief the implementation details for NodeT and SubgraphT, followed by details of the incremental computational operator, and the parallel data fetch operation.

Figure \[fig:taf-code-ex\] shows sample code snippets for three different analytical tasks – (a) finding the node with the *highest clustering coefficient* in a historical snapshot; (b) *comparing different communities* in a network; (c) finding the *evolution of network density* over a sample of ten points.

\
\

[ ]{} A set of temporal nodes is represented with an `RDD` of `NodeT` (temporal node). A temporal node contains the information for a node during a specified time interval. The question of the appropriate physical storage of the `NodeT` (or `SubgraphT`) structure is quite similar to storing a temporal graph on disk such as the one using a DeltaGraph or a TGI, however, in-memory instead of disk. Since NodeT is fetched at query time, it is preferable to avoid creating a complicated index, since the cost to create the index at query time is likely to offset any access latency benefits due to the index. An intuitive guess based upon examination of certain temporal analysis tasks is that its access pattern is most likely going to be in a chronological order, i.e., the query requesting the subsequent versions or changes, in order of time. Hence, we store `NodeT` (and `SubgraphT`) as an initial snapshot of the node (or subgraph), followed by a list of chronologically sorted events. It provides methods such as `GetStartTime()`, `GetEndTime()`, `GetStateAt()`, `GetIterator()`, `Iter- ator.GetNextVersion()`, `Iterator.GetNextEvent()`, and so on. We omit the details of these methods as their functionality is apparent from the nomenclature.

[ ]{} `NodeComputeDelta` evaluates a quantity over each NodeT (or SubgraphT) using two supplied methods, $f()$ which computes the quantity on a state of the node or subgraph, and, $f_\Delta()$, which updates the quantity on a state of the node or subgraph for a given set of event updates. Consider a simple example of finding the fraction of nodes with a specific attribute value in a given `SubgraphT`. If this were to be performed using\
`NodeComputeTemporal`, the quantity will be computed afresh on each new version of the subgraph, which would cost $\mathcal{O}(N.T)$ operations where $N$ is the size of the operand (number of nodes) and $T$ is the number of versions. However, using the incremental computation, each new version can be processed in constant time after the first snapshot, which adds up to, $\mathcal{O}(N + T)$. While performing the incremental computation, the corresponding $f_\Delta ()$ method is expected to be defined so as to evaluate the nature of the event – whether it brings about any changing the output quantity or not, i.e., a scalar change value based upon the actual event and the concerned portions of the state of the graph, and also update the auxiliary structure, if used. Code in Figure \[fig:computedelta\_taf\] illustrates the usage of [NodeComputeTemporal]{} and [NodeComputeDelta]{} in a similar example.

\

Consider a somewhat more intricate example, where one needs to find counts of a small pattern [*over time*]{} on an [SoTS]{}, such as finding the occurrence of a subgraph pattern in the data graph’s history. In order to perform such pattern matching over long sequences of subgraph versions, it is essential to maintain certain inverted indexes which can be looked up to answer in constant time whether an event has caused a change in the answer from a previous state or caused a change in the index itself, or both. Such inverted indexes, quite common to subgraph pattern matching, are required to be updated with every event; otherwise, with every new event update, we would need to look up the new state of the subgraph afresh which would simply reduce it to performing non-indexed subgraph pattern matching over new snapshots of a subgraph at each time point, which is a fairly expensive task. In order to utilize a constantly updated set of indices, the auxiliary information, which is a parameter and a return type for $f_\Delta()$, can be utilized. Note that such an incremental computational operator opens up possibilities for using a considerable amount of algorithmic work available in literature on online and streaming graph query evaluation, respectively, to be applied to historical graph analysis. For instance, there is work on pattern matching in streaming [@wang2009continuous; @gao2014continuous] and incremental computing [@fan2013incremental; @varro2006incremental] contexts, respectively.

[ ]{} In the -oriented operators on an [SoN]{} or an [SoTS]{}, the time points of evaluation, by default, are all the points of change in the given operand. However, a user may choose to provide a definition of which points to select. This can be as simple as returning a constant set of timepoints, or based on a more complex function of the operand(s). Except the [Compare]{} operator, which accepts two operands, other operators allow an optional function, which works on a singe temporal operand; the compare accepts a similar function that operates on two such operands. Two such examples can be seen in Figure \[fig:timepts\_taf\].

\

[ ]{} In a temporal graph analysis task, we first need to instantiate a *TGI connection handler* instance. It contains configurations such as address and port of the TGI *query manager host*, *graph-id*, and a *SparkContext* object. Then, a SON (or SOTS) object is instantiated by passing the reference to the TGI handler, and any query specific parameters (such as k-value for fetching 1-hop neighborhoods with SOTS). The next few instructions specify the semantics of the graph to be fetched from the TGI. This is done through the commands explained in Section \[subsec:tafcore\], such as the `Select`, `Filter`, `Timeslice`, etc. However, the actual retrieval from the index doesn’t happen until the first statement following the specification instructions. A `fetch()` command can be used explicitly to tell the system to perform the fetch operation. Upon the `fetch()` call, the analytics framework sends the combined instructions to the query planner of the TGI, which translates those instructions into an optimal retrieval plan. This prevents the system from retrieving large amounts of data from the index that is a superset of the required information and prune it later.

The analytics engine runs in parallel on a set of machines, so does the graph index. The parallelism at both places speeds up and scales both the tasks. However, if the retrieval graph at the TGI cluster was aggregated at the Query Manager and sent serially to the master of the analytical framework engine after which it was distributed to the different machines on the cluster, it would create a space and time bottleneck at the Query Manager and the master, respectively, for large graphs. In order to bypass this situation, we have designed a parallel fetch operation, in which there is a direct communication between the nodes of the analytics framework cluster and the nodes of the TGI cluster. This happens through a protocol that can be seen in Figure \[fig:parallel-fetch\]. The protocol is briefly described in the following ordered steps:

1.  Analytics query containing fetch instructions is received by the TAF master.

2.  A handshake between the TAF master and TGI query manager is established. The latter receives fetch instructions and the former is made aware of the active TGI query processor nodes.

3.  Parallel fetch starts at the TGI cluster.

4.  The TAF master instantiates a TGIDriver instance at each of its cluster machines wrapped in a RDD.

5.  Each node at the TAF performs a handshake with one or more of the TGI nodes.

6.  Upon completion of fetch at TGI, the individual TGI nodes transfer the SoN to an RDDs on the corresponding TAF nodes.

Experimental Evaluation {#sec:experiments}
=======================

In this section, we empirically evaluate the efficiency of TGI and TAF. To recap, TGI is a persistent store for entire histories of large graphs, that enables fast retrieval for a diverse set of graph primitives – snapshots, subgraphs, and nodes at past time points or across intervals of time. We primarily highlight the performance of TGI across the entire spectrum of retrieval primitives. We are not aware of a baseline that may compete with TGI across all or a substantial subset of these retrieval primitives. Specialized alternatives such as DeltaGraph for snapshot retrieval is highly unsuitable for node or neighbor version retrieval; a version centric index may be specialized for node-version retrieval but is highly unsuitable for snapshot or neighborhood-version style retrieval. Also note that TGI generalizes all the known approaches including those two; using appropriate parameter configurations, it can even converge to any specific alternative. Secondly, we demonstrate the scalability of TGI design through experiments on parallel fetching for large and varying data sizes. Finally, we also report experiments demonstrating computational scalability of the TAF for a graph analysis task, as well as the benefits of our incremental computational operator.

[ ]{} We use four datasets: (1) Wikipedia citation network consisting of 266,769,613 edge addition events from Jan 2001 to Sept 2010. At its largest point, the graph consists of 21,443,529 nodes and 122,075,026 edges; (2) We augment Dataset 1 by adding around 333 million synthetic events which randomly add new edges or delete existing edges over a period of time, making a total of 700 million events; (3) Similarly, we add 733 million events, making the total around 1 billion events; (4) Using a Friendster gaming network snapshot, we add synthetic dates at uniform intervals to 500 million events with a total of approximately 37.5 million nodes and 500 million edges.

Following key parameters that are varied in the experiments: data store machine count ($m$), replication across dataset ($r$), number of parallel fetching clients ($c$), eventlist size ($l$), snapshot or eventlist partition size ($ps$), and Spark cluster size ($m_a$).

We conducted all experiments on an Amazon EC2 cluster. Cassandra ran on machines containing 4 cores and 15GB of available memory. We did not use row caching and the actual memory consumption was much lower that the available limit on those machines. Each fetch client ran on a single core with up to 7.5GB available memory. The machines with TAF nodes running Spark workers ran on a single core and 7.5GB of available memory each.

![Snapshot retrieval times for varying parallel fetch factor ($c$), on Dataset 1; $m=4$; $r=1, ps=500$.[]{data-label="fig:ss1"}](SS267m-MultiCore_4MachDB.pdf){width="\linewidth"}

[ ]{} Figure \[fig:ss1\] shows the snapshot retrieval times for Dataset 1 for different values of the parallel fetch factor, $c$. As we can see, the retrieval cost is directly proportional to the size of the output. Further, using multiple clients to retrieve the snapshots in parallel gives near-linear speedup, especially with low parallelism. This demonstrates that TGI can exploit available parallelism well. We expect that with higher values of $m$ (i.e., if the index were distributed across a larger number of machines), the linear speedup would be seen for larger values of $c$ (this is also corroborated by the next set of experiments). The snapshot retrieval times for dataset 4 can be seen in Figure \[fig:friendsterss\].

Figure \[fig:multss\] shows snapshot retrieval performance for three different sets of values for $m$ and $r$. We can see that while there is no considerable difference in performance across the different configurations, using two storage machines slightly decreases the query latency over using one machine, in the case of a single query client, $c=1$. For higher $c$ values, we see that $m=2$ has a slight edge over $m=1$. Also, the behavior for the two $m=1$ and $m=2;r=2$ cases are quite similar for same $c$ values. However, we observed that the latter case allows a higher possibility of $c$ value whereas the former peaks out at a lower $c$ value. Further, the net effect of Cassandra compression for deltas is negligible for TGI. We omit the detailed points of our investigation, but Figure \[fig:compression\] is representative of the general behavior.

Size of the delta partitions (or the number) affects the performance the snapshot retrieval performance only to a small degree as seen in Figure \[fig:ss-varp\]. This occurs due to a the TGI design which makes sure that all the partitions of a delta (micro-deltas) are stored contiguously in a cluster. This demonstrates that TGI is a superset of DeltaGraph where we are able to handle other queries along with efficient snapshot retrieval. Note that we do not provide experimental results on the internals of snapshot retrieval which have been thoroughly explored in our prior work [@icdepaper].

[ ]{} Smaller eventlists or partition sizes provide a lower latency time for retrieving different versions of a node, which can be seen in Figure \[fig:nver-varl\] and Figure \[fig:nver-varp\], respectively. This is primarily due to the reduction in work required for fetching and deserialization. A higher parallel fetch factor is effective in reducing the latency for version retrieval (Figure \[fig:nver-varc\]). Note that the performance of version retrieval and snapshot retrieval with respect to varying partition sizes is contrary and represents a trade-off. However, smaller eventlist sizes benefit both version retrieval and snapshots. Node version retrieval for Dataset 4 shows a similar behavior, which can be seen in Figure \[fig:friendsterNV\].

[ ]{} We compared the performance of retrieving 1-hop neighborhoods, both static and specific versions, using different graph partitioning and replication choices. A topological, flow-based partitioning accesses fewer graph partitions compared to a random partitioning scheme, and a 1-hop neighborhood replication restricts the access to a single partition.This can be seen in Figure \[fig:part\_replication\] for 1-hop neighborhood retrieval latencies. As discussed in Section \[sec:tgi\], the 1-hop replication does not affect other queries involving snapshots or individual nodes, as the replicated portion is stored separately from the original partition. In case of a 2-hop neighborhood retrieval, there are similar performance benefits over random partitioning, which can be reasoned based upon similar speed-ups for 1-hop neighborhoods.

[ ]{} We observed the fetch performance of TGI with an increasing size of the index. We measured the latencies for retrieving certain snapshots upon varying the time duration of the graph dataset, as shown in Figure \[fig:datascale\]. Datasets 2 and 3 contain additional 333 million and 733 million events over dataset 1, respectively. Only a marginal difference in snapshot retrieval performance demonstrates TGI’s scalability for large datasets.

[ ]{} We examined TAF’s performance through an analytical task for determining the highest local clustering coefficient in historical graph snapshot. Figure \[fig:cc\_taf\] shows compute times for the given task on different graph sizes, as well as varying size of the Spark cluster. Speedups due to parallel execution can be observed, especially for larger datasets.

[ ]{} Earlier in the chapter, we presented two separate ways of computing a quantity over changing versions of a graph (or node). Those include, evaluating the quantity on different versions of the graph separately, and alternatively, performing it in an incremental fashion, utilizing the result for the previous version and updating it with respect to the graph updates. This can be seen for a simple node label counting task in Figure \[fig:computedelta\_taf\]. the benefits due to the incremental ([NodeComputeDelta]{} operator) computation over a version-based computation ([NodeComputeTemporal]{} operator) can be seen in Figure \[fig:incr\_exp\_taf\].

Conclusion {#sec:conclusion}
==========

Graph analytics are increasingly considered crucial in obtaining insights about how interconnected entities behave, how information spreads, what are the most influential entities in the data, and many other characteristics. Analyzing the history of how a graph evolved can provide significant additional insights, especially about the future. Most real-world networks however, are large and highly dynamic. This leads to creation of very large histories, making it challenging to store, query, or analyze them. In this paper, we presented a novel Temporal Graph Index that enables compact storage of very large historical graph traces in a distributed fashion, supporting a wide range of retrieval queries to access and analyze only the required portions of the history. We also present a distributed analytics framework, built on top of Apache Spark, that allows analysts to quickly write complex temporal analysis tasks. Our experiments show that our temporal index exhibits very efficient retrieval performance across a wide range of queries, and can effectively exploit the available parallelism in a distributed setting.

[^1]: https://cassandra.apache.org

[^2]: http://thinkaurelius.github.io/titan/

[^3]: https://github.com/tinkerpop/blueprints/wiki

[^4]: https://github.com/tinkerpop/gremlin

[^5]: A delta formalism provided by Ghandeharizadeh et al. [@Ghandeharizadeh:1996:HED:232753.232801] is an interesting related read on this topic.

[^6]: In general, this depends on the underlying storage mechanism. While the physical placement of micro-deltas is irrelevant for a memory-based storage, it is significant for any disk-based storage due to seek times.
