//////////////////////////////////////////////////////////////////////////////// /// DISCLAIMER /// /// Copyright 2014-2016 ArangoDB GmbH, Cologne, Germany /// Copyright 2004-2014 triAGENS GmbH, Cologne, Germany /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. /// You may obtain a copy of the License at /// /// http://www.apache.org/licenses/LICENSE-2.0 /// /// Unless required by applicable law or agreed to in writing, software /// distributed under the License is distributed on an "AS IS" BASIS, /// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. /// See the License for the specific language governing permissions and /// limitations under the License. /// /// Copyright holder is ArangoDB GmbH, Cologne, Germany /// /// @author Markus Pfeiffer //////////////////////////////////////////////////////////////////////////////// #include "KShortestPathsFinder.h" #include "Aql/AqlValue.h" #include "Cluster/ServerState.h" #include "Graph/EdgeCursor.h" #include "Graph/EdgeDocumentToken.h" #include "Graph/ShortestPathOptions.h" #include "Graph/ShortestPathResult.h" #include "Graph/TraverserCache.h" #include "Transaction/Helpers.h" #include "Utils/OperationCursor.h" #include "VocBase/LogicalCollection.h" #include #include #include #include using namespace arangodb; using namespace arangodb::graph; // KShortestPathsFinder::KShortestPathsFinder(ShortestPathOptions& options) : ShortestPathFinder(options), _pathAvailable(false) {} KShortestPathsFinder::~KShortestPathsFinder() = default; // Sets up k-shortest-paths traversal from start to end bool KShortestPathsFinder::startKShortestPathsTraversal( arangodb::velocypack::Slice const& start, arangodb::velocypack::Slice const& end) { TRI_ASSERT(start.isString()); TRI_ASSERT(end.isString()); _start = arangodb::velocypack::StringRef(start); _end = arangodb::velocypack::StringRef(end); _pathAvailable = true; _shortestPaths.clear(); _candidatePaths.clear(); TRI_IF_FAILURE("TraversalOOMInitialize") { THROW_ARANGO_EXCEPTION(TRI_ERROR_DEBUG); } return true; } bool KShortestPathsFinder::computeShortestPath(VertexRef const& start, VertexRef const& end, std::unordered_set const& forbiddenVertices, std::unordered_set const& forbiddenEdges, Path& result) { Ball left(start, FORWARD); Ball right(end, BACKWARD); VertexRef join; result.clear(); auto currentBest = std::optional{}; // We will not improve anymore if we have found a best path and the smallest // combined distance between left and right is bigger than that path while (!left._frontier.empty() && !right._frontier.empty() && !(currentBest.has_value() && (left._closest + right._closest > currentBest.value()))) { _options.isQueryKilledCallback(); // Choose the smaller frontier to expand. if (left._frontier.size() < right._frontier.size()) { advanceFrontier(left, right, forbiddenVertices, forbiddenEdges, join, currentBest); } else { advanceFrontier(right, left, forbiddenVertices, forbiddenEdges, join, currentBest); } } if (currentBest.has_value()) { reconstructPath(left, right, join, result); return true; } else { // No path found return false; } } void KShortestPathsFinder::computeNeighbourhoodOfVertexCache(VertexRef vertex, Direction direction, std::vector*& res) { auto lookup = _vertexCache.try_emplace(vertex, FoundVertex(vertex)).first; auto& cache = lookup->second; // want to update the cached vertex in place switch (direction) { case BACKWARD: if (!cache._hasCachedInNeighbours) { computeNeighbourhoodOfVertex(vertex, direction, cache._inNeighbours); cache._hasCachedInNeighbours = true; } res = &cache._inNeighbours; break; case FORWARD: if (!cache._hasCachedOutNeighbours) { computeNeighbourhoodOfVertex(vertex, direction, cache._outNeighbours); cache._hasCachedOutNeighbours = true; } res = &cache._outNeighbours; break; default: TRI_ASSERT(false); } } void KShortestPathsFinder::computeNeighbourhoodOfVertex(VertexRef vertex, Direction direction, std::vector& steps) { std::unique_ptr edgeCursor; switch (direction) { case BACKWARD: edgeCursor.reset(_options.nextReverseCursor(vertex)); break; case FORWARD: edgeCursor.reset(_options.nextCursor(vertex)); break; default: TRI_ASSERT(false); } // TODO: This is a bit of a hack if (_options.useWeight()) { auto callback = [&](EdgeDocumentToken&& eid, VPackSlice edge, size_t cursorIdx) -> void { if (edge.isString()) { VPackSlice doc = _options.cache()->lookupToken(eid); double weight = _options.weightEdge(doc); if (edge.compareString(vertex.data(), vertex.length()) != 0) { VertexRef id = _options.cache()->persistString(VertexRef(edge)); steps.emplace_back(std::move(eid), id, weight); } } else { VertexRef other(transaction::helpers::extractFromFromDocument(edge)); if (other == vertex) { other = VertexRef(transaction::helpers::extractToFromDocument(edge)); } if (other != vertex) { VertexRef id = _options.cache()->persistString(other); steps.emplace_back(std::move(eid), id, _options.weightEdge(edge)); } } }; edgeCursor->readAll(callback); } else { auto callback = [&](EdgeDocumentToken&& eid, VPackSlice edge, size_t cursorIdx) -> void { if (edge.isString()) { if (edge.compareString(vertex.data(), vertex.length()) != 0) { VertexRef id = _options.cache()->persistString(VertexRef(edge)); steps.emplace_back(std::move(eid), id, 1); } } else { VertexRef other(transaction::helpers::extractFromFromDocument(edge)); if (other == vertex) { other = VertexRef(transaction::helpers::extractToFromDocument(edge)); } if (other != vertex) { VertexRef id = _options.cache()->persistString(other); steps.emplace_back(std::move(eid), id, 1); } } }; edgeCursor->readAll(callback); } } void KShortestPathsFinder::advanceFrontier(Ball& source, Ball const& target, std::unordered_set const& forbiddenVertices, std::unordered_set const& forbiddenEdges, VertexRef& join, std::optional& currentBest) { VertexRef vr; DijkstraInfo *v, *w; std::vector* neighbours; bool success = source._frontier.popMinimal(vr, v); TRI_ASSERT(v != nullptr); TRI_ASSERT(vr == v->_vertex); if (!success) { return; } computeNeighbourhoodOfVertexCache(vr, source._direction, neighbours); TRI_ASSERT(neighbours != nullptr); for (auto& s : *neighbours) { if (forbiddenEdges.find(s._edge) == forbiddenEdges.end() && forbiddenVertices.find(s._vertex) == forbiddenVertices.end()) { double weight = v->_weight + s._weight; auto lookup = source._frontier.find(s._vertex); if (lookup != nullptr) { if (lookup->_weight > weight) { source._frontier.lowerWeight(s._vertex, weight); lookup->_pred = vr; lookup->_edge = s._edge; lookup->_weight = weight; } } else { source._frontier.insert(s._vertex, std::make_unique(s._vertex, std::move(s._edge), vr, weight)); } } } v->_done = true; source._closest = v->_weight; w = target._frontier.find(v->_vertex); if (w != nullptr && w->_done) { // The total weight of the found path double totalWeight = v->_weight + w->_weight; if (!currentBest.has_value() || totalWeight < currentBest.value()) { join = v->_vertex; currentBest = totalWeight; } } } void KShortestPathsFinder::reconstructPath(Ball const& left, Ball const& right, VertexRef const& join, Path& result) { result.clear(); TRI_ASSERT(!join.empty()); result._vertices.emplace_back(join); DijkstraInfo* it; it = left._frontier.find(join); TRI_ASSERT(it != nullptr); double startToJoin = it->_weight; result._weight = startToJoin; while (it != nullptr && it->_weight > 0) { result._vertices.push_front(it->_pred); result._edges.push_front(it->_edge); result._weights.push_front(it->_weight); it = left._frontier.find(it->_pred); } // Initial vertex has weight 0 result._weights.push_front(0); it = right._frontier.find(join); TRI_ASSERT(it != nullptr); double joinToEnd = it->_weight; result._weight += joinToEnd; while (it != nullptr && it->_weight > 0) { result._vertices.emplace_back(it->_pred); result._edges.emplace_back(it->_edge); it = right._frontier.find(it->_pred); TRI_ASSERT(it != nullptr); // should run into 0 weight before result._weights.emplace_back(startToJoin + (joinToEnd - it->_weight)); } TRI_IF_FAILURE("TraversalOOMPath") { THROW_ARANGO_EXCEPTION(TRI_ERROR_DEBUG); } } bool KShortestPathsFinder::computeNextShortestPath(Path& result) { std::unordered_set forbiddenVertices; std::unordered_set forbiddenEdges; Path tmpPath, candidate; TRI_ASSERT(!_shortestPaths.empty()); auto& lastShortestPath = _shortestPaths.back(); bool available = false; for (size_t i = lastShortestPath._branchpoint; i + 1 < lastShortestPath.length(); ++i) { auto& spur = lastShortestPath._vertices.at(i); forbiddenVertices.clear(); forbiddenEdges.clear(); // Must not use vertices on the prefix for (size_t j = 0; j < i; ++j) { forbiddenVertices.emplace(lastShortestPath._vertices[j]); } // TODO: This can be done more efficiently by storing shortest // paths in a prefix/postfix tree // previous paths with same prefix must not be for (auto const& p : _shortestPaths) { bool eq = true; for (size_t e = 0; e < i; ++e) { if (!p._edges.at(e).equals(lastShortestPath._edges.at(e))) { eq = false; break; } } if (eq && (i < p._edges.size())) { forbiddenEdges.emplace(p._edges.at(i)); } } if (computeShortestPath(spur, _end, forbiddenVertices, forbiddenEdges, tmpPath)) { candidate.clear(); candidate.append(lastShortestPath, 0, i); candidate.append(tmpPath, 0, tmpPath.length() - 1); candidate._branchpoint = i; auto it = find_if(_candidatePaths.begin(), _candidatePaths.end(), [candidate](Path const& v) { return v._weight >= candidate._weight; }); if (it == _candidatePaths.end() || !(*it == candidate)) { _candidatePaths.emplace(it, candidate); } } } if (!_candidatePaths.empty()) { auto const& p = _candidatePaths.front(); result.clear(); result.append(p, 0, p.length() - 1); result._branchpoint = p._branchpoint; _candidatePaths.pop_front(); available = true; } return available; } bool KShortestPathsFinder::getNextPath(Path& result) { bool available = false; result.clear(); // TODO: this looks a bit ugly if (_shortestPaths.empty()) { if (_start == _end) { TRI_ASSERT(!_start.empty()); result._vertices.emplace_back(_start); result._weight = 0; available = true; } else { available = computeShortestPath(_start, _end, {}, {}, result); result._branchpoint = 0; } } else { if (_start == _end) { available = false; } else { available = computeNextShortestPath(result); } } if (available) { _shortestPaths.emplace_back(result); _options.fetchVerticesCoordinator(result._vertices); TRI_IF_FAILURE("TraversalOOMPath") { THROW_ARANGO_EXCEPTION(TRI_ERROR_DEBUG); } } _pathAvailable = available; return available; } bool KShortestPathsFinder::getNextPathShortestPathResult(ShortestPathResult& result) { Path path; result.clear(); if (getNextPath(path)) { result._vertices = path._vertices; result._edges = path._edges; return true; } else { return false; } } bool KShortestPathsFinder::getNextPathAql(arangodb::velocypack::Builder& result) { Path path; if (getNextPath(path)) { result.clear(); result.openObject(); result.add(VPackValue("edges")); result.openArray(); for (auto const& it : path._edges) { _options.cache()->insertEdgeIntoResult(it, result); } result.close(); // Array result.add(VPackValue("vertices")); result.openArray(); for (auto const& it : path._vertices) { _options.cache()->insertVertexIntoResult(it, result); } result.close(); // Array if (_options.useWeight()) { result.add("weight", VPackValue(path._weight)); } else { // If not using weight, weight is defined as 1 per edge result.add("weight", VPackValue(path._edges.size())); } result.close(); // Object TRI_ASSERT(result.isClosed()); return true; } else { return false; } }