Рекурсивный обход графа Arangodb AQL

У меня есть граф с тремя коллекциями, элементы которых можно соединять ребрами. ItemA является родительским элементом элемента B, который, в свою очередь, является родительским элементом элемента C. Элементы можно соединять только ребрами в направлении

"_from : child, _to : parent"

В настоящее время я могу получить только "линейный" результат с помощью этого запроса AQL:

LET contains = (FOR v IN 1..? INBOUND 'collectionA/itemA' GRAPH 'myGraph' RETURN v)

     RETURN {
        "root": {
            "id": "ItemA",
            "contains": contains
       }
   }

Результат выглядит так:

"root": {
    "id": "itemA",
    "contains": [
        {
            "id": "itemB"
        },
        {
            "id": "itemC"
        }
    ]
}

Но мне нужно получить такой «иерархический» результат обхода графа:

"root": {
    "id": "itemA",
    "contains": [
        {
            "id": "itemB",
            "contains": [
                {
                    "id": "itemC"
                }
            }
        ]
    }

Итак, могу ли я получить этот "иерархический" результат, выполнив запрос aql?

Еще одна вещь: обход должен выполняться до тех пор, пока не встретятся листовые узлы. Так что глубина обхода заранее неизвестна.


person Alice Smith    schedule 06.10.2016    source источник
comment
Пара подходящих приемов, которые могут вам помочь: for v, e, p in 1..3 inbound и вернуть p. Если вам нужна более конкретная информация, вы можете использовать p.vertices[0], p.vertices[1], p.vertices[2]. Оттуда вы можете структурировать свой возврат, чтобы отображать нужные вам значения, хотя p уже находится в иерархическом формате.   -  person Nate Gardner    schedule 08.10.2016
comment
Известна максимальная глубина вложения? Или это рекурсивно без предсказуемой глубины?   -  person David Thomas    schedule 10.10.2016
comment
Почему результат должен быть иерархическим? Это должно предотвратить дублирование в наборе результатов?   -  person CodeManX    schedule 10.10.2016
comment
@DavidThomas Это рекурсивный метод без предсказуемой глубины.   -  person Alice Smith    schedule 11.10.2016
comment
@CoDEmanX, да, предполагается (я думаю, для этой цели я должен использовать uniqueVertices : global option в моем обходе)   -  person Alice Smith    schedule 11.10.2016
comment
@NateGardner, нет, p не в иерархическом формате. Он просто содержит ребра и вершины (оба в формате массива). Мне нужно вернуть иерархическую структуру, даже если глубина неизвестна, поэтому я не могу работать с p.vertices[0], p.vertices[1], p.vertices[2]   -  person Alice Smith    schedule 11.10.2016


Ответы (2)


Я нашел решение. Мы решили использовать UDF (пользовательские функции < / а>).

Вот несколько шагов для построения правильной иерархической структуры:

  1. Зарегистрируйте функцию в arango db.
  2. Запустите ваш aql-запрос, который создает плоскую структуру (вершина и соответствующий путь для этой вершины). И передайте результат в качестве входного параметра вашей функции UDF. Здесь моя функция просто добавляет каждый элемент к его родительскому элементу

В моем случае: 1) Зарегистрируйте функцию в arango db.

db.createFunction(
        'GO::LOCATED_IN::APPENT_CHILD_STRUCTURE',
            String(function (root, flatStructure) {
                if (root && root.id) {
                    var elsById = {};
                    elsById[root.id] = root;

                    flatStructure.forEach(function (element) {
                        elsById[element.id] = element;
                        var parentElId = element.path[element.path.length - 2];
                        var parentEl = elsById[parentElId];

                        if (!parentEl.contains)
                            parentEl.contains = new Array();

                        parentEl.contains.push(element);
                        delete element.path;
                    });
                }
                return root;
            })
        );

