Получение данных для d3 из ArangoDB с использованием AQL (или arangojs)

Я создаю приложение, основанное на d3-силовом графике с ArangoDB на бэкэнд, и я хочу иметь возможность загружать node и динамически связывать данные из Arango как можно эффективнее.

Я не эксперт в d3, но, как правило, силовой макет, похоже, хочет, чтобы его данные представляли собой массив узлов и массив ссылок, которые имеют фактические объекты node в качестве своих источников и целей, например

var nodes = [
        {id: 0, reflexive: false},
        {id: 1, reflexive: true },
        {id: 2, reflexive: false}
    ],
    links = [
        {source: nodes[0], target: nodes[1], left: false, right: true },
        {source: nodes[1], target: nodes[2], left: false, right: true }
    ];

В настоящее время я использую следующий запрос AQL для получения соседних узлов, но это довольно громоздко. Часть трудности состоит в том, что я хочу включать информацию о краях для узлов, даже если эти ребра не пройдены (чтобы отобразить количество ссылок, которые < node имеет перед загрузкой этих ссылок из базы данных).

LET docId = "ExampleDocClass/1234567"

 // get data for all the edges
LET es = GRAPH_EDGES('EdgeClass',docId,{direction:'any',maxDepth:1,includeData:true})

// create an array of all the neighbor nodes
LET vArray = ( 
    FOR v IN GRAPH_TRAVERSAL('EdgeClass',docId[0],'any',{ maxDepth:1})
        FOR v1 IN v RETURN v1.vertex
    )

// using node array, return inbound and outbound for each node 
LET vs = (
    FOR v IN vArray
        // inbound and outbound are separate queries because I couldn't figure out
        // how to get Arango to differentiate inbout and outbound in the query results
        LET oe = (FOR oe1 IN GRAPH_EDGES('EdgeClass',v,{direction:'outbound',maxDepth:1,includeData:true}) RETURN oe1._to)
        LET ie = (FOR ie1 IN GRAPH_EDGES('EdgeClass',v,{direction:'inbound',maxDepth:1,includeData:true}) RETURN ie1._from)
        RETURN {'vertexData': v, 'outEdges': oe, 'inEdges': ie}
    )
RETURN {'edges':es,'vertices':vs}

Конечный вывод выглядит следующим образом: http://pastebin.com/raw.php?i=B7uzaWxs ... который можно прочитать почти непосредственно в d3 (мне просто нужно немного дедуплицировать).

У моих узлов графа есть большое количество ссылок, поэтому производительность важна (как с точки зрения нагрузки на сервер и клиент, так и размер файла для связи между ними). Я также планирую создавать различные команды для взаимодействия с графиком, а не просто расширения соседних узлов. Есть ли способ лучше структурировать этот запрос AQL (например, избегая четырех отдельных запросов графа) или вообще избегать AQL с использованием функций arangojs или приложения FOXX, сохраняя при этом структурирование ответа в формате, который мне нужен для d3 (включая данные ссылок с каждым node)?

Ответ 1

Извините за поздний ответ, мы были заняты зданием v2.8;) Я предлагаю сделать как можно больше возможностей на стороне базы данных, так как копирование и сериализация/десериализация JSON по сети обычно дорога, поэтому хорошая передача данных как можно меньше.

Прежде всего, я использовал ваш запрос и выполнил его на образце набора данных, который я создал (~ 800 вершин и 800 ребер попали в мой набор данных) В качестве базовой линии я использовал время выполнения вашего запроса, которое в моем случае было ~ 5.0s

Итак, я попытался создать тот же результат, что и в AQL. Я нашел некоторые улучшения в вашем запросе: 1. GRAPH_NEIGHBORS немного быстрее, чем GRAPH_EDGES. 2. Если возможно, избегайте {includeData: true}, если вам не нужны данные Особенно, если вам нужно /from vertices._id только GRAPH_NEIGHBORS с {includeData: false} превосходит GRAPH_EDGES на порядок. 3. GRAPH_NEIGHBORS дедуплицируется, GRAPH_EDGES нет. Что в вашем случае кажется желательным. 3. Вы можете избавиться от пары подзапросов там.

Итак, вот чистый запрос AQL, который я мог бы придумать:

