グラフ走査

ストリーミング式を使用したグラフ走査では、`nodes` 関数を使用して幅優先グラフ走査を実行します。

`nodes` 関数は、`scoreNodes` 関数と組み合わせて推奨事項を提供できます。 `nodes` は、より広範なストリーミング式ライブラリと組み合わせて、収集されたノードセットに対して複雑な操作を実行することもできます。

`nodes` 走査は SolrCloud コレクション内で分散され、コレクションをまたぐことができます。

`nodes` は、グラフ内の近傍にズームインし、正確な走査を実行してノードセットと集計を収集するユースケース向けに設計されています。 これらのタイプのユースケースでは、`nodes` は多くの場合 1 秒未満のパフォーマンスを提供します。いくつかのサンプルユースケースは、ドキュメントの後半で提供されています。.

このドキュメントでは、グラフの用語とストリーミング式に関する基本的な理解があることを前提としています。 グラフ走査の概念については、この Wikipedia の記事 から始めることができます。ストリーミング式の詳細については、このガイドの ストリーミング式 セクションを参照してください。

基本構文

最も基本的な構文から始めて、徐々に複雑さを増していきます。 `nodes` の最も基本的な構文は次のとおりです。

nodes(emails,
      walk="johndoe@apache.org->from",
      gather="to")

この簡単な式を分解してみましょう。

最初のパラメータ `emails` は、走査されるコレクションです。 2 番目のパラメータ `walk` は、ハードコードされたノード ID ("johndoe@apache.org") をインデックス内のフィールド (`from`) にマップします。 これにより、`from` フィールドに `johndoe@apache.org` が含まれるインデックス内のすべての **エッジ** が返されます。

`gather` パラメータは、関数に `to` フィールドの値を収集するように指示します。収集される値は、関数によって出力されるノード ID です。

上記の例では、出力されるノードは、「johndoe@apache.org」がメールを送信したすべての人になります。

walk パラメータは、ルートノード ID のリストも受け入れます。

nodes(emails,
      walk="johndoe@apache.org, janesmith@apache.org->from",
      gather="to")

上記の `nodes` 関数は、`from` フィールドに「johndoe@apache.org」または「janesmith@apache.org」を含むすべてのエッジを見つけ、`to` フィールドを収集します。

すべての ストリーミング式 と同様に、`nodes` 式は `/stream` ハンドラに送信することで実行できます。 例えば

curl --data-urlencode 'expr=nodes(emails,
                                  walk="johndoe@apache.org, janesmith@apache.org->from",
                                  gather="to")' https://127.0.0.1:8983/solr/emails/stream

この式の出力は次のようになります。

{
  "result-set": {
    "docs": [
      {
        "node": "slist@campbell.com",
        "collection": "emails",
        "field": "to",
        "level": 1
      },
      {
        "node": "catherine.pernot@enron.com",
        "collection": "emails",
        "field": "to",
        "level": 1
      },
      {
        "node": "airam.arteaga@enron.com",
        "collection": "emails",
        "field": "to",
        "level": 1
      },
      {
        "EOF": true,
        "RESPONSE_TIME": 44
      }
    ]
  }
}

返されるすべてのタプルには、`node` フィールドがあります。 `node` フィールドには、関数によって収集されたノード ID が含まれています。 走査の `collection`、`field`、`level` も出力に含まれます。

例の各タプルのレベルが「1」であることに注意してください。 ルートノードはレベル 0 です (上記の例では、ルートノードは「johndoe@apache.org」、「janesmith@apache.org」です)。 デフォルトでは、`nodes` 関数は走査の **リーフノード** (最外部ノードセット) のみを出力します。 ルートノードを出力するには、`scatter` パラメータを指定します。

nodes(emails,
      walk="johndoe@apache.org->from",
      gather="to",
      scatter="branches, leaves")

`scatter` パラメータは、**ブランチ** を **リーフ** とともに出力するかどうかを制御します。 ルートノードは、走査の最外部レベルではないため、「ブランチ」と見なされます。

ブランチとリーフの両方を出力する場合、出力は次のようになります。

{
  "result-set": {
    "docs": [
      {
        "node": "johndoe@apache.org",
        "collection": "emails",
        "field": "node",
        "level": 0
      },
      {
        "node": "slist@campbell.com",
        "collection": "emails",
        "field": "to",
        "level": 1
      },
      {
        "node": "catherine.pernot@enron.com",
        "collection": "emails",
        "field": "to",
        "level": 1
      },
      {
        "node": "airam.arteaga@enron.com",
        "collection": "emails",
        "field": "to",
        "level": 1
      },
      {
        "EOF": true,
        "RESPONSE_TIME": 44
      }
    ]
  }
}