2) Запустите AQL с udf:

    LET flatStructure = (FOR v,e,p IN 1..? INBOUND 'collectionA/itemA' GRAPH 'myGraph' 
       LET childPath = (FOR pv IN p.vertices RETURN pv.id_source)
    RETURN MERGE(v, childPath))

    LET root = {"id": "ItemA"} 

    RETURN GO::LOCATED_IN::APPENT_CHILD_STRUCTURE(root, flatStructure)

Примечание. Не забывайте соглашение об именах при реализации ваших функций.

person Alice Smith    schedule 25.10.2016
comment
Является ли пользовательская функция / конечная точка Foxx рекомендуемым подходом еще в 2019 году? - person Biel Simon; 12.09.2019

Мне также нужно было знать ответ на этот вопрос, поэтому вот решение, которое работает.

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

Решение состоит в том, чтобы использовать микросервис Foxx, который поддерживает рекурсию и строит дерево. У меня есть проблема с циклическими путями, но я реализовал ограничение максимальной глубины, которое останавливает это, жестко закодированное до 10 в приведенном ниже примере.

Чтобы создать микросервис Foxx:

  1. Создайте новую папку (например, рекурсивное дерево)
  2. Создайте скрипты каталога
  3. Поместите файлы manifest.json и index.js в корневой каталог.
  4. Поместите файл setup.js в каталог скриптов.
  5. Затем создайте новый zip-файл с этими тремя файлами (например, Foxx.zip)
  6. Перейдите в консоль администратора ArangoDB.
  7. Щелкните "Услуги" | Добавить услугу
  8. Введите соответствующую точку монтирования, например / мое / дерево
  9. Перейдите на вкладку Zip.
  10. Перетащите созданный файл Foxx.zip, он должен создать без проблем.
  11. Если вы получите сообщение об ошибке, убедитесь, что коллекции myItems и myConnections не существуют, а граф с именем myGraph не существует, так как он попытается создать их с образцами данных.
  12. Затем перейдите в консоль администратора ArangoDB, Services | / мое / дерево
  13. Нажмите на API
  14. Развернуть / tree / {rootId}
  15. Укажите параметр rootId для ItemA и нажмите «Попробовать».
  16. Вы должны увидеть результат из предоставленного корневого идентификатора.

Если rootId не существует, он ничего не возвращает.Если rootId не имеет дочерних элементов, он возвращает пустой массив для 'contains'. Если rootId имеет циклические значения 'contains', он возвращает вложенность до предела глубины, я бы хотел, чтобы был более чистый способ остановить это.

Вот три файла: setup.js (должны находиться в папке скриптов):

'use strict';
const db = require('@arangodb').db;
const graph_module =  require("org/arangodb/general-graph");

const itemCollectionName = 'myItems';
const edgeCollectionName = 'myConnections';
const graphName = 'myGraph';

if (!db._collection(itemCollectionName)) {
  const itemCollection = db._createDocumentCollection(itemCollectionName);
  itemCollection.save({_key: "ItemA" });
  itemCollection.save({_key: "ItemB" });
  itemCollection.save({_key: "ItemC" });
  itemCollection.save({_key: "ItemD" });
  itemCollection.save({_key: "ItemE" });

  if (!db._collection(edgeCollectionName)) {
    const edgeCollection = db._createEdgeCollection(edgeCollectionName);
    edgeCollection.save({_from: itemCollectionName + '/ItemA', _to: itemCollectionName + '/ItemB'});
    edgeCollection.save({_from: itemCollectionName + '/ItemB', _to: itemCollectionName + '/ItemC'});
    edgeCollection.save({_from: itemCollectionName + '/ItemB', _to: itemCollectionName + '/ItemD'});
    edgeCollection.save({_from: itemCollectionName + '/ItemD', _to: itemCollectionName + '/ItemE'});
  }

  const graphDefinition = [ 
    { 
      "collection": edgeCollectionName, 
      "from":[itemCollectionName], 
      "to":[itemCollectionName]
    }
  ];

  const graph = graph_module._create(graphName, graphDefinition);
}

