Статьи

Поиск пути с Neo4j

В моем предыдущем посте я говорил о графических базах данных ( в частности, Neo4j ) и о том, как их можно применять к определенным классам задач, где данные могут иметь несколько степеней разделения в своих отношениях.

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

Рассмотрим график, который представляет карту города. Каждый узел на графике представляет собой пересечение двух или более улиц. Каждое направление улицы представлено в виде разных отношений: улицы с двусторонним движением будут иметь два отношения между одними и теми же двумя узлами, одно указывает от первого узла ко второму, а другое — от второго узла к первому. Улицы с односторонним движением будут иметь единую связь. Каждое отношение улицы также будет иметь свойство расстояния . Вот как может выглядеть одна такая карта:

Каждая улица с двусторонним движением, за исключением Market Alley и Park Drive. Числа в скобках представляют расстояние до улицы в сотнях ярдов (то есть 3 = 300 ярдов.)

Вот код для создания этого графа в базе данных с использованием
клиента REST, над которым я работал (инициализация соединения не показана):

$intersections = array(
    "First & Main",
    "Second & Main",
    "Third & Main",
    "First & Oak",
    "Second & Oak",
    "Third & Market",
);

$streets = array(
    // start, end, [direction, distance, name]
    array(0, 1, array('direction'=>'east', 'distance'=>2, 'name'=>'Main St.')),
    array(0, 3, array('direction'=>'south', 'distance'=>2, 'name'=>'First Ave.')),
    array(0, 4, array('direction'=>'south', 'distance'=>3, 'name'=>'Park Dr.')),

    array(1, 0, array('direction'=>'west', 'distance'=>2, 'name'=>'Main St.')),
    array(1, 2, array('direction'=>'east', 'distance'=>2, 'name'=>'Main St.')),
    array(1, 4, array('direction'=>'south', 'distance'=>2, 'name'=>'Second Ave.')),

    array(2, 1, array('direction'=>'west', 'distance'=>2, 'name'=>'Main St.')),
    array(2, 5, array('direction'=>'south', 'distance'=>1, 'name'=>'Third Ave.')),

    array(3, 0, array('direction'=>'north', 'distance'=>2, 'name'=>'First Ave.')),
    array(3, 4, array('direction'=>'east', 'distance'=>2, 'name'=>'Oak St')),

    array(4, 3, array('direction'=>'west', 'distance'=>2, 'name'=>'Oak St.')),
    array(4, 1, array('direction'=>'north', 'distance'=>2, 'name'=>'Second Ave.')),

    array(5, 2, array('direction'=>'north', 'distance'=>1, 'name'=>'Third Ave.')),
    array(5, 4, array('direction'=>'west', 'distance'=>2, 'name'=>'Market Alley')),
);

$nodes = array();
$intersectionIndex = new Index($client, Index::TypeNode, 'intersections');
foreach ($intersections as $intersection) {
    $node = new Node($client);
    $node->setProperty('name', $intersection)->save();
    $intersectionIndex->add($node, 'name', $node->getProperty('name'));
    $nodes[] = $node;
}

foreach ($streets as $street) {
    $start = $nodes[$street[0]];
    $end = $nodes[$street[1]];
    $properties = $street[2];
    $start->relateTo($end, 'CONNECTS')
        ->setProperties($properties)
        ->save();
}

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

Затем мы создаем каждый узел пересечения. Каждый узел также добавляется в индекс. Индексы позволяют быстро найти сохраненные узлы. Узлы и отношения могут быть проиндексированы, и индексация может происходить в любом узле или поле отношения. Вы даже можете индексировать поля, которые не существуют в узле или взаимосвязи. Мы будем использовать индекс позже, чтобы найти начальную и конечную точки нашего пути по имени.

Наконец, мы создаем отношения для каждой улицы. К отношениям могут быть прикреплены произвольные данные, как и к узлам.

Теперь мы создаем способ найти и отобразить направления движения от любого перекрестка до любого другого перекрестка:

$turns = array(
    'east' => array('north' => 'left','south' => 'right'),
    'west' => array('north' => 'right','south' => 'left'),
    'north' => array('east' => 'right','west' => 'left'),
    'south' => array('east' => 'left','west' => 'right'),
);