これで、レベル 0 のルートノードが出力に含まれるようになりました。

集計

nodes は集計もサポートしています。例えば

nodes(emails,
      walk="johndoe@apache.org, janesmith@apache.org->from",
      gather="to",
      count(*))

上記の式は、from フィールドに "johndoe@apache.org" または "janesmith@apache.org" が含まれるエッジを見つけ、to フィールドから値を収集します。また、収集された各ノードIDのカウントを集計します。

"johndoe@apache.org" と "janesmith@apache.org" の両方が同じ人物にメールを送信した場合、収集されたノードのカウントは 2 になる可能性があります。ノードセットには一意のノードセットが含まれているため、同じ人物がノードセットに2回表示されることはありませんが、カウントにはトラバーサル中に2回出現したことが反映されます。

エッジはトラバーサルの一部として一意化されるため、カウントは "johndoe@apache.org" が同じ人物にメールを送信した回数には**反映されません**。たとえば、personA が personB に 100 回メールを送信したとします。これらのエッジは一意化され、1回だけカウントされます。しかし、personC も personB にメールを送信した場合、personB のカウントは増加します。

サポートされている集計関数は、count(*)sum(field)min(field)max(field)、および avg(field) です。集計されるフィールドは、トラバーサル中に収集されたエッジに存在する必要があります。後の例(下記)では、集計が推奨事項を提供し、トラバーサルの範囲を制限するための強力なツールになることを示します。

nodes 関数のネスト

nodes 関数は、グラフをより深くトラバースするためにネストすることができます。例えば

nodes(emails,
      nodes(emails,
            walk="johndoe@apache.org->from",
            gather="to"),
      walk="node->from",
      gather="to")

上記の例では、外側の nodes 関数は、内側の nodes 関数から収集されたノードセットに対して動作します。

内側の nodes 関数は、既に説明した例とまったく同じように動作することに注意してください。しかし、外側の nodes 関数の walk パラメータは異なる動作をします。

外側の nodes 関数では、walk パラメータは内部ストリーミング式からのタプルで動作します。このシナリオでは、walk パラメータは node フィールドを from フィールドにマッピングします。内部の nodes 式から収集されたノードIDは node フィールドに配置されることを覚えておいてください。

より簡単に言えば、内部式は "johndoe@apache.org" がメールを送信したすべての人物を収集します。このグループを "johndoe@apache.org の友人" と呼ぶことができます。外部式は、"johndoe@apache.org の友人" がメールを送信したすべての人物を収集します。これは、基本的な友人の友人のトラバーサルです。

nodes 関数をネストするこの構成は、グラフを制御しながらトラバースするための基本的な手法です。

サイクル検出

nodes 関数は、トラバーサル全体でサイクル検出を実行します。これにより、既に訪問されたノードが再びトラバースされないことが保証されます。サイクル検出は、トラバーサルのサイズを制限し、正確な集計を収集するために重要です。サイクル検出がない場合、トラバーサルのサイズはトラバーサル内の各ホップで指数関数的に増加する可能性があります。サイクル検出では、新たに出会ったノードのみがトラバースされます。

サイクル検出はコレクションの境界を**越えません**。これは、内部的にはコレクション名がノードIDの一部であるためです。たとえば、ノードID "johndoe@apache.org" は実際には emails/johndoe@apache.org です。別のコレクションにトラバースする場合、"johndoe@apache.org" はトラバースされます。

トラバーサルのフィルタリング

トラバーサルの各レベルは、フィルタクエリでフィルタリングできます。例えば

nodes(emails,
      walk="johndoe@apache.org->from",
      fq="body:(solr rocks)",
      gather="to")

上記の例では、フィルタクエリに一致するメールのみがトラバーサルに含まれます。任意のSolrクエリをここに含めることができます。そのため、地理空間クエリを実行したり、利用可能なクエリパーサーを適用したり、カスタムクエリパーサーを作成してトラバーサルを制限したりといった、楽しいことができます。

ルートストリーム

任意のストリーミング式を使用して、トラバーサルのルートノードを提供できます。例えば

nodes(emails,
      search(emails, q="body:(solr rocks)", fl="to", sort="score desc", rows="20")
      walk="to->from",
      gather="to")