mainfest.json (должен находиться в корневой папке):

{
  "engines": {
    "arangodb": "^3.0.0"
  },
  "main": "index.js",
  "scripts": {
    "setup": "scripts/setup.js"
  }
}

index.js (должен находиться в корневой папке):

'use strict';
const createRouter = require('@arangodb/foxx/router');
const router = createRouter();
const joi = require('joi');

const db = require('@arangodb').db;
const aql = require('@arangodb').aql;

const recursionQuery = function(itemId, tree, depth) {
  const result = db._query(aql`
    FOR d IN myItems
    FILTER d._id == ${itemId}
    LET contains = (
      FOR c IN 1..1 OUTBOUND ${itemId} GRAPH 'myGraph' RETURN { "_id": c._id }
    )
    RETURN MERGE({"_id": d._id}, {"contains": contains})
  `);

  tree = result._documents[0];

  if (depth < 10) {
    if ((result._documents[0]) && (result._documents[0].contains) && (result._documents[0].contains.length > 0)) {
        for (var i = 0; i < result._documents[0].contains.length; i++) {
        tree.contains[i] = recursionQuery(result._documents[0].contains[i]._id, tree.contains[i], depth + 1);
        }
    }
  }
  return tree;
}

router.get('/tree/:rootId', function(req, res) {
  let myResult = recursionQuery('myItems/' + req.pathParams.rootId, {}, 0);
  res.send(myResult);
})
  .response(joi.object().required(), 'Tree of child nodes.')
  .summary('Tree of child nodes')
  .description('Tree of child nodes underneath the provided node.');

module.context.use(router);

Теперь вы можете вызвать конечную точку Foxx Microservice API, указав rootId, и она вернет полное дерево. Это очень быстро.

Пример вывода этого для ItemA:

{
  "_id": "myItems/ItemA",
  "contains": [
    {
      "_id": "myItems/ItemB",
      "contains": [
        {
          "_id": "myItems/ItemC",
          "contains": []
        },
        {
          "_id": "myItems/ItemD",
          "contains": [
            {
              "_id": "myItems/ItemE",
              "contains": []
            }
          ]
        }
      ]
    }
  ]
}

Вы можете видеть, что Item B содержит двух дочерних элементов, ItemC и ItemD, а затем ItemD также содержит ItemE.

Я не могу дождаться, пока ArangoDB AQL улучшит обработку путей переменной глубины в запросах стиля FOR v, e, p IN 1..100 OUTBOUND 'abc/def' GRAPH 'someGraph'. Настраиваемые посетители не рекомендуются для использования в 3.x, но на самом деле они не были заменены чем-то более мощным для обработки запросов с подстановочными знаками на глубине вершины в пути или обработки команд стиля prune или exclude при обходе пути.

Хотелось бы получить комментарии / отзывы, если это можно упростить.

person David Thomas    schedule 12.10.2016
comment
AQL поддерживает пути переменной глубины - вы можете получить любой слой глубины, используя path.vertices[n] или path.edges[n], где n - глубина. - person Nate Gardner; 13.10.2016
comment
Да, это так, но, к сожалению, вы должны указать n, вы не можете использовать подстановочный знак. Например, предположим, что вы хотите запросить все исходящие пути от узла глубиной 1..10, а затем выполнить специальное действие, если одно из ребер в этом пути имеет определенный атрибут или вершина в пути имеет ценность. В конечном итоге вы пишете код, который определяет n 10 раз, у вас огромные команды IF. Я бы хотел, чтобы он был таким же простым, как IF path.vertices[*].myKey == 'trigger', и он динамически обрабатывал все возможные глубины для этого графического запроса. Также имеется возможность отменить обработку пути при срабатывании триггера. - person David Thomas; 13.10.2016