LET docId = "ExampleDocClass/1234567"
LET edges = GRAPH_EDGES('EdgeClass',docId,{direction:'any',maxDepth:1,includeData:true})
LET verticesTmp = (FOR v IN GRAPH_NEIGHBORS('EdgeClass', docId, {direction: 'any', maxDepth: 1, includeData: true})
  RETURN {
    vertexData: v,
    outEdges: GRAPH_NEIGHBORS('EdgeClass', v, {direction: 'outbound', maxDepth: 1, includeData: false}),
    inEdges: GRAPH_NEIGHBORS('EdgeClass', v, {direction: 'inbound', maxDepth: 1, includeData: false})
  })
LET vertices = PUSH(verticesTmp, {
  vertexData: DOCUMENT(docId),
  outEdges: GRAPH_NEIGHBORS('EdgeClass', docId, {direction: 'outbound', maxDepth: 1, includeData: false}),
  inEdges: GRAPH_NEIGHBORS('EdgeClass', docId, {direction: 'inbound', maxDepth: 1, includeData: false})
})
RETURN { edges, vertices }

Это дает тот же формат результата, что и ваш запрос, и имеет то преимущество, что каждая вершина, связанная с docId, хранится ровно один раз в вершинах. Также сам docId хранится ровно один раз в вершинах. Не требуется дедупликация на стороне клиента. Но в outEdges/inEdges каждой вершины все связные вершины также ровно один раз, я не знаю, нужно ли вам знать, есть ли несколько ребер между вершинами в этом списке.

Этот запрос использует ~ 0,06 с в моем наборе данных.

Однако, если вы приложите еще больше усилий, вы также можете использовать ручной обход внутри приложения Foxx. Это немного сложнее, но может быть быстрее в вашем случае, поскольку вы выполняете меньше подзапросов. Код для этого может выглядеть следующим образом:

var traversal = require("org/arangodb/graph/traversal");
var result = {
  edges: [],
  vertices: {}
}
var myVisitor = function (config, result, vertex, path, connected) {
  switch (path.edges.length) {
    case 0:
      if (! result.vertices.hasOwnProperty(vertex._id)) {
        // If we visit a vertex, we store it data and prepare out/in
        result.vertices[vertex._id] = {
          vertexData: vertex,
          outEdges: [],
          inEdges: []
        };
      }

      // No further action
      break;
    case 1:
      if (! result.vertices.hasOwnProperty(vertex._id)) {
        // If we visit a vertex, we store it data and prepare out/in
        result.vertices[vertex._id] = {
          vertexData: vertex,
          outEdges: [],
          inEdges: []
        };
      }
      // First Depth, we need EdgeData
      var e = path.edges[0];
      result.edges.push(e);
      // We fill from / to for both vertices
      result.vertices[e._from].outEdges.push(e._to);
      result.vertices[e._to].inEdges.push(e._from);
      break;
    case 2:
      // Second Depth, we do not need EdgeData
      var e = path.edges[1];
      // We fill from / to for all vertices that exist
      if (result.vertices.hasOwnProperty(e._from)) {
        result.vertices[e._from].outEdges.push(e._to);
      }
      if (result.vertices.hasOwnProperty(e._to)) {
        result.vertices[e._to].inEdges.push(e._from);
      }
      break;
  }
};
var config = {
  datasource: traversal.generalGraphDatasourceFactory("EdgeClass"),
  strategy: "depthfirst",
  order: "preorder",
  visitor: myVisitor,
  expander: traversal.anyExpander,
  minDepth: 0,
  maxDepth: 2
};
var traverser = new traversal.Traverser(config);
traverser.traverse(result, {_id: "ExampleDocClass/1234567"});
return {
  edges: result.edges,
  vertices: Object.keys(result.vertices).map(function (key) {
              return result.vertices[key];
            })
};

Идея этого обхода - это посетить все вершины от начальной вершины до двух краев. Все вершины с глубиной 0 - 1 будут добавлены с данными в объект вершин. Все ребра, исходящие из начальной вершины, будут добавлены с данными в список ребер. Все вершины глубиной 2 будут только устанавливать outEdges/inEdges в результате.

Это имеет то преимущество, что vertices дедуплицируется. и outEdges/inEdges содержат все связанные вершины несколько раз, если между ними есть несколько ребер.

Этот обход выполняется в моем наборе данных в ~ 0.025 с, поэтому он в два раза быстрее, чем решение AQL.

надеюсь, что это все еще помогает;)