上記の例では、検索式を介してルートノードを提供しています。また、結合などを含む任意の複雑なネストされたストリーミング式を提供して、ルートノードを指定することもできます。

walk パラメータは、内部ストリームによって生成されたタプルからのフィールドをマッピングすることに注意してください。この場合、内部ストリームの to フィールドを from フィールドにマッピングします。

高頻度ノードのスキップ

グラフ内の高頻度ノードのトラバースをスキップすることが望ましいことがよくあります。これは、検索語のストップリストと性質が似ています。これを説明する最良の方法は、ユースケースの例を示すことです。

協調フィルタに基づいてユーザーにコンテンツを推奨したいとしましょう。以下は、単純な協調フィルタのアプローチです。

  1. userA が読んだすべてのコンテンツを見つけます。

  2. 読書リストが userA に最も近いユーザーを見つけます。これらは userA と同様の嗜好を持つユーザーです。

  3. 手順2のユーザーが読んでいて、userA がまだ読んでいないコンテンツを推奨します。

手順2をよく見てください。大規模なグラフでは、手順2は非常に大規模なトラバーサルにつながる可能性があります。これは、userA が何百万人もの他の人々が閲覧したコンテンツを閲覧している可能性があるためです。これらの高頻度ノードをスキップするには、2つの理由があります。

  1. 数百万個の一意のノードを訪問する大規模なトラバーサルは、サイクル検出がメモリ内で追跡されるため、低速で多くのメモリを消費します。

  2. 高頻度ノードは、同様の嗜好を持つユーザーを判別するのにも役立ちません。より少ない人が閲覧したコンテンツは、より正確な推奨事項を提供します。

nodes 関数には、高頻度ノードをフィルタリングするための maxDocFreq パラメータがあります。以下のサンプルコードは、推奨事項の手順1と2を示しています。

 nodes(logs,
       search(logs, q="userID:user1", fl="articleID", sort="articleID asc", fq="action:view", qt="/export"),
       walk="articleID->articleID",
       gather="userID",
       fq="action:view",
       maxDocFreq="10000",
       count(*)))

上記の例では、内部検索式は logs コレクションを検索し、"user1" が閲覧したすべての記事を返します。外側の nodes 式は、内部検索式から出力されたすべての記事を取得し、それらの記事の logs コレクション内のすべてのレコードを見つけます。次に、記事を読んだユーザーを収集して集計します。maxDocFreq パラメータは、返される記事を(シャードごとに)10,000件以下のログレコードに表示される記事に制限します。これは、何百万人ものユーザーが閲覧した記事が返されるのを防ぎます。

トラバーサルの追跡

デフォルトでは、nodes 関数はサイクル検出を行うのに十分な情報のみを追跡します。これは、グラフ内のノードと集計を出力するのに十分な情報を提供します。

グラフの視覚化など、一部のユースケースでは、エッジを出力する必要もあります。trackTraversal="true" を設定すると、nodes はノード間の接続を追跡するため、エッジを構築できます。trackTraversal が有効になっている場合、新しい ancestors プロパティが各ノードに表示されます。ancestors プロパティには、ノードを指していたノードIDのリストが含まれています。

以下は、trackTraversal が true に設定された nodes 式の例です。

nodes(emails,
      nodes(emails,
            walk="johndoe@apache.org->from",
            gather="to",
            trackTraversal="true"),
      walk="node->from",
      trackTraversal="true",
      gather="to")

コレクション間のトラバーサル

ネストされた nodes 関数は、異なる SolrCloud コレクションで動作できます。これにより、トラバーサルは1つのコレクションから別の koleksiyon に「移動」してノードを収集できます。サイクル検出はコレクションの境界を越えないため、1つのコレクションで収集されたノードは別のコレクションでトラバースされます。これは、コレクション間のトラバーサルをサポートするために意図的に行われました。コレクション間のトラバーサルの出力には、異なるコレクション属性を持つ重複ノードが含まれる可能性があることに注意してください。

以下は、"emails" コレクションから "logs" コレクションにトラバースする nodes 式の例です。

nodes(logs,
      nodes(emails,
            search(emails, q="body:(solr rocks)", fl="from", sort="score desc", rows="20")
            walk="from->from",
            gather="to",
            scatter="leaves, branches"),
      walk="node->user",
      fq="action:edit",
      gather="contentID")