$fromNode = $intersectionIndex->findOne("Second & Oak");
$toNode = $intersectionIndex->findOne("Third & Main");

$paths = $fromNode->findPathsTo($toNode, 'CONNECTS', Relationship::DirectionOut)
    ->setMaxDepth(5)
    ->getPaths();

foreach ($paths as $i => $path) {
    $path->setContext(Path::ContextRelationship);
    $prevDirection = null;
    $totalDistance = 0;

    echo "Path " . ($i+1) .":\n";
    foreach ($path as $j => $rel) {
        $direction = $rel->getProperty('direction');
        $distance = $rel->getProperty('distance') * 100;
        $name = $rel->getProperty('name');

        if (!$prevDirection) {
            $action = 'Head';
        } else if ($prevDirection == $direction) {
            $action = 'Continue';
        } else {
            $turn = $turns[$prevDirection][$direction];
            $action = "Turn $turn, and continue";
        }
        $prevDirection = $direction;
        $step = $j+1;
        $totalDistance += $distance;

        echo "\t{$step}: {$action} {$direction} on {$name} for {$distance} yards.\n";
    }
    echo "\tTravel distance: {$totalDistance} yards\n\n";
}

Обратите внимание, что в вызове `findPathsTo` мы указываем, что нам нужны только исходящие отношения. Таким образом мы не можем получить указания, которые могут привести нас в неверном направлении по улице с односторонним движением. Мы также устанавливаем максимальную глубину, поскольку глубина поиска по умолчанию равна 1. Также обратите внимание на вызов `setContext`, который указывает путь, по которому мы хотим, чтобы` foreach` проходил через отношения, а не узлы. Запуск этого дает следующие направления:

Path 1:
    1: Head north on Second Ave. for 200 yards.
    2: Turn left, and continue west on Main St. for 200 yards.
    Travel distance: 400 yards

Мы вернули только один путь, потому что по умолчанию алгоритм поиска пути находит самый
короткий путь , то есть путь с наименьшим количеством отношений. Если переключить
в и
из перекрестков, мы можем получить направление обратно к исходной точке:

Path 1:
    1: Head west on Main St. for 200 yards.
    2: Turn left, and continue south on Second Ave. for 200 yards.
    Travel distance: 400 yards

Path 2:
    1: Head south on Third Ave. for 100 yards.
    2: Turn right, and continue west on Market Alley for 200 yards.
    Travel distance: 300 yards

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

$paths = $fromNode->findPathsTo($toNode, 'CONNECTS', Relationship::DirectionOut)
    ->setAlgorithm(PathFinder::AlgoDijkstra)
    ->setCostProperty('distance')
    ->setMaxDepth(5)
    ->getPaths();

Мы говорим искателю пути использовать
алгоритм Дейкстры , алгоритм для поиска пути с
наименьшей стоимостью , который отличается от самого
короткого пути. Поскольку наша карта использует расстояние,
стоимость пути является суммой всех свойств расстояния отношений в этом пути. Запуск скрипта теперь дает нам только один путь, путь с наименьшим расстоянием:

Path 1:
    1: Head south on Third Ave. for 100 yards.
    2: Turn right, and continue west on Market Alley for 200 yards.
    Travel distance: 300 yards

Если бы существовал другой путь с таким же общим расстоянием, он также был бы отображен. Стоимость пути игнорирует количество связей в пути. Если бы существовал путь с 3 отношениями на общую сумму 300 ярдов, а другой — с 1 отношением на общую сумму 300 ярдов, то отображались бы оба.

Neo4j имеет несколько встроенных алгоритмов поиска пути. Некоторые из них:

  • кратчайший путь — найти пути с наименьшим количеством отношений
  • dijkstra — найти пути с наименьшей стоимостью
  • простой путь — найти пути без повторяющихся узлов
  • все — найти все пути между двумя узлами

Neo4j также поставляется с выразительным API-интерфейсом обхода, который позволяет создавать собственные алгоритмы поиска пути для конкретных задач.