mirror of https://gitee.com/bigwinds/arangodb
Fixed AttribtueWeighted ShortestPath computation.
This commit is contained in:
parent
034b38f0cc
commit
5f33a95a82
|
@ -41,157 +41,6 @@ typedef arangodb::graph::AttributeWeightShortestPathFinder ArangoDBPathFinder;
|
|||
|
||||
using namespace arangodb::aql;
|
||||
|
||||
/// @brief Local class to expand edges.
|
||||
/// Will be handed over to the path finder
|
||||
namespace arangodb {
|
||||
namespace aql {
|
||||
|
||||
/// @brief Expander for weighted edges
|
||||
struct EdgeWeightExpanderLocal {
|
||||
private:
|
||||
/// @brief reference to the Block
|
||||
ShortestPathBlock const* _block;
|
||||
|
||||
/// @brief Defines if this expander follows the edges in reverse
|
||||
bool _reverse;
|
||||
|
||||
public:
|
||||
EdgeWeightExpanderLocal(ShortestPathBlock const* block, bool reverse)
|
||||
: _block(block), _reverse(reverse) {}
|
||||
|
||||
void inserter(std::unordered_map<VPackSlice, size_t>& candidates,
|
||||
std::vector<ArangoDBPathFinder::Step*>& result,
|
||||
VPackSlice const& s, VPackSlice const& t, double currentWeight,
|
||||
VPackSlice edge) {
|
||||
auto cand = candidates.find(t);
|
||||
if (cand == candidates.end()) {
|
||||
// Add weight
|
||||
auto step =
|
||||
std::make_unique<ArangoDBPathFinder::Step>(t, s, currentWeight, edge);
|
||||
result.emplace_back(step.release());
|
||||
candidates.emplace(t, result.size() - 1);
|
||||
} else {
|
||||
// Compare weight
|
||||
auto old = result[cand->second];
|
||||
auto oldWeight = old->weight();
|
||||
if (currentWeight < oldWeight) {
|
||||
old->setWeight(currentWeight);
|
||||
old->_predecessor = s;
|
||||
old->_edge = edge;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void operator()(VPackSlice const& source,
|
||||
std::vector<ArangoDBPathFinder::Step*>& result) {
|
||||
TRI_ASSERT(source.isString());
|
||||
std::string id = source.copyString();
|
||||
ManagedDocumentResult* mmdr = _block->_mmdr.get();
|
||||
std::unique_ptr<arangodb::OperationCursor> edgeCursor;
|
||||
std::unordered_map<VPackSlice, size_t> candidates;
|
||||
for (auto const& edgeCollection : _block->_collectionInfos) {
|
||||
TRI_ASSERT(edgeCollection != nullptr);
|
||||
if (_reverse) {
|
||||
edgeCursor = edgeCollection->getReverseEdges(id, mmdr);
|
||||
} else {
|
||||
edgeCursor = edgeCollection->getEdges(id, mmdr);
|
||||
}
|
||||
|
||||
candidates.clear();
|
||||
|
||||
LogicalCollection* collection = edgeCursor->collection();
|
||||
auto cb = [&](DocumentIdentifierToken const& element) {
|
||||
if (collection->readDocument(_block->transaction(), element, *mmdr)) {
|
||||
VPackSlice edge(mmdr->vpack());
|
||||
VPackSlice from = transaction::helpers::extractFromFromDocument(edge);
|
||||
VPackSlice to = transaction::helpers::extractToFromDocument(edge);
|
||||
double currentWeight = edgeCollection->weightEdge(edge);
|
||||
if (from == source) {
|
||||
inserter(candidates, result, from, to, currentWeight, edge);
|
||||
} else {
|
||||
inserter(candidates, result, to, from, currentWeight, edge);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
while (edgeCursor->getMore(cb, 1000)) {
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/// @brief Expander for weighted edges
|
||||
struct EdgeWeightExpanderCluster {
|
||||
private:
|
||||
/// @brief reference to the Block
|
||||
ShortestPathBlock* _block;
|
||||
|
||||
/// @brief Defines if this expander follows the edges in reverse
|
||||
bool _reverse;
|
||||
|
||||
public:
|
||||
EdgeWeightExpanderCluster(ShortestPathBlock* block, bool reverse)
|
||||
: _block(block), _reverse(reverse) {}
|
||||
|
||||
void operator()(VPackSlice const& source,
|
||||
std::vector<ArangoDBPathFinder::Step*>& result) {
|
||||
int res = TRI_ERROR_NO_ERROR;
|
||||
std::unordered_map<VPackSlice, size_t> candidates;
|
||||
|
||||
for (auto const& edgeCollection : _block->_collectionInfos) {
|
||||
TRI_ASSERT(edgeCollection != nullptr);
|
||||
VPackBuilder edgesBuilder;
|
||||
if (_reverse) {
|
||||
res = edgeCollection->getReverseEdgesCoordinator(source, edgesBuilder);
|
||||
} else {
|
||||
res = edgeCollection->getEdgesCoordinator(source, edgesBuilder);
|
||||
}
|
||||
|
||||
if (res != TRI_ERROR_NO_ERROR) {
|
||||
THROW_ARANGO_EXCEPTION(res);
|
||||
}
|
||||
|
||||
candidates.clear();
|
||||
|
||||
auto inserter = [&](VPackSlice const& s, VPackSlice const& t,
|
||||
double currentWeight, VPackSlice const& edge) {
|
||||
auto cand = candidates.find(t);
|
||||
if (cand == candidates.end()) {
|
||||
// Add weight
|
||||
auto step = std::make_unique<ArangoDBPathFinder::Step>(
|
||||
t, s, currentWeight, edge);
|
||||
result.emplace_back(step.release());
|
||||
candidates.emplace(t, result.size() - 1);
|
||||
} else {
|
||||
// Compare weight
|
||||
auto old = result[cand->second];
|
||||
auto oldWeight = old->weight();
|
||||
if (currentWeight < oldWeight) {
|
||||
old->setWeight(currentWeight);
|
||||
old->_predecessor = s;
|
||||
old->_edge = edge;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
VPackSlice edges = edgesBuilder.slice().get("edges");
|
||||
for (auto const& edge : VPackArrayIterator(edges)) {
|
||||
VPackSlice from = transaction::helpers::extractFromFromDocument(edge);
|
||||
VPackSlice to = transaction::helpers::extractToFromDocument(edge);
|
||||
double currentWeight = edgeCollection->weightEdge(edge);
|
||||
if (from == source) {
|
||||
inserter(from, to, currentWeight, edge);
|
||||
} else {
|
||||
inserter(to, from, currentWeight, edge);
|
||||
}
|
||||
}
|
||||
_block->_coordinatorCache.emplace_back(edgesBuilder.steal());
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
ShortestPathBlock::ShortestPathBlock(ExecutionEngine* engine,
|
||||
ShortestPathNode const* ep)
|
||||
: ExecutionBlock(engine, ep),
|
||||
|
@ -252,21 +101,19 @@ ShortestPathBlock::ShortestPathBlock(ExecutionEngine* engine,
|
|||
|
||||
if (arangodb::ServerState::instance()->isCoordinator()) {
|
||||
if (_opts->useWeight()) {
|
||||
_finder.reset(new arangodb::graph::AttributeWeightShortestPathFinder(
|
||||
EdgeWeightExpanderCluster(this, false),
|
||||
EdgeWeightExpanderCluster(this, true), _opts->bidirectional));
|
||||
_finder.reset(
|
||||
new arangodb::graph::AttributeWeightShortestPathFinder(_opts));
|
||||
} else {
|
||||
_finder.reset(
|
||||
new arangodb::graph::ConstantWeightShortestPathFinder(this));
|
||||
new arangodb::graph::ConstantWeightShortestPathFinder(_opts));
|
||||
}
|
||||
} else {
|
||||
if (_opts->useWeight()) {
|
||||
_finder.reset(new arangodb::graph::AttributeWeightShortestPathFinder(
|
||||
EdgeWeightExpanderLocal(this, false),
|
||||
EdgeWeightExpanderLocal(this, true), _opts->bidirectional));
|
||||
_finder.reset(
|
||||
new arangodb::graph::AttributeWeightShortestPathFinder(_opts));
|
||||
} else {
|
||||
_finder.reset(
|
||||
new arangodb::graph::ConstantWeightShortestPathFinder(this));
|
||||
new arangodb::graph::ConstantWeightShortestPathFinder(_opts));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -457,18 +304,14 @@ AqlItemBlock* ShortestPathBlock::getSome(size_t, size_t atMost) {
|
|||
// only copy 1st row of registers inherited from previous frame(s)
|
||||
inheritRegisters(cur, res.get(), _pos);
|
||||
|
||||
// TODO: lease builder?
|
||||
VPackBuilder resultBuilder;
|
||||
for (size_t j = 0; j < toSend; j++) {
|
||||
if (usesVertexOutput()) {
|
||||
resultBuilder.clear();
|
||||
_path->vertexToVelocyPack(_trx, _mmdr.get(), _posInPath, resultBuilder);
|
||||
res->setValue(j, _vertexReg, AqlValue(resultBuilder.slice()));
|
||||
res->setValue(j, _vertexReg,
|
||||
_path->vertexToAqlValue(_opts->cache(), _posInPath));
|
||||
}
|
||||
if (usesEdgeOutput()) {
|
||||
resultBuilder.clear();
|
||||
_path->edgeToVelocyPack(_trx, _mmdr.get(), _posInPath, resultBuilder);
|
||||
res->setValue(j, _edgeReg, AqlValue(resultBuilder.slice()));
|
||||
res->setValue(j, _edgeReg,
|
||||
_path->edgeToAqlValue(_opts->cache(), _posInPath));
|
||||
}
|
||||
if (j > 0) {
|
||||
// re-use already copied aqlvalues
|
||||
|
|
|
@ -25,12 +25,275 @@
|
|||
|
||||
#include "Basics/Exceptions.h"
|
||||
#include "Basics/StringRef.h"
|
||||
#include "Graph/EdgeCursor.h"
|
||||
#include "Graph/ShortestPathOptions.h"
|
||||
#include "Graph/ShortestPathResult.h"
|
||||
#include "Transaction/Helpers.h"
|
||||
#include "VocBase/ManagedDocumentResult.h"
|
||||
#include "VocBase/TraverserCache.h"
|
||||
|
||||
#include <velocypack/Slice.h>
|
||||
#include <velocypack/velocypack-aliases.h>
|
||||
|
||||
using namespace arangodb;
|
||||
using namespace arangodb::graph;
|
||||
|
||||
AttributeWeightShortestPathFinder::Searcher::Searcher(
|
||||
AttributeWeightShortestPathFinder* pathFinder, ThreadInfo& myInfo,
|
||||
ThreadInfo& peerInfo, arangodb::StringRef const& start,
|
||||
bool isBackward)
|
||||
: _pathFinder(pathFinder),
|
||||
_myInfo(myInfo),
|
||||
_peerInfo(peerInfo),
|
||||
_start(start),
|
||||
_isBackward(isBackward) {}
|
||||
|
||||
void AttributeWeightShortestPathFinder::Searcher::insertNeighbor(
|
||||
Step* step, double newWeight) {
|
||||
Step* s = _myInfo._pq.find(step->_vertex);
|
||||
|
||||
// Not found, so insert it:
|
||||
if (s == nullptr) {
|
||||
step->setWeight(newWeight);
|
||||
_myInfo._pq.insert(step->_vertex, step);
|
||||
return;
|
||||
}
|
||||
if (!s->_done && s->weight() > newWeight) {
|
||||
s->_predecessor = step->_predecessor;
|
||||
s->_edge = step->_edge;
|
||||
_myInfo._pq.lowerWeight(s->_vertex, newWeight);
|
||||
}
|
||||
delete step;
|
||||
}
|
||||
|
||||
void AttributeWeightShortestPathFinder::Searcher::lookupPeer(
|
||||
arangodb::StringRef& vertex, double weight) {
|
||||
Step* s = _peerInfo._pq.find(vertex);
|
||||
|
||||
if (s == nullptr) {
|
||||
// Not found, nothing more to do
|
||||
return;
|
||||
}
|
||||
double total = s->weight() + weight;
|
||||
|
||||
// Update the highscore:
|
||||
if (!_pathFinder->_highscoreSet || total < _pathFinder->_highscore) {
|
||||
_pathFinder->_highscoreSet = true;
|
||||
_pathFinder->_highscore = total;
|
||||
_pathFinder->_intermediate = vertex;
|
||||
_pathFinder->_intermediateSet = true;
|
||||
}
|
||||
|
||||
// Now the highscore is set!
|
||||
|
||||
// Did we find a solution together with the other thread?
|
||||
if (s->_done) {
|
||||
if (total <= _pathFinder->_highscore) {
|
||||
_pathFinder->_intermediate = vertex;
|
||||
_pathFinder->_intermediateSet = true;
|
||||
}
|
||||
// Hacki says: If the highscore was set, and even if
|
||||
// it is better than total, then this observation here
|
||||
// proves that it will never be better, so: BINGO.
|
||||
_pathFinder->_bingo = true;
|
||||
// We found a way, but somebody else found a better way,
|
||||
// so this is not the shortest path
|
||||
return;
|
||||
}
|
||||
|
||||
// Did we find a solution on our own? This is for the
|
||||
// single thread case and for the case that the other
|
||||
// thread is too slow to even finish its own start vertex!
|
||||
if (s->weight() == 0) {
|
||||
// We have found the target, we have finished all
|
||||
// vertices with a smaller weight than this one (and did
|
||||
// not succeed), so this must be a best solution:
|
||||
_pathFinder->_intermediate = vertex;
|
||||
_pathFinder->_intermediateSet = true;
|
||||
_pathFinder->_bingo = true;
|
||||
}
|
||||
}
|
||||
|
||||
bool AttributeWeightShortestPathFinder::Searcher::oneStep() {
|
||||
arangodb::StringRef v;
|
||||
Step* s = nullptr;
|
||||
bool b = _myInfo._pq.popMinimal(v, s, true);
|
||||
|
||||
if (_pathFinder->_bingo || !b) {
|
||||
// We can leave this functino only under 2 conditions:
|
||||
// 1) already bingo==true => bingo = true no effect
|
||||
// 2) This queue is empty => if there would be a
|
||||
// path we would have found it here
|
||||
// => No path possible. Set bingo, intermediate is empty.
|
||||
_pathFinder->_bingo = true;
|
||||
return false;
|
||||
}
|
||||
|
||||
TRI_ASSERT(s != nullptr);
|
||||
|
||||
std::vector<Step*> neighbors;
|
||||
_pathFinder->expandVertex(_isBackward, v, neighbors);
|
||||
for (Step* neighbor : neighbors) {
|
||||
insertNeighbor(neighbor, s->weight() + neighbor->weight());
|
||||
}
|
||||
lookupPeer(v, s->weight());
|
||||
|
||||
Step* s2 = _myInfo._pq.find(v);
|
||||
s2->_done = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
AttributeWeightShortestPathFinder::AttributeWeightShortestPathFinder(
|
||||
ShortestPathOptions* options)
|
||||
: _highscoreSet(false),
|
||||
_highscore(0),
|
||||
_bingo(false),
|
||||
_resultCode(TRI_ERROR_NO_ERROR),
|
||||
_intermediateSet(false),
|
||||
_intermediate(),
|
||||
_mmdr(new ManagedDocumentResult{}),
|
||||
_options(options) {}
|
||||
|
||||
AttributeWeightShortestPathFinder::~AttributeWeightShortestPathFinder(){};
|
||||
|
||||
bool AttributeWeightShortestPathFinder::shortestPath(
|
||||
arangodb::velocypack::Slice const& st,
|
||||
arangodb::velocypack::Slice const& ta, ShortestPathResult& result,
|
||||
std::function<void()> const& callback) {
|
||||
// For the result:
|
||||
result.clear();
|
||||
_highscoreSet = false;
|
||||
_highscore = 0;
|
||||
_bingo = false;
|
||||
_intermediateSet = false;
|
||||
|
||||
StringRef start = _options->cache()->persistString(StringRef(st));
|
||||
StringRef target = _options->cache()->persistString(StringRef(ta));
|
||||
|
||||
// Forward with initialization:
|
||||
arangodb::StringRef emptyVertex;
|
||||
arangodb::StringRef emptyEdge;
|
||||
ThreadInfo forward;
|
||||
forward._pq.insert(start, new Step(start, emptyVertex, 0, emptyEdge));
|
||||
|
||||
// backward with initialization:
|
||||
ThreadInfo backward;
|
||||
backward._pq.insert(target, new Step(target, emptyVertex, 0, emptyEdge));
|
||||
|
||||
// Now the searcher threads:
|
||||
Searcher forwardSearcher(this, forward, backward, start, false);
|
||||
std::unique_ptr<Searcher> backwardSearcher;
|
||||
if (_options->bidirectional) {
|
||||
backwardSearcher.reset(new Searcher(this, backward, forward, target, true));
|
||||
}
|
||||
|
||||
TRI_IF_FAILURE("TraversalOOMInitialize") {
|
||||
THROW_ARANGO_EXCEPTION(TRI_ERROR_DEBUG);
|
||||
}
|
||||
|
||||
int counter = 0;
|
||||
|
||||
while (!_bingo) {
|
||||
if (!forwardSearcher.oneStep()) {
|
||||
break;
|
||||
}
|
||||
if (_options->bidirectional && !backwardSearcher->oneStep()) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (++counter == 10) {
|
||||
// check for abortion
|
||||
callback();
|
||||
counter = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (!_bingo || _intermediateSet == false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Step* s = forward._pq.find(_intermediate);
|
||||
result._vertices.emplace_back(_intermediate);
|
||||
|
||||
// FORWARD Go path back from intermediate -> start.
|
||||
// Insert all vertices and edges at front of vector
|
||||
// Do NOT! insert the intermediate vertex
|
||||
while (!s->_predecessor.empty()) {
|
||||
// TODO FIXME
|
||||
result._edges.push_front(StringRef(s->_edge));
|
||||
result._vertices.push_front(StringRef(s->_predecessor));
|
||||
s = forward._pq.find(s->_predecessor);
|
||||
}
|
||||
|
||||
// BACKWARD Go path back from intermediate -> target.
|
||||
// Insert all vertices and edges at back of vector
|
||||
// Also insert the intermediate vertex
|
||||
s = backward._pq.find(_intermediate);
|
||||
while (!s->_predecessor.empty()) {
|
||||
result._edges.emplace_back(StringRef(s->_edge));
|
||||
result._vertices.emplace_back(StringRef(s->_predecessor));
|
||||
s = backward._pq.find(s->_predecessor);
|
||||
}
|
||||
|
||||
TRI_IF_FAILURE("TraversalOOMPath") {
|
||||
THROW_ARANGO_EXCEPTION(TRI_ERROR_DEBUG);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void AttributeWeightShortestPathFinder::inserter(
|
||||
std::unordered_map<StringRef, size_t>& candidates,
|
||||
std::vector<Step*>& result, StringRef const& s,
|
||||
StringRef const& t, double currentWeight, StringRef edge) {
|
||||
auto cand = candidates.find(t);
|
||||
if (cand == candidates.end()) {
|
||||
// Add weight
|
||||
auto step =
|
||||
std::make_unique<Step>(t, s, currentWeight, edge);
|
||||
result.emplace_back(step.release());
|
||||
candidates.emplace(t, result.size() - 1);
|
||||
} else {
|
||||
// Compare weight
|
||||
auto old = result[cand->second];
|
||||
auto oldWeight = old->weight();
|
||||
if (currentWeight < oldWeight) {
|
||||
old->setWeight(currentWeight);
|
||||
old->_predecessor = s;
|
||||
old->_edge = edge;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void AttributeWeightShortestPathFinder::expandVertex(
|
||||
bool isBackward,
|
||||
arangodb::StringRef const& vertex,
|
||||
std::vector<Step*>& result) {
|
||||
std::unique_ptr<EdgeCursor> edgeCursor;
|
||||
if (isBackward) {
|
||||
edgeCursor.reset(_options->nextReverseCursor(_mmdr.get(), vertex));
|
||||
} else {
|
||||
edgeCursor.reset(_options->nextCursor(_mmdr.get(), vertex));
|
||||
}
|
||||
|
||||
std::unordered_map<StringRef, size_t> candidates;
|
||||
auto callback = [&] (arangodb::StringRef const& eid, VPackSlice edge, size_t cursorIdx) -> void {
|
||||
StringRef fromTmp(transaction::helpers::extractFromFromDocument(edge));
|
||||
StringRef toTmp(transaction::helpers::extractToFromDocument(edge));
|
||||
StringRef from = _options->cache()->persistString(fromTmp);
|
||||
StringRef to = _options->cache()->persistString(toTmp);
|
||||
double currentWeight = _options->weightEdge(edge);
|
||||
if (from == vertex) {
|
||||
inserter(candidates, result, from, to, currentWeight, eid);
|
||||
} else {
|
||||
inserter(candidates, result, to, from, currentWeight, eid);
|
||||
}
|
||||
};
|
||||
|
||||
edgeCursor->readAll(callback);
|
||||
}
|
||||
|
||||
/*
|
||||
AttributeWeightShortestPathFinder::SearcherTwoThreads::SearcherTwoThreads(
|
||||
AttributeWeightShortestPathFinder* pathFinder, ThreadInfo& myInfo,
|
||||
ThreadInfo& peerInfo, arangodb::velocypack::Slice const& start,
|
||||
|
@ -167,211 +430,11 @@ void AttributeWeightShortestPathFinder::SearcherTwoThreads::start() {
|
|||
void AttributeWeightShortestPathFinder::SearcherTwoThreads::join() {
|
||||
_thread.join();
|
||||
}
|
||||
*/
|
||||
|
||||
AttributeWeightShortestPathFinder::Searcher::Searcher(
|
||||
AttributeWeightShortestPathFinder* pathFinder, ThreadInfo& myInfo,
|
||||
ThreadInfo& peerInfo, arangodb::velocypack::Slice const& start,
|
||||
ExpanderFunction expander, std::string const& id)
|
||||
: _pathFinder(pathFinder),
|
||||
_myInfo(myInfo),
|
||||
_peerInfo(peerInfo),
|
||||
_start(start),
|
||||
_expander(expander),
|
||||
_id(id) {}
|
||||
|
||||
void AttributeWeightShortestPathFinder::Searcher::insertNeighbor(
|
||||
Step* step, double newWeight) {
|
||||
Step* s = _myInfo._pq.find(step->_vertex);
|
||||
|
||||
// Not found, so insert it:
|
||||
if (s == nullptr) {
|
||||
step->setWeight(newWeight);
|
||||
_myInfo._pq.insert(step->_vertex, step);
|
||||
return;
|
||||
}
|
||||
if (!s->_done && s->weight() > newWeight) {
|
||||
s->_predecessor = step->_predecessor;
|
||||
s->_edge = step->_edge;
|
||||
_myInfo._pq.lowerWeight(s->_vertex, newWeight);
|
||||
}
|
||||
delete step;
|
||||
}
|
||||
|
||||
void AttributeWeightShortestPathFinder::Searcher::lookupPeer(
|
||||
arangodb::velocypack::Slice& vertex, double weight) {
|
||||
Step* s = _peerInfo._pq.find(vertex);
|
||||
|
||||
if (s == nullptr) {
|
||||
// Not found, nothing more to do
|
||||
return;
|
||||
}
|
||||
double total = s->weight() + weight;
|
||||
|
||||
// Update the highscore:
|
||||
if (!_pathFinder->_highscoreSet || total < _pathFinder->_highscore) {
|
||||
_pathFinder->_highscoreSet = true;
|
||||
_pathFinder->_highscore = total;
|
||||
_pathFinder->_intermediate = vertex;
|
||||
_pathFinder->_intermediateSet = true;
|
||||
}
|
||||
|
||||
// Now the highscore is set!
|
||||
|
||||
// Did we find a solution together with the other thread?
|
||||
if (s->_done) {
|
||||
if (total <= _pathFinder->_highscore) {
|
||||
_pathFinder->_intermediate = vertex;
|
||||
_pathFinder->_intermediateSet = true;
|
||||
}
|
||||
// Hacki says: If the highscore was set, and even if
|
||||
// it is better than total, then this observation here
|
||||
// proves that it will never be better, so: BINGO.
|
||||
_pathFinder->_bingo = true;
|
||||
// We found a way, but somebody else found a better way,
|
||||
// so this is not the shortest path
|
||||
return;
|
||||
}
|
||||
|
||||
// Did we find a solution on our own? This is for the
|
||||
// single thread case and for the case that the other
|
||||
// thread is too slow to even finish its own start vertex!
|
||||
if (s->weight() == 0) {
|
||||
// We have found the target, we have finished all
|
||||
// vertices with a smaller weight than this one (and did
|
||||
// not succeed), so this must be a best solution:
|
||||
_pathFinder->_intermediate = vertex;
|
||||
_pathFinder->_intermediateSet = true;
|
||||
_pathFinder->_bingo = true;
|
||||
}
|
||||
}
|
||||
|
||||
bool AttributeWeightShortestPathFinder::Searcher::oneStep() {
|
||||
arangodb::velocypack::Slice v;
|
||||
Step* s = nullptr;
|
||||
bool b = _myInfo._pq.popMinimal(v, s, true);
|
||||
|
||||
if (_pathFinder->_bingo || !b) {
|
||||
// We can leave this functino only under 2 conditions:
|
||||
// 1) already bingo==true => bingo = true no effect
|
||||
// 2) This queue is empty => if there would be a
|
||||
// path we would have found it here
|
||||
// => No path possible. Set bingo, intermediate is empty.
|
||||
_pathFinder->_bingo = true;
|
||||
return false;
|
||||
}
|
||||
|
||||
TRI_ASSERT(s != nullptr);
|
||||
|
||||
std::vector<Step*> neighbors;
|
||||
_expander(v, neighbors);
|
||||
for (Step* neighbor : neighbors) {
|
||||
insertNeighbor(neighbor, s->weight() + neighbor->weight());
|
||||
}
|
||||
lookupPeer(v, s->weight());
|
||||
|
||||
Step* s2 = _myInfo._pq.find(v);
|
||||
s2->_done = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
AttributeWeightShortestPathFinder::AttributeWeightShortestPathFinder(
|
||||
ExpanderFunction&& forwardExpander, ExpanderFunction&& backwardExpander,
|
||||
bool bidirectional)
|
||||
: _highscoreSet(false),
|
||||
_highscore(0),
|
||||
_bingo(false),
|
||||
_resultCode(TRI_ERROR_NO_ERROR),
|
||||
_intermediateSet(false),
|
||||
_intermediate(),
|
||||
_forwardExpander(forwardExpander),
|
||||
_backwardExpander(backwardExpander),
|
||||
_bidirectional(bidirectional){};
|
||||
|
||||
bool AttributeWeightShortestPathFinder::shortestPath(
|
||||
arangodb::velocypack::Slice const& start,
|
||||
arangodb::velocypack::Slice const& target,
|
||||
ShortestPathResult& result,
|
||||
std::function<void()> const& callback) {
|
||||
// For the result:
|
||||
result.clear();
|
||||
_highscoreSet = false;
|
||||
_highscore = 0;
|
||||
_bingo = false;
|
||||
_intermediateSet = false;
|
||||
|
||||
// Forward with initialization:
|
||||
arangodb::velocypack::Slice emptyVertex;
|
||||
arangodb::velocypack::Slice emptyEdge;
|
||||
ThreadInfo forward;
|
||||
forward._pq.insert(start, new Step(start, emptyVertex, 0, emptyEdge));
|
||||
|
||||
// backward with initialization:
|
||||
ThreadInfo backward;
|
||||
backward._pq.insert(target, new Step(target, emptyVertex, 0, emptyEdge));
|
||||
|
||||
// Now the searcher threads:
|
||||
Searcher forwardSearcher(this, forward, backward, start, _forwardExpander,
|
||||
"Forward");
|
||||
std::unique_ptr<Searcher> backwardSearcher;
|
||||
if (_bidirectional) {
|
||||
backwardSearcher.reset(new Searcher(this, backward, forward, target,
|
||||
_backwardExpander, "Backward"));
|
||||
}
|
||||
|
||||
TRI_IF_FAILURE("TraversalOOMInitialize") {
|
||||
THROW_ARANGO_EXCEPTION(TRI_ERROR_DEBUG);
|
||||
}
|
||||
|
||||
int counter = 0;
|
||||
|
||||
while (!_bingo) {
|
||||
if (!forwardSearcher.oneStep()) {
|
||||
break;
|
||||
}
|
||||
if (_bidirectional && !backwardSearcher->oneStep()) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (++counter == 10) {
|
||||
// check for abortion
|
||||
callback();
|
||||
counter = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (!_bingo || _intermediateSet == false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Step* s = forward._pq.find(_intermediate);
|
||||
result._vertices.emplace_back(_intermediate);
|
||||
|
||||
// FORWARD Go path back from intermediate -> start.
|
||||
// Insert all vertices and edges at front of vector
|
||||
// Do NOT! insert the intermediate vertex
|
||||
while (!s->_predecessor.isNone()) {
|
||||
// TODO FIXME
|
||||
result._edges.push_front(StringRef(s->_edge));
|
||||
result._vertices.push_front(StringRef(s->_predecessor));
|
||||
s = forward._pq.find(s->_predecessor);
|
||||
}
|
||||
|
||||
// BACKWARD Go path back from intermediate -> target.
|
||||
// Insert all vertices and edges at back of vector
|
||||
// Also insert the intermediate vertex
|
||||
s = backward._pq.find(_intermediate);
|
||||
while (!s->_predecessor.isNone()) {
|
||||
result._edges.emplace_back(StringRef(s->_edge));
|
||||
result._vertices.emplace_back(StringRef(s->_predecessor));
|
||||
s = backward._pq.find(s->_predecessor);
|
||||
}
|
||||
|
||||
TRI_IF_FAILURE("TraversalOOMPath") {
|
||||
THROW_ARANGO_EXCEPTION(TRI_ERROR_DEBUG);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/* Here is a proof for the correctness of this algorithm:
|
||||
*
|
||||
|
@ -444,6 +507,8 @@ bool AttributeWeightShortestPathFinder::shortestPath(
|
|||
* algorithm is correct.
|
||||
*/
|
||||
|
||||
/* Unused code. Maybe reactivated
|
||||
|
||||
bool AttributeWeightShortestPathFinder::shortestPathTwoThreads(
|
||||
arangodb::velocypack::Slice& start, arangodb::velocypack::Slice& target,
|
||||
ShortestPathResult& result) {
|
||||
|
@ -525,3 +590,4 @@ bool AttributeWeightShortestPathFinder::shortestPathTwoThreads(
|
|||
|
||||
return true;
|
||||
}
|
||||
*/
|
||||
|
|
|
@ -26,16 +26,21 @@
|
|||
|
||||
#include "Basics/Mutex.h"
|
||||
#include "Basics/MutexLocker.h"
|
||||
#include "Basics/StringRef.h"
|
||||
|
||||
#include "Graph/ShortestPathFinder.h"
|
||||
#include "Graph/ShortestPathPriorityQueue.h"
|
||||
|
||||
#include <thread>
|
||||
|
||||
|
||||
namespace arangodb {
|
||||
|
||||
class ManagedDocumentResult;
|
||||
|
||||
namespace graph {
|
||||
|
||||
class ShortestPathOptions;
|
||||
|
||||
class AttributeWeightShortestPathFinder : public ShortestPathFinder {
|
||||
public:
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
|
@ -47,16 +52,16 @@ class AttributeWeightShortestPathFinder : public ShortestPathFinder {
|
|||
double _weight;
|
||||
|
||||
public:
|
||||
arangodb::velocypack::Slice _vertex;
|
||||
arangodb::velocypack::Slice _predecessor;
|
||||
arangodb::velocypack::Slice _edge;
|
||||
arangodb::StringRef _vertex;
|
||||
arangodb::StringRef _predecessor;
|
||||
arangodb::StringRef _edge;
|
||||
bool _done;
|
||||
|
||||
Step() : _weight(0.0), _done(false) {}
|
||||
|
||||
Step(arangodb::velocypack::Slice const& vert,
|
||||
arangodb::velocypack::Slice const& pred, double weig,
|
||||
arangodb::velocypack::Slice const& edge)
|
||||
Step(arangodb::StringRef const& vert,
|
||||
arangodb::StringRef const& pred, double weig,
|
||||
arangodb::StringRef const& edge)
|
||||
: _weight(weig),
|
||||
_vertex(vert),
|
||||
_predecessor(pred),
|
||||
|
@ -67,7 +72,7 @@ class AttributeWeightShortestPathFinder : public ShortestPathFinder {
|
|||
|
||||
void setWeight(double w) { _weight = w; }
|
||||
|
||||
arangodb::velocypack::Slice const& getKey() const { return _vertex; }
|
||||
arangodb::StringRef const& getKey() const { return _vertex; }
|
||||
};
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
|
@ -76,20 +81,12 @@ class AttributeWeightShortestPathFinder : public ShortestPathFinder {
|
|||
|
||||
typedef enum { FORWARD, BACKWARD } Direction;
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
/// @brief callback to find neighbors
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
typedef std::function<void(arangodb::velocypack::Slice const&,
|
||||
std::vector<Step*>&)>
|
||||
ExpanderFunction;
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
/// @brief our specialization of the priority queue
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
typedef arangodb::graph::ShortestPathPriorityQueue<
|
||||
arangodb::velocypack::Slice, Step, double>
|
||||
arangodb::StringRef, Step, double>
|
||||
PQueue;
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
|
@ -105,6 +102,7 @@ class AttributeWeightShortestPathFinder : public ShortestPathFinder {
|
|||
/// @brief a Dijkstra searcher for the multi-threaded search
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
/*
|
||||
class SearcherTwoThreads {
|
||||
AttributeWeightShortestPathFinder* _pathFinder;
|
||||
ThreadInfo& _myInfo;
|
||||
|
@ -154,23 +152,24 @@ class AttributeWeightShortestPathFinder : public ShortestPathFinder {
|
|||
private:
|
||||
std::thread _thread;
|
||||
};
|
||||
*/
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
/// @brief a Dijkstra searcher for the single-threaded search
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
class Searcher {
|
||||
AttributeWeightShortestPathFinder* _pathFinder;
|
||||
ThreadInfo& _myInfo;
|
||||
ThreadInfo& _peerInfo;
|
||||
arangodb::velocypack::Slice _start;
|
||||
ExpanderFunction _expander;
|
||||
std::string _id;
|
||||
|
||||
public:
|
||||
Searcher(AttributeWeightShortestPathFinder* pathFinder, ThreadInfo& myInfo,
|
||||
ThreadInfo& peerInfo, arangodb::velocypack::Slice const& start,
|
||||
ExpanderFunction expander, std::string const& id);
|
||||
ThreadInfo& peerInfo, arangodb::StringRef const& start,
|
||||
bool isBackward);
|
||||
|
||||
public:
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
/// @brief Do one step only.
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
bool oneStep();
|
||||
|
||||
private:
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
@ -183,20 +182,22 @@ class AttributeWeightShortestPathFinder : public ShortestPathFinder {
|
|||
/// @brief Lookup our current vertex in the data of our peer.
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
void lookupPeer(arangodb::velocypack::Slice& vertex, double weight);
|
||||
void lookupPeer(arangodb::StringRef& vertex, double weight);
|
||||
|
||||
public:
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
/// @brief Do one step only.
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
private:
|
||||
AttributeWeightShortestPathFinder* _pathFinder;
|
||||
ThreadInfo& _myInfo;
|
||||
ThreadInfo& _peerInfo;
|
||||
arangodb::StringRef _start;
|
||||
bool _isBackward;
|
||||
|
||||
bool oneStep();
|
||||
};
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
AttributeWeightShortestPathFinder(AttributeWeightShortestPathFinder const&) =
|
||||
delete;
|
||||
|
||||
AttributeWeightShortestPathFinder& operator=(
|
||||
AttributeWeightShortestPathFinder const&) = delete;
|
||||
AttributeWeightShortestPathFinder() = delete;
|
||||
|
@ -205,11 +206,9 @@ class AttributeWeightShortestPathFinder : public ShortestPathFinder {
|
|||
/// @brief create the PathFinder
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
AttributeWeightShortestPathFinder(ExpanderFunction&& forwardExpander,
|
||||
ExpanderFunction&& backwardExpander,
|
||||
bool bidirectional = true);
|
||||
AttributeWeightShortestPathFinder(ShortestPathOptions* options);
|
||||
|
||||
~AttributeWeightShortestPathFinder(){};
|
||||
~AttributeWeightShortestPathFinder();
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
/// @brief Find the shortest path between start and target.
|
||||
|
@ -225,6 +224,14 @@ class AttributeWeightShortestPathFinder : public ShortestPathFinder {
|
|||
arangodb::graph::ShortestPathResult& result,
|
||||
std::function<void()> const& callback) override;
|
||||
|
||||
void inserter(std::unordered_map<arangodb::StringRef, size_t>& candidates,
|
||||
std::vector<Step*>& result, arangodb::StringRef const& s,
|
||||
arangodb::StringRef const& t, double currentWeight,
|
||||
arangodb::StringRef edge);
|
||||
|
||||
void expandVertex(bool isBackward, arangodb::StringRef const& source,
|
||||
std::vector<Step*>& result);
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
/// @brief return the shortest path between the start and target vertex,
|
||||
/// multi-threaded version using SearcherTwoThreads.
|
||||
|
@ -234,9 +241,11 @@ class AttributeWeightShortestPathFinder : public ShortestPathFinder {
|
|||
// If this returns true there is a path, if this returns false there is no
|
||||
// path
|
||||
|
||||
/* Unused for now maybe reactived
|
||||
bool shortestPathTwoThreads(arangodb::velocypack::Slice& start,
|
||||
arangodb::velocypack::Slice& target,
|
||||
arangodb::graph::ShortestPathResult& result);
|
||||
*/
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
/// @brief lowest total weight for a complete path found
|
||||
|
@ -276,12 +285,18 @@ class AttributeWeightShortestPathFinder : public ShortestPathFinder {
|
|||
//////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
bool _intermediateSet;
|
||||
arangodb::velocypack::Slice _intermediate;
|
||||
arangodb::StringRef _intermediate;
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
/// @brief Reusable ManagedDocumentResult that temporarily takes
|
||||
/// responsibility for one document.
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
private:
|
||||
ExpanderFunction _forwardExpander;
|
||||
ExpanderFunction _backwardExpander;
|
||||
bool _bidirectional;
|
||||
|
||||
std::unique_ptr<ManagedDocumentResult> _mmdr;
|
||||
|
||||
ShortestPathOptions* _options;
|
||||
};
|
||||
|
||||
} // namespace graph
|
||||
|
|
|
@ -42,9 +42,8 @@ using namespace arangodb;
|
|||
using namespace arangodb::graph;
|
||||
|
||||
ConstantWeightShortestPathFinder::ConstantWeightShortestPathFinder(
|
||||
arangodb::aql::ShortestPathBlock* block)
|
||||
: _block(block),
|
||||
_options(block->_opts),
|
||||
ShortestPathOptions* options)
|
||||
: _options(options),
|
||||
_mmdr(new ManagedDocumentResult{}) {}
|
||||
|
||||
ConstantWeightShortestPathFinder::~ConstantWeightShortestPathFinder() {
|
||||
|
|
|
@ -58,7 +58,7 @@ class ConstantWeightShortestPathFinder : public ShortestPathFinder {
|
|||
};
|
||||
|
||||
public:
|
||||
ConstantWeightShortestPathFinder(arangodb::aql::ShortestPathBlock* block);
|
||||
ConstantWeightShortestPathFinder(ShortestPathOptions* options);
|
||||
|
||||
~ConstantWeightShortestPathFinder();
|
||||
|
||||
|
@ -81,9 +81,6 @@ class ConstantWeightShortestPathFinder : public ShortestPathFinder {
|
|||
std::unordered_map<arangodb::StringRef, PathSnippet*> _rightFound;
|
||||
std::deque<arangodb::StringRef> _rightClosure;
|
||||
|
||||
// TODO Remove Me!
|
||||
arangodb::aql::ShortestPathBlock* _block;
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
/// @brief The options to modify this shortest path computation
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
|
|
|
@ -107,6 +107,12 @@ void ShortestPathOptions::addReverseLookupInfo(
|
|||
attributeName, condition);
|
||||
}
|
||||
|
||||
double ShortestPathOptions::weightEdge(VPackSlice edge) {
|
||||
TRI_ASSERT(useWeight());
|
||||
return arangodb::basics::VelocyPackHelper::getNumericValue<double>(
|
||||
edge, weightAttribute.c_str(), defaultWeight);
|
||||
}
|
||||
|
||||
EdgeCursor* ShortestPathOptions::nextCursor(ManagedDocumentResult* mmdr,
|
||||
StringRef vid) {
|
||||
if (_isCoordinator) {
|
||||
|
|
|
@ -86,6 +86,9 @@ struct ShortestPathOptions : public BaseOptions {
|
|||
std::string const& attributeName,
|
||||
aql::AstNode* condition);
|
||||
|
||||
// Compute the weight of the given edge
|
||||
double weightEdge(arangodb::velocypack::Slice const);
|
||||
|
||||
EdgeCursor* nextCursor(ManagedDocumentResult*, StringRef vid);
|
||||
|
||||
EdgeCursor* nextReverseCursor(ManagedDocumentResult*, StringRef vid);
|
||||
|
|
|
@ -23,17 +23,21 @@
|
|||
|
||||
#include "Graph/ShortestPathResult.h"
|
||||
|
||||
#include "Aql/AqlValue.h"
|
||||
#include "Basics/StringRef.h"
|
||||
#include "Basics/VelocyPackHelper.h"
|
||||
#include "Transaction/Helpers.h"
|
||||
#include "Transaction/Methods.h"
|
||||
#include "VocBase/TraverserCache.h"
|
||||
|
||||
#include <velocypack/Builder.h>
|
||||
#include <velocypack/velocypack-aliases.h>
|
||||
|
||||
using namespace arangodb;
|
||||
using namespace arangodb::aql;
|
||||
using namespace arangodb::graph;
|
||||
using namespace arangodb::transaction;
|
||||
using namespace arangodb::traverser;
|
||||
|
||||
ShortestPathResult::ShortestPathResult() : _readDocuments(0) {}
|
||||
|
||||
|
@ -45,39 +49,16 @@ void ShortestPathResult::clear() {
|
|||
_edges.clear();
|
||||
}
|
||||
|
||||
void ShortestPathResult::edgeToVelocyPack(transaction::Methods*,
|
||||
ManagedDocumentResult* mmdr,
|
||||
size_t position,
|
||||
VPackBuilder& builder) {
|
||||
TRI_ASSERT(position < length());
|
||||
AqlValue ShortestPathResult::edgeToAqlValue(TraverserCache* cache, size_t position) const {
|
||||
if (position == 0) {
|
||||
builder.add(basics::VelocyPackHelper::NullValue());
|
||||
} else {
|
||||
TRI_ASSERT(position - 1 < _edges.size());
|
||||
// FIXME ADD CACHE!
|
||||
std::string tmp = _edges[position - 1].toString();
|
||||
builder.add(VPackValue(tmp));
|
||||
// First Edge is defined as NULL
|
||||
return AqlValue(arangodb::basics::VelocyPackHelper::NullValue());
|
||||
}
|
||||
TRI_ASSERT(position - 1 < _edges.size());
|
||||
return cache->fetchAqlResult(_edges[position - 1]);
|
||||
}
|
||||
|
||||
void ShortestPathResult::vertexToVelocyPack(transaction::Methods* trx,
|
||||
ManagedDocumentResult* mmdr,
|
||||
size_t position,
|
||||
VPackBuilder& builder) {
|
||||
TRI_ASSERT(position < length());
|
||||
StringRef v = _vertices[position];
|
||||
std::string collection = v.toString();
|
||||
size_t p = collection.find("/");
|
||||
TRI_ASSERT(p != std::string::npos);
|
||||
|
||||
transaction::BuilderLeaser searchBuilder(trx);
|
||||
searchBuilder->add(VPackValue(collection.substr(p + 1)));
|
||||
collection = collection.substr(0, p);
|
||||
|
||||
Result res = trx->documentFastPath(collection, mmdr, searchBuilder->slice(),
|
||||
builder, true);
|
||||
if (!res.ok()) {
|
||||
builder.clear(); // Just in case...
|
||||
builder.add(basics::VelocyPackHelper::NullValue());
|
||||
}
|
||||
AqlValue ShortestPathResult::vertexToAqlValue(TraverserCache* cache, size_t position) const {
|
||||
TRI_ASSERT(position < _vertices.size());
|
||||
return cache->fetchAqlResult(_vertices[position]);
|
||||
}
|
||||
|
|
|
@ -31,10 +31,18 @@ namespace arangodb {
|
|||
class ManagedDocumentResult;
|
||||
class StringRef;
|
||||
|
||||
namespace aql {
|
||||
class AqlValue;
|
||||
}
|
||||
|
||||
namespace transaction {
|
||||
class Methods;
|
||||
}
|
||||
|
||||
namespace traverser {
|
||||
class TraverserCache;
|
||||
}
|
||||
|
||||
namespace velocypack {
|
||||
class Builder;
|
||||
}
|
||||
|
@ -60,16 +68,18 @@ class ShortestPathResult {
|
|||
/// @brief Clears the path
|
||||
void clear();
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
/// @brief Builds only the last edge pointing to the vertex at position as
|
||||
/// VelocyPack
|
||||
|
||||
void edgeToVelocyPack(transaction::Methods*, ManagedDocumentResult*, size_t, arangodb::velocypack::Builder&);
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
/// @brief Builds only the vertex at position as VelocyPack
|
||||
/// AqlValue
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
void vertexToVelocyPack(transaction::Methods*, ManagedDocumentResult*, size_t, arangodb::velocypack::Builder&);
|
||||
aql::AqlValue edgeToAqlValue(traverser::TraverserCache* cache, size_t depth) const;
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
/// @brief Builds only the vertex at position as AqlValue
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
aql::AqlValue vertexToAqlValue(traverser::TraverserCache* cache, size_t depth) const;
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
/// @brief Gets the amount of read documents
|
||||
|
|
Loading…
Reference in New Issue