上記の例では、本文に「solr rocks」が含まれるメールを送信したすべての人を見つけます。次に、これらの人々がメールを送信したすべての人を見つけます。次に、logs コレクションにトラバースし、これらの人々が編集したすべてのコンテンツIDを収集します。

nodes と他のストリーミング式の組み合わせ

nodes 関数は、ストリームソースとストリームデコレータの両方として機能できます。より広範なストリーム式ライブラリとの接続により、グラフトラバーサルを実行する際に非常に強力で柔軟性があります。2つのフレンドネットワークを交差させるためにストリーミング式ライブラリを使用する例を次に示します。

            intersect(on="node",
                      sort(by="node asc",
                           nodes(emails,
                                 nodes(emails,
                                       walk="johndoe@apache.org->from",
                                       gather="to"),
                                 walk="node->from",
                                 gather="to",
                                 scatter="branches,leaves")),
                       sort(by="node asc",
                            nodes(emails,
                                  nodes(emails,
                                        walk="janedoe@apache.org->from",
                                        gather="to"),
                                  walk="node->from",
                                  gather="to",
                                  scatter="branches,leaves")))

上記の例では、"johndoe@apache.org" をルートとするフレンドネットワークと "janedoe@apache.org" をルートとするフレンドネットワークの2つの別々のフレンドネットワークを収集します。次に、フレンドネットワークは node フィールドでソートされ、交差されます。結果のノードセットは、2つのフレンドネットワークの交差部分になります。

グラフトラバーサルのユースケース例

マーケットバスケットの共起性を計算する

どの製品が特定の製品と最も頻繁に購入されるかを知ることはしばしば役に立ちます。この例では、過去のショッピングバスケットを格納するために、単純なマーケットバスケットテーブル(Solrにインデックス付けされています)を使用します。テーブルのスキーマは非常にシンプルで、各行には basketIDproductID が含まれています。これは、テーブルの各行がエッジを表すグラフと見なすことができます。また、グラフに数十億のエッジが含まれている場合でも、バスケットの共起性を計算するために非常に迅速にトラバースできます。

サンプル構文は次のとおりです。

top(n="5",
    sort="count(*) desc",
    nodes(baskets,
          random(baskets, q="productID:ABC", fl="basketID", rows="500"),
          walk="basketID->basketID",
          fq="-productID:ABC",
          gather="productID",
          count(*)))

このトラバーサルが何をしているのかを正確に分析してみましょう.

  1. 最初に評価される式は、内部の random 式で、baskets コレクションから、productID が "ABC" であるランダムな basketID を 500 個返します。random 式は、トラバーサルを固定数のバスケットに制限し、推奨事項にサプライズ要素を追加するため、推奨事項に非常に役立ちます。random 関数を使用すると、非常に大きなグラフから高速なサンプルセットを提供できます。

  2. 外側の nodes 式は、手順1で生成された basketID の baskets コレクション内のすべてのレコードを見つけます。また、結果に表示されないように productID "ABC" を除外します。次に、これらのバスケット全体の productID を収集してカウントします。

  3. 外側の top 式は、手順2で出力された productID をカウントでランク付けし、上位5つを選択します。

要するに、この式は、過去のショッピングバスケットで product "ABC" と最も頻繁に共起する製品を見つけます。

scoreNodes 関数を使用して推奨事項を作成する

このユースケースは、productID:ABC と最も頻繁に共起する製品を計算する上記のマーケットバスケットの例に基づいています。ランク付けされた共起カウントは、推奨事項の候補を提供します。scoreNodes 関数を使用して候補をスコアリングし、最適な推奨事項を見つけることができます。

scoreNodes 関数の構文について詳しく説明する前に、生の共起カウントが最適な推奨事項を生成しない可能性がある理由を理解しておくと役立ちます。その理由は、生の共起カウントは、すべてのバスケットで頻繁に発生するアイテムを優先するためです。より良い推奨事項は、productID ABC と最も重要な関係を持つ製品を見つけることです。scoreNodes 関数は、単語頻度-逆文書頻度(TF-IDF)アルゴリズムを使用して、最も重要な関係を見つけます。

scoreNodes の仕組み

scoreNodes関数は、nodes式によって出力された各ノードにスコアを割り当てます。デフォルトでは、scoreNodes関数は、TF値として共起カウントであるcount(*)集計を使用します。各ノードのIDF値は、ノードが収集されたコレクションから取得されます。次に、各ノードはTF*IDF式を使用してスコアリングされ、すべてのマーケットバスケットで頻度が低いノードにブーストが提供されます。

共起カウントとIDFを組み合わせることで、製品ID ABCと推奨候補間の関係の重要性を示すスコアが得られます。

scoreNodes関数は、nodeScoreフィールドの各ノードにスコアを追加します。

scoreNodes構文の例

top(n="1",
    sort="nodeScore desc",
    scoreNodes(top(n="50",
                   sort="count(*) desc",
                   nodes(baskets,
                         random(baskets, q="productID:ABC", fl="basketID", rows="500"),
                         walk="basketID->basketID",
                         fq="-productID:ABC",
                         gather="productID",
                         count(*)))))

この例は、前の例「マーケットバスケットの共起カウントの計算」に基づいています。

  1. 最も内側のtop関数は、製品ID ABCと最も頻繁に共起する上位50製品を取得していることに注意してください。これは、50の推奨候補を提供します。

  2. 次に、scoreNodes関数は、各ノードのTF*IDFに基づいて候補にスコアを割り当てます。

  3. 外側のtop式は、最もスコアの高いノードを選択します。これが推奨事項です。

協調フィルタリングに基づくコンテンツの推奨

この例では、協調フィルタリングに基づいてユーザーにコンテンツを推奨します。この推奨は、userIDarticleID、および実行されたアクションを含むログレコードを使用して行われます。このシナリオでは、各ログレコードはグラフのエッジと見なすことができます。userIDとarticleIDはノードであり、アクションはトラバーサルをフィルタリングするために使用されるエッジプロパティです。

サンプル構文は次のとおりです。

top(n="5",
    sort="count(*) desc",
    nodes(logs,
          top(n="30",
              sort="count(*) desc",
              nodes(logs,
                    search(logs, q="userID:user1", fl="articleID", sort="articleID asc", fq="action:read", qt="/export"),
                    walk="articleID->articleID",
                    gather="userID",
                    fq="action:read",
                    maxDocFreq="10000",
                    count(*))),
              walk="node->userID",
              gather="articleID",
              fq="action:read",
              count(*)))

上記の式をステップバイステップで分解してみましょう。

  1. 最初に評価される式は、内側のsearch式です。この式は、logsコレクションで「user1」と一致するすべてのレコードを検索します。これは、推奨を行うユーザーです。

    「action:read」のレコードのみを取得するために適用されるフィルターがあります。見つかった各レコードのarticleIDを返します。つまり、この式は「user1」が読んだすべての記事を返します。

  2. 内側のnodes式は、ステップ1から返されたarticleIDに対して動作します。見つかった各articleIDを取得し、articleIDフィールドに対して検索します。

    maxDocFreqパラメーターを使用して高頻度ノードをスキップし、ログに10,000回以上出現する記事を除外していることに注意してください。userIDを収集し、各ユーザーのカウントを集計します。このステップでは、「user1」が読んだ記事と同じ記事を読んだユーザーを見つけ、同じ記事をいくつ読んだかをカウントします。

  3. 内側のtop式は、ステップ2から出力されたユーザーをランク付けします。「user1」の読書リストと最も重複する上位30人のユーザーを出力します。

  4. 外側のnodes式は、ステップ3から出力されたユーザーの読書リストを収集します。収集されたarticleIDをカウントします。

    ステップ1(user1の読書リスト)で選択された記事は、サイクル検出のため、このステップには表示されません。そのため、このステップでは、「user1」と最も類似した読書習慣を持つユーザーが読んだ記事の中で、「user1」がまだ読んでいない記事が返されます。また、このユーザーグループ全体で各記事が読まれた回数をカウントします。

  5. 外側のtop式は、ステップ4から出力された上位の記事を取得します。これが推奨事項です。

タンパク質経路トラバーサル

近年、科学者たちは、一部のがんの原因となるオンコジーンと呼ばれる変異タンパク質を標的とする薬剤を合理的に設計できるようになってきています。タンパク質は通常、経路と呼ばれる複数のタンパク質間の化学的相互作用の長い連鎖を通じて作用し、経路内のオンコジーンに対応する薬剤がない場合でも、経路内の別のタンパク質には薬剤が存在する可能性があります。タンパク質の相互作用と薬剤を記録するタンパク質コレクションのグラフトラバーサルは、候補となる可能性があります。(この例を提供してくれたNCBIのLewis Geerに感謝します)。

以下の例は、タンパク質経路トラバーサルを示しています。

nodes(proteins,
      nodes(proteins,
            walk="NRAS->name",
            gather="interacts"),
      walk="node->name",
      gather="drug")

このトラバーサルが何をしているのかを正確に分析してみましょう.

  1. 内側のnodes式は、proteinsコレクションをトラバースします。タンパク質の名前が「NRAS」であるグラフ内のすべてのエッジを見つけます。次に、interactsフィールドのタンパク質を収集します。これにより、「NRAS」と相互作用するすべてのタンパク質が収集されます。

  2. 外側のnodes式もproteinsコレクションで動作します。ステップ1から出力されたタンパク質に対応するすべての薬剤を収集します。

  3. この段階的アプローチを使用すると、ルートタンパク質から任意のステップ数だけ離れた相互作用の経路に沿って薬剤を収集できます。

グラフの視覚化をサポートするためのGraphMLのエクスポート

上記の例では、nodes式は、他のストリーミング式と同様にSolrの/streamハンドラーに送信されました。このアプローチは、他のストリーミング式と同じJSONタプル形式でノードを出力するため、他のストリーミング式と同様に扱うことができます。上記の推奨ユースケースのように、タプルを直接操作する必要がある場合は、/streamハンドラーを使用できます。

グラフの視覚化を含む他のグラフトラバーサルユースケースがあります。Solrは、nodes式を受け取り、GraphMLで結果を出力する/graphリクエストハンドラーを導入することで、これらのユースケースをサポートしています。

GraphMLは、Gephiなどのグラフ視覚化ツールでサポートされているXML形式です。Gephiは、グラフを統計的に分析および視覚化するための高度なオープンソースツールです。nodes式を使用して、より大きなグラフの一部をGraphMLでエクスポートし、Gephiなどのツールにインポートできます。

GraphMLでグラフをエクスポートする際には、いくつかの注意点があります。

  1. /graphハンドラーは、グラフ内のノードとエッジの両方をエクスポートできます。デフォルトでは、ノードのみをエクスポートします。エッジをエクスポートするには、nodes式でtrackTraversal="true"を設定する必要があります。

  2. /graphハンドラーは現在、nodes式を含む任意の複雑なストリーミング式を受け入れます。ストリーミング式にnodes式が含まれていない場合、/graphハンドラーはGraphMLを正しく出力しません。

  3. /graphハンドラーは現在、リクエストごとに1つの任意の複雑なネストされたnodes式を受け入れます。これは、複数のnodes式からのノードセットを結合または交差させるストリーミング式を送信できないことを意味します。/graphハンドラーは、単一のnodes式内での任意のレベルのネストをサポートします。/streamハンドラーはノードセットの結合と交差をサポートしますが、/graphハンドラーは現在サポートしていません。

GraphMLリクエストの例

curl --data-urlencode 'expr=nodes(enron_emails,
                                  nodes(enron_emails,
                                        walk="kayne.coulter@enron.com->from",
                                        trackTraversal="true",
                                        gather="to"),
                                  walk="node->from",
                                  scatter="leaves,branches",
                                  trackTraversal="true",
                                  gather="to")' https://127.0.0.1:8983/solr/enron_emails/graph

GraphML出力の例

<graphml xmlns="http://graphml.graphdrawing.org/xmlns"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://graphml.graphdrawing.org/xmlns http://graphml.graphdrawing.org/xmlns/1.0/graphml.xsd">
<graph id="G" edgedefault="directed">
     <node id="kayne.coulter@enron.com">
           <data key="field">node</data>
           <data key="level">0</data>
           <data key="count(*)">0.0</data>
     </node>
     <node id="don.baughman@enron.com">
           <data key="field">to</data>
           <data key="level">1</data>
           <data key="count(*)">1.0</data>
     </node>
     <edge id="1"  source="kayne.coulter@enron.com"  target="don.baughman@enron.com"/>
     <node id="john.kinser@enron.com">
           <data key="field">to</data>
           <data key="level">1</data>
           <data key="count(*)">1.0</data>
    </node>
    <edge id="2"  source="kayne.coulter@enron.com"  target="john.kinser@enron.com"/>
    <node id="jay.wills@enron.com">
          <data key="field">to</data>
          <data key="level">1</data>
          <data key="count(*)">1.0</data>
    </node>
    <edge id="3"  source="kayne.coulter@enron.com"  target="jay.wills@enron.com"/>
</graph></graphml>