Add ExpandWeightedShortestPathCursor

Summary: Implement Dijkstras algorithm to find weighted shortest path.

Reviewers: teon.banek, florijan, buda

Reviewed By: teon.banek

Subscribers: pullbot, mtomic

Differential Revision: https://phabricator.memgraph.io/D1182
This commit is contained in:
Matija Santl 2018-02-08 11:45:30 +01:00
parent 4326847ab3
commit b1a8e48dc4
8 changed files with 845 additions and 64 deletions

View File

@ -470,10 +470,10 @@ To find only the shortest path, simply append `LIMIT 1` to the `RETURN` clause.
MATCH (a {id: 723})-[r:Type *bfs..10]-(b {id: 882}) RETURN * LIMIT 1
Breadth-fist expansion allows an arbitrary expression filter that determines
Breadth-first expansion allows an arbitrary expression filter that determines
if an expansion is allowed. Following is an example in which expansion is
allowed only over edges whose `x` property is greater then `12` and nodes `y`
whose property is lesser then `3`:
allowed only over edges whose `x` property is greater than `12` and nodes `y`
whose property is less than `3`:
MATCH (a {id: 723})-[*bfs..10 (e, n | e.x > 12 and n.y < 3)]-() RETURN *
@ -490,6 +490,40 @@ neighbourhood in breadth-first manner.
Currently, it isn't possible to get all shortest paths to a single node using
Memgraph's breadth-first expansion.
#### Weighted Shortest Path
Another standard use-case in a graph is searching for the weighted shortest
path between nodes. The openCypher standard does not define this feature, so
Memgraph provides a custom implementation, based on the edge expansion syntax.
Finding the weighted shortest path between nodes is done using the weighted
shortest path expansion:
MATCH (a {id: 723})-[le *wShortest 10 (e, n | e.weight) total_weight]-(b {id: 882}) RETURN *
The above query will find the shortest path of length up to 10 nodes between
nodes `a` and `b`.
Weighted Shortest Path expansion allows an arbitrary expression that determines
the weight for the current expansion. Total weight of a path is calculated as
the sum of all weights on the path between two nodes. Following is an example in
which the weight between nodes is defined as the product of edge weights
(instead of sum), assuming all weights are greater than '1':
MATCH (a {id: 723})-[le *wShortest 10 (e, n | log(e.weight)) total_weight]-(b {id: 882}) RETURN exp(total_weight)
Weighted Shortest Path expansions also allows an arbitrary expression filter
that determines if an expansion is allowed. Following is an example in which
expansion is allowed only over edges whose `x` property is greater than `12`
and nodes `y` whose property is less than `3`:
MATCH (a {id: 723})-[le *wShortest 10 (e, n | e.weight) total_weight (e, n | e.x > 12 and n.y < 3)]-(b {id: 882}) RETURN exp(total_weight)
Both weight and filter expression are defined as lambda functions over `e` and
`n`, which denote the edge and the node being expanded over in the weighted
shortest path search.
#### UNWIND
The `UNWIND` clause is used to unwind a list of values as individual rows.

View File

@ -2,6 +2,7 @@
#include <algorithm>
#include <limits>
#include <queue>
#include <string>
#include <type_traits>
#include <utility>
@ -322,10 +323,10 @@ std::unique_ptr<Cursor> ScanAllByLabelPropertyRange::MakeCursor(
context.symbol_table_, db, graph_view_);
auto convert = [&evaluator](const auto &bound)
-> std::experimental::optional<utils::Bound<PropertyValue>> {
if (!bound) return std::experimental::nullopt;
return std::experimental::make_optional(utils::Bound<PropertyValue>(
bound.value().value()->Accept(evaluator), bound.value().type()));
};
if (!bound) return std::experimental::nullopt;
return std::experimental::make_optional(utils::Bound<PropertyValue>(
bound.value().value()->Accept(evaluator), bound.value().type()));
};
return db.Vertices(label_, property_, convert(lower_bound()),
convert(upper_bound()), graph_view_ == GraphView::NEW);
};
@ -573,20 +574,23 @@ ExpandVariable::ExpandVariable(
const std::vector<storage::EdgeType> &edge_types, bool is_reverse,
Expression *lower_bound, Expression *upper_bound,
const std::shared_ptr<LogicalOperator> &input, Symbol input_symbol,
bool existing_node, Symbol inner_edge_symbol, Symbol inner_node_symbol,
Expression *filter, GraphView graph_view)
bool existing_node, Lambda filter_lambda,
std::experimental::optional<Lambda> weight_lambda,
std::experimental::optional<Symbol> total_weight, GraphView graph_view)
: ExpandCommon(node_symbol, edge_symbol, direction, edge_types, input,
input_symbol, existing_node, graph_view),
type_(type),
is_reverse_(is_reverse),
lower_bound_(lower_bound),
upper_bound_(upper_bound),
inner_edge_symbol_(inner_edge_symbol),
inner_node_symbol_(inner_node_symbol),
filter_(filter) {
filter_lambda_(filter_lambda),
weight_lambda_(weight_lambda),
total_weight_(total_weight) {
DCHECK(type_ == EdgeAtom::Type::DEPTH_FIRST ||
type_ == EdgeAtom::Type::BREADTH_FIRST)
<< "ExpandVariable can only be used with breadth or depth first type";
type_ == EdgeAtom::Type::BREADTH_FIRST ||
type_ == EdgeAtom::Type::WEIGHTED_SHORTEST_PATH)
<< "ExpandVariable can only be used with breadth first, depth first or "
"weighted shortest path type";
DCHECK(!(type_ == EdgeAtom::Type::BREADTH_FIRST && is_reverse))
<< "Breadth first expansion can't be reversed";
}
@ -855,9 +859,11 @@ class ExpandVariableCursor : public Cursor {
if (!self_.HandleExistingNode(current_vertex, frame)) continue;
// Skip expanding out of filtered expansion.
frame[self_.inner_edge_symbol_] = current_edge.first;
frame[self_.inner_node_symbol_] = current_vertex;
if (self_.filter_ && !EvaluateFilter(evaluator, self_.filter_)) continue;
frame[self_.filter_lambda_.inner_edge_symbol] = current_edge.first;
frame[self_.filter_lambda_.inner_node_symbol] = current_vertex;
if (self_.filter_lambda_.expression &&
!EvaluateFilter(evaluator, self_.filter_lambda_.expression))
continue;
// we are doing depth-first search, so place the current
// edge's expansions onto the stack, if we should continue to expand
@ -899,11 +905,11 @@ class ExpandBreadthFirstCursor : public query::plan::Cursor {
SwitchAccessor(edge, self_.graph_view_);
SwitchAccessor(vertex, self_.graph_view_);
frame[self_.inner_edge_symbol_] = edge;
frame[self_.inner_node_symbol_] = vertex;
frame[self_.filter_lambda_.inner_edge_symbol] = edge;
frame[self_.filter_lambda_.inner_node_symbol] = vertex;
if (self_.filter_) {
TypedValue result = self_.filter_->Accept(evaluator);
if (self_.filter_lambda_.expression) {
TypedValue result = self_.filter_lambda_.expression->Accept(evaluator);
switch (result.type()) {
case TypedValue::Type::Null:
return;
@ -1035,10 +1041,211 @@ class ExpandBreadthFirstCursor : public query::plan::Cursor {
std::deque<std::pair<EdgeAccessor, VertexAccessor>> to_visit_next_;
};
class ExpandWeightedShortestPathCursor : public query::plan::Cursor {
public:
ExpandWeightedShortestPathCursor(const ExpandVariable &self,
database::GraphDbAccessor &db)
: self_(self), db_(db), input_cursor_(self_.input_->MakeCursor(db)) {}
bool Pull(Frame &frame, Context &context) override {
ExpressionEvaluator evaluator(frame, context.parameters_,
context.symbol_table_, db_,
self_.graph_view_);
// For the given (vertex, edge, vertex) tuple checks if they satisfy the
// "where" condition. if so, places them in the priority queue.
auto expand_pair = [this, &evaluator, &frame](
VertexAccessor from, EdgeAccessor edge, VertexAccessor vertex) {
SwitchAccessor(edge, self_.graph_view_);
SwitchAccessor(vertex, self_.graph_view_);
if (self_.filter_lambda_.expression) {
frame[self_.filter_lambda_.inner_edge_symbol] = edge;
frame[self_.filter_lambda_.inner_node_symbol] = vertex;
if (!EvaluateFilter(evaluator, self_.filter_lambda_.expression)) return;
}
frame[self_.weight_lambda_->inner_edge_symbol] = edge;
frame[self_.weight_lambda_->inner_node_symbol] = vertex;
TypedValue typed_weight =
self_.weight_lambda_->expression->Accept(evaluator);
if (!typed_weight.IsNumeric()) {
throw QueryRuntimeException("Calculated weight must be numeric, got {}",
typed_weight.type());
}
if ((typed_weight < 0).Value<bool>()) {
throw QueryRuntimeException("Calculated weight can't be negative!");
}
auto total_weight = weights_[from] + typed_weight;
auto found_it = weights_.find(vertex);
if (found_it != weights_.end() &&
found_it->second.Value<double>() <= total_weight.Value<double>())
return;
pq_.push(std::make_pair(std::make_pair(vertex, edge),
total_weight.Value<double>()));
};
// Populates the priority queue structure with expansions
// from the given vertex. skips expansions that don't satisfy
// the "where" condition.
auto expand_from_vertex = [this, &expand_pair](VertexAccessor &vertex) {
if (self_.direction_ != EdgeAtom::Direction::IN) {
for (const EdgeAccessor &edge : vertex.out(&self_.edge_types_)) {
expand_pair(vertex, edge, edge.to());
}
}
if (self_.direction_ != EdgeAtom::Direction::OUT) {
for (const EdgeAccessor &edge : vertex.in(&self_.edge_types_)) {
expand_pair(vertex, edge, edge.from());
}
}
};
while (true) {
if (pq_.empty()) {
if (!input_cursor_->Pull(frame, context)) return false;
auto vertex_value = frame[self_.input_symbol_];
if (vertex_value.IsNull()) continue;
auto vertex = vertex_value.Value<VertexAccessor>();
if (self_.existing_node_) {
TypedValue &node = frame[self_.node_symbol_];
// Due to optional matching the existing node could be null.
// Skip expansion for such nodes.
if (node.IsNull()) continue;
}
SwitchAccessor(vertex, self_.graph_view_);
upper_bound_ =
self_.upper_bound_
? EvaluateInt(evaluator, self_.upper_bound_,
"Max depth in weighted shortest path expansion")
: std::numeric_limits<int>::max();
if (upper_bound_ < 1)
throw QueryRuntimeException(
"Max depth in weighted shortest path expansion must be greater "
"than zero");
// Clear existing data structures.
previous_.clear();
weights_.clear();
pq_.push(std::make_pair(
std::make_pair(vertex, std::experimental::nullopt), 0.0));
}
while (!pq_.empty()) {
auto current = pq_.top();
pq_.pop();
// Check if the edge has already been processed.
if (weights_.find(current.first.first) != weights_.end()) {
continue;
}
previous_.emplace(current.first.first, current.first.second);
weights_.emplace(current.first.first, current.second);
// Reconstruct the path.
auto last_vertex = current.first.first;
std::vector<TypedValue> edge_list{};
while (true) {
// Origin_vertex must be in previous.
const auto &previous_edge = previous_.find(last_vertex)->second;
if (!previous_edge) break;
last_vertex = previous_edge->from() == last_vertex
? previous_edge->to()
: previous_edge->from();
edge_list.push_back(previous_edge.value());
}
// Expand only if what we've just expanded is less then max depth.
if (static_cast<int>(edge_list.size()) < upper_bound_)
expand_from_vertex(current.first.first);
if (edge_list.empty()) continue;
// Place destination node on the frame, handle existence flag.
if (self_.existing_node_) {
TypedValue &node = frame[self_.node_symbol_];
if ((node != current.first.first).Value<bool>())
continue;
else
// Prevent expanding other paths, because we found the
// shortest to existing node.
ClearQueue();
} else {
frame[self_.node_symbol_] = current.first.first;
}
if (!self_.is_reverse_) {
// Place edges on the frame in the correct order.
std::reverse(edge_list.begin(), edge_list.end());
}
frame[self_.edge_symbol_] = std::move(edge_list);
frame[self_.total_weight_.value()] = current.second;
return true;
}
}
}
void Reset() override {
input_cursor_->Reset();
previous_.clear();
weights_.clear();
ClearQueue();
}
private:
const ExpandVariable &self_;
database::GraphDbAccessor &db_;
const std::unique_ptr<query::plan::Cursor> input_cursor_;
// Upper bound on the path length.
int upper_bound_{-1};
// Maps vertices to weights they got in expansion.
std::unordered_map<VertexAccessor, TypedValue> weights_;
// Maps vertices to edges used to reach them.
std::unordered_map<VertexAccessor, std::experimental::optional<EdgeAccessor>>
previous_;
// Priority queue comparator. Keep lowest weight on top of the queue.
class PriorityQueueComparator {
public:
bool operator()(
const std::pair<std::pair<VertexAccessor,
std::experimental::optional<EdgeAccessor>>,
double> &lhs,
const std::pair<std::pair<VertexAccessor,
std::experimental::optional<EdgeAccessor>>,
double> &rhs) {
return lhs.second > rhs.second;
}
};
std::priority_queue<
std::pair<
std::pair<VertexAccessor, std::experimental::optional<EdgeAccessor>>,
double>,
std::vector<std::pair<
std::pair<VertexAccessor, std::experimental::optional<EdgeAccessor>>,
double>>,
PriorityQueueComparator>
pq_;
void ClearQueue() {
while (!pq_.empty()) pq_.pop();
}
};
std::unique_ptr<Cursor> ExpandVariable::MakeCursor(
database::GraphDbAccessor &db) const {
if (type_ == EdgeAtom::Type::BREADTH_FIRST)
return std::make_unique<ExpandBreadthFirstCursor>(*this, db);
else if (type_ == EdgeAtom::Type::WEIGHTED_SHORTEST_PATH)
return std::make_unique<ExpandWeightedShortestPathCursor>(*this, db);
else
return std::make_unique<ExpandVariableCursor>(*this, db);
}
@ -1597,8 +1804,8 @@ bool ExpandUniquenessFilter<TAccessor>::ExpandUniquenessFilterCursor::Pull(
for (const auto &previous_symbol : self_.previous_symbols_) {
TypedValue &previous_value = frame[previous_symbol];
// This shouldn't raise a TypedValueException, because the planner
// makes sure these are all of the expected type. In case they are not, an
// error should be raised long before this code is executed.
// makes sure these are all of the expected type. In case they are not,
// an error should be raised long before this code is executed.
if (ContainsSame<TAccessor>(previous_value, expand_value)) return false;
}
return true;
@ -2626,8 +2833,8 @@ bool PullRemote::PullRemoteCursor::Pull(Frame &frame, Context &context) {
if (!have_remote_results) {
// If we didn't find any remote results and there aren't any remote
// pulls, we've exhausted all remote results. Make sure we signal that to
// workers and exit the loop.
// pulls, we've exhausted all remote results. Make sure we signal that
// to workers and exit the loop.
if (remote_pulls_.empty()) {
EndRemotePull();
break;
@ -2659,8 +2866,8 @@ bool PullRemote::PullRemoteCursor::Pull(Frame &frame, Context &context) {
remote_results_[pull_from_worker_id].resize(
remote_results_[pull_from_worker_id].size() - 1);
// Remove the worker if we exhausted all locally stored results and there are
// no more pending remote pulls for that worker.
// Remove the worker if we exhausted all locally stored results and there
// are no more pending remote pulls for that worker.
if (remote_results_[pull_from_worker_id].empty() &&
remote_pulls_.find(pull_from_worker_id) == remote_pulls_.end()) {
worker_ids_.erase(worker_ids_.begin() + last_pulled_worker_id_index_);

View File

@ -807,8 +807,33 @@ class ExpandVariable : public LogicalOperator, public ExpandCommon {
// that should be inaccessible (private class function won't compile)
friend class ExpandVariableCursor;
friend class ExpandBreadthFirstCursor;
friend class ExpandWeightedShortestPathCursor;
public:
struct Lambda {
// Symbols for a single node and edge that are currently getting expanded.
Symbol inner_edge_symbol;
Symbol inner_node_symbol;
// Expression used in lambda during expansion.
Expression *expression;
BOOST_SERIALIZATION_SPLIT_MEMBER();
template <class TArchive>
void save(TArchive &ar, const unsigned int) const {
ar &inner_edge_symbol;
ar &inner_node_symbol;
SavePointer(ar, expression);
}
template <class TArchive>
void load(TArchive &ar, const unsigned int) {
ar &inner_edge_symbol;
ar &inner_node_symbol;
LoadPointer(ar, expression);
}
};
/**
* Creates a variable-length expansion. Most params are forwarded
* to the @c ExpandCommon constructor, and are documented there.
@ -838,9 +863,9 @@ class ExpandVariable : public LogicalOperator, public ExpandCommon {
bool is_reverse, Expression *lower_bound,
Expression *upper_bound,
const std::shared_ptr<LogicalOperator> &input,
Symbol input_symbol, bool existing_node,
Symbol inner_edge_symbol, Symbol inner_node_symbol,
Expression *filter = nullptr,
Symbol input_symbol, bool existing_node, Lambda filter_lambda,
std::experimental::optional<Lambda> weight_lambda,
std::experimental::optional<Symbol> total_weight,
GraphView graph_view = GraphView::AS_IS);
bool Accept(HierarchicalLogicalOperatorVisitor &visitor) override;
@ -858,12 +883,10 @@ class ExpandVariable : public LogicalOperator, public ExpandCommon {
// both are optional, defaults are (1, inf)
Expression *lower_bound_;
Expression *upper_bound_;
// symbols for a single node and edge that are currently getting expanded
Symbol inner_edge_symbol_;
Symbol inner_node_symbol_;
// a filtering expression for skipping expansions during expansion
// can refer to inner node and edges
Expression *filter_;
Lambda filter_lambda_;
std::experimental::optional<Lambda> weight_lambda_;
std::experimental::optional<Symbol> total_weight_;
ExpandVariable() {}
@ -879,9 +902,9 @@ class ExpandVariable : public LogicalOperator, public ExpandCommon {
ar &is_reverse_;
SavePointer(ar, lower_bound_);
SavePointer(ar, upper_bound_);
ar &inner_edge_symbol_;
ar &inner_node_symbol_;
SavePointer(ar, filter_);
ar &filter_lambda_;
ar &weight_lambda_;
ar &total_weight_;
}
template <class TArchive>
@ -892,9 +915,9 @@ class ExpandVariable : public LogicalOperator, public ExpandCommon {
ar &is_reverse_;
LoadPointer(ar, lower_bound_);
LoadPointer(ar, upper_bound_);
ar &inner_edge_symbol_;
ar &inner_node_symbol_;
LoadPointer(ar, filter_);
ar &filter_lambda_;
ar &weight_lambda_;
ar &total_weight_;
}
};

View File

@ -352,24 +352,37 @@ class RuleBasedPlanner {
DCHECK(!utils::Contains(bound_symbols, edge_symbol))
<< "Existing edges are not supported";
if (edge->IsVariable()) {
Symbol inner_edge_symbol =
std::experimental::optional<ExpandVariable::Lambda> weight_lambda;
std::experimental::optional<Symbol> total_weight;
if (edge->type_ == EdgeAtom::Type::WEIGHTED_SHORTEST_PATH) {
weight_lambda.emplace(ExpandVariable::Lambda{
symbol_table.at(*edge->weight_lambda_.inner_edge),
symbol_table.at(*edge->weight_lambda_.inner_node),
edge->weight_lambda_.expression});
total_weight.emplace(symbol_table.at(*edge->total_weight_));
}
ExpandVariable::Lambda filter_lambda;
filter_lambda.inner_edge_symbol =
symbol_table.at(*edge->filter_lambda_.inner_edge);
Symbol inner_node_symbol =
filter_lambda.inner_node_symbol =
symbol_table.at(*edge->filter_lambda_.inner_node);
{
// Bind the inner edge and node symbols so they're available for
// inline filtering in ExpandVariable.
bool inner_edge_bound =
bound_symbols.insert(inner_edge_symbol).second;
bound_symbols.insert(filter_lambda.inner_edge_symbol).second;
bool inner_node_bound =
bound_symbols.insert(inner_node_symbol).second;
bound_symbols.insert(filter_lambda.inner_node_symbol).second;
DCHECK(inner_edge_bound && inner_node_bound)
<< "An inner edge and node can't be bound from before";
}
// Join regular filters with lambda filter expression, so that they
// are done inline together. Semantic analysis should guarantee that
// lambda filtering uses bound symbols.
auto *filter_expr = impl::BoolJoin<AndOperator>(
filter_lambda.expression = impl::BoolJoin<AndOperator>(
storage, impl::ExtractFilters(bound_symbols, filters, storage),
edge->filter_lambda_.expression);
// At this point it's possible we have leftover filters for inline
@ -378,22 +391,24 @@ class RuleBasedPlanner {
// will ever bind them again.
filters.erase(
std::remove_if(filters.begin(), filters.end(),
[ e = inner_edge_symbol,
n = inner_node_symbol ](FilterInfo & fi) {
[
e = filter_lambda.inner_edge_symbol,
n = filter_lambda.inner_node_symbol
](FilterInfo & fi) {
return utils::Contains(fi.used_symbols, e) ||
utils::Contains(fi.used_symbols, n);
}),
filters.end());
// Unbind the temporarily bound inner symbols for filtering.
bound_symbols.erase(inner_edge_symbol);
bound_symbols.erase(inner_node_symbol);
bound_symbols.erase(filter_lambda.inner_edge_symbol);
bound_symbols.erase(filter_lambda.inner_node_symbol);
// TODO: Pass weight lambda.
last_op = std::make_unique<ExpandVariable>(
node_symbol, edge_symbol, edge->type_, expansion.direction,
edge->edge_types_, expansion.is_flipped, edge->lower_bound_,
edge->upper_bound_, std::move(last_op), node1_symbol,
existing_node, inner_edge_symbol, inner_node_symbol, filter_expr,
existing_node, filter_lambda, weight_lambda, total_weight,
match_context.graph_view);
} else {
if (!existing_node) {

View File

@ -0,0 +1,119 @@
Feature: Weighted Shortest Path
Scenario: Test match wShortest upper bound
Given an empty graph
And having executed:
"""
CREATE (n {a:'0'})-[:r {w: 1}]->({a:'1'})-[:r {w: 1}]->({a:'2'}), (n)-[:r {w: 1}]->({a:'3'})
"""
When executing query:
"""
MATCH (n {a:'0'})-[le *wShortest 1 (e, n | e.w ) w]->(m) RETURN m.a
"""
Then the result should be:
| m.a |
| '1' |
| '3' |
Scenario: Test match wShortest filtered
Given an empty graph
And having executed:
"""
CREATE (n {a:'0'})-[:r {w: 1}]->({a:'1'})-[:r {w: 1}]->({a:'2'}), (n)-[:r {w: 1}]->({a:'3'})
"""
When executing query:
"""
MATCH (n {a:'0'})-[le *wShortest 1 (e, n | e.w ) w (e, n | n.a = '3')]->(m) RETURN m.a
"""
Then the result should be:
| m.a |
| '3' |
Scenario: Test match wShortest resulting edge list
Given an empty graph
And having executed:
"""
CREATE (n {a:'0'})-[:r {w: 1}]->({a:'1'})-[:r {w: 2}]->({a:'2'}), (n)-[:r {w: 4}]->({a:'3'})
"""
When executing query:
"""
MATCH (n {a:'0'})-[le *wShortest 10 (e, n | e.w ) w]->(m) RETURN m.a, size(le) as s, w
"""
Then the result should be:
| m.a | s | w |
| '1' | 1 | 1.0 |
| '2' | 2 | 3.0 |
| '3' | 1 | 4.0 |
Scenario: Test match wShortest single edge type filtered
Given an empty graph
And having executed:
"""
CREATE (n {a:'0'})-[:r0 {w: 1}]->({a:'1'})-[:r {w: 2}]->({a:'2'}), (n)-[:r {w: 3}]->({a:'4'})
"""
When executing query:
"""
MATCH ()-[le:r0 *wShortest 10 (e, n | e.w) w]->(m)
RETURN size(le) AS s, m.a
"""
Then the result should be:
| s | m.a |
| 1 | '1' |
Scenario: Test match wShortest multiple edge types filtered
Given an empty graph
And having executed:
"""
CREATE (n {a:'0'})-[:r0 {w: 1}]->({a:'1'})-[:r1 {w: 2}]->({a:'2'}), (n)-[:r {w: 3}]->({a:'4'})
"""
When executing query:
"""
MATCH ()-[le :r0|:r1 *wShortest 10 (e, n | e.w) w]->(m) WHERE size(le) > 1
RETURN size(le) AS s, (le[0]).w AS r0, (le[1]).w AS r1
"""
Then the result should be:
| s | r0 | r1 |
| 2 | 1 | 2 |
Scenario: Test match wShortest property filters
Given an empty graph
And having executed:
"""
CREATE (n {a:'0'})-[:r {w: 1}]->({a:'1'})-[:r {w: 2}]->({a:'2'}), (n)-[:r {w: 3}]->({a:'4'})
"""
When executing query:
"""
MATCH ()-[le *wShortest 10 {w:1} (e, n | e.w ) total_weight]->(m)
RETURN size(le) AS s, (le[0]).w AS r0
"""
Then the result should be:
| s | r0 |
| 1 | 1 |
Scenario: Test match wShortest weight not a number
Given an empty graph
And having executed:
"""
CREATE (n {a:'0'})-[:r {w: 'not a number'}]->({a:'1'})-[:r {w: 2}]->({a:'2'}), (n)-[:r {w: 3}]->({a:'4'})
"""
When executing query:
"""
MATCH ()-[le *wShortest 10 (e, n | e.w ) total_weight]->(m)
RETURN le, total_weight
"""
Then an error should be raised
Scenario: Test match wShortest negative weight
Given an empty graph
And having executed:
"""
CREATE (n {a:'0'})-[:r {w: -1}]->({a:'1'})-[:r {w: 2}]->({a:'2'}), (n)-[:r {w: 3}]->({a:'4'})
"""
When executing query:
"""
MATCH ()-[le *wShortest 10 (e, n | e.w ) total_weight]->(m)
RETURN le, total_weight
"""
Then an error should be raised

View File

@ -250,3 +250,51 @@ TEST_F(InterpreterTest, CreateIndexInMulticommandTransaction) {
interpreter_("CREATE INDEX ON :X(y)", dba, {}, true).PullAll(stream),
query::IndexInMulticommandTxException);
}
// Test shortest path end to end.
TEST_F(InterpreterTest, ShortestPath) {
{
ResultStreamFaker stream;
database::GraphDbAccessor dba(db_);
interpreter_(
"CREATE (n:A {x: 1}), (m:B {x: 2}), (l:C {x: 1}), (n)-[:r1 {w: 1 "
"}]->(m)-[:r2 {w: 2}]->(l), (n)-[:r3 {w: 4}]->(l)",
dba, {}, true)
.PullAll(stream);
dba.Commit();
}
ResultStreamFaker stream;
database::GraphDbAccessor dba(db_);
interpreter_("MATCH (n)-[e *wshortest 5 (e, n | e.w) ]->(m) return e", dba,
{}, false)
.PullAll(stream);
ASSERT_EQ(stream.GetHeader().size(), 1U);
EXPECT_EQ(stream.GetHeader()[0], "e");
ASSERT_EQ(stream.GetResults().size(), 3U);
std::vector<std::vector<std::string>> expected_results{
{"r1"}, {"r2"}, {"r1", "r2"}};
for (const auto &result : stream.GetResults()) {
const auto &edges =
query::test_common::ToList<EdgeAccessor>(result[0].ValueList());
std::vector<std::string> datum;
for (const auto &edge : edges) {
datum.push_back(dba.EdgeTypeName(edge.EdgeType()));
}
bool any_match = false;
for (const auto &expected : expected_results) {
if (expected == datum) {
any_match = true;
break;
}
}
EXPECT_TRUE(any_match);
}
}

View File

@ -168,11 +168,12 @@ TEST_F(QueryCostEstimator, Expand) {
}
TEST_F(QueryCostEstimator, ExpandVariable) {
MakeOp<ExpandVariable>(NextSymbol(), NextSymbol(),
EdgeAtom::Type::DEPTH_FIRST, EdgeAtom::Direction::IN,
std::vector<storage::EdgeType>{}, false, nullptr,
nullptr, last_op_, NextSymbol(), false, NextSymbol(),
NextSymbol(), nullptr);
MakeOp<ExpandVariable>(
NextSymbol(), NextSymbol(), EdgeAtom::Type::DEPTH_FIRST,
EdgeAtom::Direction::IN, std::vector<storage::EdgeType>{}, false, nullptr,
nullptr, last_op_, NextSymbol(), false,
ExpandVariable::Lambda{NextSymbol(), NextSymbol(), nullptr},
std::experimental::nullopt, std::experimental::nullopt);
EXPECT_COST(CardParam::kExpandVariable * CostParam::kExpandVariable);
}

View File

@ -429,11 +429,15 @@ class QueryPlanExpandVariable : public testing::Test {
auto convert = [this](std::experimental::optional<size_t> bound) {
return bound ? LITERAL(static_cast<int64_t>(bound.value())) : nullptr;
};
return std::make_shared<ExpandVariable>(
n_to_sym, edge_sym, EdgeAtom::Type::DEPTH_FIRST, direction,
edge_types, is_reverse, convert(lower), convert(upper), filter_op,
n_from.sym_, false, symbol_table.CreateSymbol("inner_edge", false),
symbol_table.CreateSymbol("inner_node", false), nullptr, graph_view);
n_from.sym_, false,
ExpandVariable::Lambda{symbol_table.CreateSymbol("inner_edge", false),
symbol_table.CreateSymbol("inner_node", false),
nullptr},
std::experimental::nullopt, std::experimental::nullopt, graph_view);
} else
return std::make_shared<Expand>(n_to_sym, edge_sym, direction, edge_types,
filter_op, n_from.sym_, false,
@ -763,11 +767,14 @@ class QueryPlanExpandBreadthFirst : public testing::Test {
? existing_node_input->sym_
: symbol_table.CreateSymbol("node", true);
auto edge_list_sym = symbol_table.CreateSymbol("edgelist_", true);
last_op = std::make_shared<ExpandVariable>(
node_sym, edge_list_sym, EdgeAtom::Type::BREADTH_FIRST, direction,
std::vector<storage::EdgeType>{}, false, nullptr, LITERAL(max_depth),
last_op, n.sym_, existing_node_input != nullptr, inner_edge, inner_node,
where, graph_view);
auto filter_lambda =
last_op = std::make_shared<ExpandVariable>(
node_sym, edge_list_sym, EdgeAtom::Type::BREADTH_FIRST, direction,
std::vector<storage::EdgeType>{}, false, nullptr,
LITERAL(max_depth), last_op, n.sym_, existing_node_input != nullptr,
ExpandVariable::Lambda{inner_edge, inner_node, where},
std::experimental::nullopt, std::experimental::nullopt, graph_view);
Frame frame(symbol_table.max_position());
auto cursor = last_op->MakeCursor(dba);
@ -913,6 +920,333 @@ TEST_F(QueryPlanExpandBreadthFirst, ExistingNode) {
}
}
/** A test fixture for weighted shortest path expansion */
class QueryPlanExpandWeightedShortestPath : public testing::Test {
public:
struct ResultType {
std::vector<EdgeAccessor> path;
VertexAccessor vertex;
double total_weight;
};
protected:
// style-guide non-conformant name due to PROPERTY_PAIR and PROPERTY_LOOKUP
// macro requirements
database::SingleNode db;
database::GraphDbAccessor dba{db};
std::pair<std::string, storage::Property> prop = PROPERTY_PAIR("property");
storage::EdgeType edge_type = dba.EdgeType("edge_type");
// make 5 vertices because we'll need to compare against them exactly
// v[0] has `prop` with the value 0
std::vector<VertexAccessor> v;
// make some edges too, in a map (from, to) vertex indices
std::unordered_map<std::pair<int, int>, EdgeAccessor> e;
AstTreeStorage storage;
SymbolTable symbol_table;
// inner edge and vertex symbols
Symbol filter_edge = symbol_table.CreateSymbol("f_edge", true);
Symbol filter_node = symbol_table.CreateSymbol("f_node", true);
Symbol weight_edge = symbol_table.CreateSymbol("w_edge", true);
Symbol weight_node = symbol_table.CreateSymbol("w_node", true);
Symbol total_weight = symbol_table.CreateSymbol("total_weight", true);
void SetUp() {
for (int i = 0; i < 5; i++) {
v.push_back(dba.InsertVertex());
v.back().PropsSet(prop.second, i);
}
auto add_edge = [&](int from, int to, double weight) {
EdgeAccessor edge = dba.InsertEdge(v[from], v[to], edge_type);
edge.PropsSet(prop.second, weight);
e.emplace(std::make_pair(from, to), edge);
};
add_edge(0, 1, 5);
add_edge(1, 4, 5);
add_edge(0, 2, 3);
add_edge(2, 3, 3);
add_edge(3, 4, 3);
add_edge(4, 0, 12);
dba.AdvanceCommand();
for (auto &vertex : v) vertex.Reconstruct();
for (auto &edge : e) edge.second.Reconstruct();
}
// defines and performs a breadth-first expansion with the given params
// returns a vector of pairs. each pair is (vector-of-edges, vertex)
auto ExpandWShortest(EdgeAtom::Direction direction, int max_depth,
Expression *where,
GraphView graph_view = GraphView::AS_IS,
std::experimental::optional<int> node_id = 0,
ScanAllTuple *existing_node_input = nullptr) {
// scan the nodes optionally filtering on property value
auto n =
MakeScanAll(storage, symbol_table, "n",
existing_node_input ? existing_node_input->op_ : nullptr);
auto last_op = n.op_;
if (node_id) {
last_op = std::make_shared<Filter>(
last_op,
EQ(PROPERTY_LOOKUP(n.node_->identifier_, prop), LITERAL(*node_id)));
}
auto ident_e = IDENT("e");
symbol_table[*ident_e] = weight_edge;
// expand wshortest
auto node_sym = existing_node_input
? existing_node_input->sym_
: symbol_table.CreateSymbol("node", true);
auto edge_list_sym = symbol_table.CreateSymbol("edgelist_", true);
auto filter_lambda = last_op = std::make_shared<ExpandVariable>(
node_sym, edge_list_sym, EdgeAtom::Type::WEIGHTED_SHORTEST_PATH,
direction, std::vector<storage::EdgeType>{}, false, nullptr,
LITERAL(max_depth), last_op, n.sym_, existing_node_input != nullptr,
ExpandVariable::Lambda{filter_edge, filter_node, where},
ExpandVariable::Lambda{weight_edge, weight_node,
PROPERTY_LOOKUP(ident_e, prop)},
total_weight, graph_view);
Frame frame(symbol_table.max_position());
auto cursor = last_op->MakeCursor(dba);
std::vector<ResultType> results;
Context context(dba);
context.symbol_table_ = symbol_table;
while (cursor->Pull(frame, context)) {
results.push_back(ResultType{std::vector<EdgeAccessor>(),
frame[node_sym].Value<VertexAccessor>(),
frame[total_weight].Value<double>()});
for (const TypedValue &edge : frame[edge_list_sym].ValueList())
results.back().path.emplace_back(edge.Value<EdgeAccessor>());
}
return results;
}
template <typename TAccessor>
auto GetProp(const TAccessor &accessor) {
return accessor.PropsAt(prop.second).template Value<int64_t>();
}
template <typename TAccessor>
auto GetDoubleProp(const TAccessor &accessor) {
return accessor.PropsAt(prop.second).template Value<double>();
}
Expression *PropNe(Symbol symbol, int value) {
auto ident = IDENT("inner_element");
symbol_table[*ident] = symbol;
return NEQ(PROPERTY_LOOKUP(ident, prop), LITERAL(value));
}
};
// Testing weighted shortest path on this graph:
//
// 5 5
// /-->--[1]-->--\
// / \
// / 12 \ 2
// [0]--------<--------[4]------->-------[5]
// \ / (only for GraphState test)
// \ /
// \->[2]->-[3]->/
// 3 3 3
TEST_F(QueryPlanExpandWeightedShortestPath, Basic) {
auto results =
ExpandWShortest(EdgeAtom::Direction::BOTH, 1000, LITERAL(true));
ASSERT_EQ(results.size(), 4);
// check end nodes
EXPECT_EQ(GetProp(results[0].vertex), 2);
EXPECT_EQ(GetProp(results[1].vertex), 1);
EXPECT_EQ(GetProp(results[2].vertex), 3);
EXPECT_EQ(GetProp(results[3].vertex), 4);
// check paths and total weights
EXPECT_EQ(results[0].path.size(), 1);
EXPECT_EQ(GetDoubleProp(results[0].path[0]), 3);
EXPECT_EQ(results[0].total_weight, 3);
EXPECT_EQ(results[1].path.size(), 1);
EXPECT_EQ(GetDoubleProp(results[1].path[0]), 5);
EXPECT_EQ(results[1].total_weight, 5);
EXPECT_EQ(results[2].path.size(), 2);
EXPECT_EQ(GetDoubleProp(results[2].path[0]), 3);
EXPECT_EQ(GetDoubleProp(results[2].path[1]), 3);
EXPECT_EQ(results[2].total_weight, 6);
EXPECT_EQ(results[3].path.size(), 3);
EXPECT_EQ(GetDoubleProp(results[3].path[0]), 3);
EXPECT_EQ(GetDoubleProp(results[3].path[1]), 3);
EXPECT_EQ(GetDoubleProp(results[3].path[2]), 3);
EXPECT_EQ(results[3].total_weight, 9);
}
TEST_F(QueryPlanExpandWeightedShortestPath, EdgeDirection) {
{
auto results =
ExpandWShortest(EdgeAtom::Direction::OUT, 1000, LITERAL(true));
ASSERT_EQ(results.size(), 4);
EXPECT_EQ(GetProp(results[0].vertex), 2);
EXPECT_EQ(results[0].total_weight, 3);
EXPECT_EQ(GetProp(results[1].vertex), 1);
EXPECT_EQ(results[1].total_weight, 5);
EXPECT_EQ(GetProp(results[2].vertex), 3);
EXPECT_EQ(results[2].total_weight, 6);
EXPECT_EQ(GetProp(results[3].vertex), 4);
EXPECT_EQ(results[3].total_weight, 9);
}
{
auto results =
ExpandWShortest(EdgeAtom::Direction::IN, 1000, LITERAL(true));
ASSERT_EQ(results.size(), 4);
EXPECT_EQ(GetProp(results[0].vertex), 4);
EXPECT_EQ(results[0].total_weight, 12);
EXPECT_EQ(GetProp(results[1].vertex), 3);
EXPECT_EQ(results[1].total_weight, 15);
EXPECT_EQ(GetProp(results[2].vertex), 1);
EXPECT_EQ(results[2].total_weight, 17);
EXPECT_EQ(GetProp(results[3].vertex), 2);
EXPECT_EQ(results[3].total_weight, 18);
}
}
TEST_F(QueryPlanExpandWeightedShortestPath, Where) {
{
auto results = ExpandWShortest(EdgeAtom::Direction::BOTH, 1000,
PropNe(filter_node, 2));
ASSERT_EQ(results.size(), 3);
EXPECT_EQ(GetProp(results[0].vertex), 1);
EXPECT_EQ(results[0].total_weight, 5);
EXPECT_EQ(GetProp(results[1].vertex), 4);
EXPECT_EQ(results[1].total_weight, 10);
EXPECT_EQ(GetProp(results[2].vertex), 3);
EXPECT_EQ(results[2].total_weight, 13);
}
{
auto results = ExpandWShortest(EdgeAtom::Direction::BOTH, 1000,
PropNe(filter_node, 1));
ASSERT_EQ(results.size(), 3);
EXPECT_EQ(GetProp(results[0].vertex), 2);
EXPECT_EQ(results[0].total_weight, 3);
EXPECT_EQ(GetProp(results[1].vertex), 3);
EXPECT_EQ(results[1].total_weight, 6);
EXPECT_EQ(GetProp(results[2].vertex), 4);
EXPECT_EQ(results[2].total_weight, 9);
}
}
TEST_F(QueryPlanExpandWeightedShortestPath, GraphState) {
auto ExpandSize = [this](GraphView graph_view) {
return ExpandWShortest(EdgeAtom::Direction::BOTH, 1000, LITERAL(true),
graph_view)
.size();
};
EXPECT_EQ(ExpandSize(GraphView::OLD), 4);
EXPECT_EQ(ExpandSize(GraphView::NEW), 4);
auto new_vertex = dba.InsertVertex();
new_vertex.PropsSet(prop.second, 5);
auto edge = dba.InsertEdge(v[4], new_vertex, edge_type);
edge.PropsSet(prop.second, 2);
EXPECT_EQ(CountIterable(dba.Vertices(false)), 5);
EXPECT_EQ(CountIterable(dba.Vertices(true)), 6);
EXPECT_EQ(ExpandSize(GraphView::OLD), 4);
EXPECT_EQ(ExpandSize(GraphView::NEW), 5);
dba.AdvanceCommand();
EXPECT_EQ(ExpandSize(GraphView::OLD), 5);
EXPECT_EQ(ExpandSize(GraphView::NEW), 5);
}
TEST_F(QueryPlanExpandWeightedShortestPath, ExistingNode) {
auto ExpandPreceeding = [this](
std::experimental::optional<int> preceeding_node_id) {
// scan the nodes optionally filtering on property value
auto n0 = MakeScanAll(storage, symbol_table, "n0");
if (preceeding_node_id) {
auto filter = std::make_shared<Filter>(
n0.op_, EQ(PROPERTY_LOOKUP(n0.node_->identifier_, prop),
LITERAL(*preceeding_node_id)));
// inject the filter op into the ScanAllTuple. that way the filter op
// can be passed into the ExpandWShortest function without too much
// refactor
n0.op_ = filter;
}
return ExpandWShortest(EdgeAtom::Direction::OUT, 1000, LITERAL(true),
GraphView::AS_IS, std::experimental::nullopt, &n0);
};
EXPECT_EQ(ExpandPreceeding(std::experimental::nullopt).size(), 20);
{
auto results = ExpandPreceeding(3);
ASSERT_EQ(results.size(), 4);
for (int i = 0; i < 4; i++) EXPECT_EQ(GetProp(results[i].vertex), 3);
}
}
TEST_F(QueryPlanExpandWeightedShortestPath, UpperBound) {
{
auto results = ExpandWShortest(EdgeAtom::Direction::BOTH, 2, LITERAL(true));
ASSERT_EQ(results.size(), 4);
EXPECT_EQ(GetProp(results[0].vertex), 2);
EXPECT_EQ(results[0].total_weight, 3);
EXPECT_EQ(GetProp(results[1].vertex), 1);
EXPECT_EQ(results[1].total_weight, 5);
EXPECT_EQ(GetProp(results[2].vertex), 3);
EXPECT_EQ(results[2].total_weight, 6);
EXPECT_EQ(GetProp(results[3].vertex), 4);
EXPECT_EQ(results[3].total_weight, 10);
}
{
auto results = ExpandWShortest(EdgeAtom::Direction::BOTH, 1, LITERAL(true));
ASSERT_EQ(results.size(), 3);
EXPECT_EQ(GetProp(results[0].vertex), 2);
EXPECT_EQ(results[0].total_weight, 3);
EXPECT_EQ(GetProp(results[1].vertex), 1);
EXPECT_EQ(results[1].total_weight, 5);
EXPECT_EQ(GetProp(results[2].vertex), 4);
EXPECT_EQ(results[2].total_weight, 12);
}
}
TEST_F(QueryPlanExpandWeightedShortestPath, Exceptions) {
{
auto new_vertex = dba.InsertVertex();
new_vertex.PropsSet(prop.second, 5);
auto edge = dba.InsertEdge(v[4], new_vertex, edge_type);
edge.PropsSet(prop.second, "not a number");
EXPECT_THROW(ExpandWShortest(EdgeAtom::Direction::BOTH, 1000, LITERAL(true),
GraphView::NEW),
QueryRuntimeException);
}
{
auto new_vertex = dba.InsertVertex();
new_vertex.PropsSet(prop.second, 5);
auto edge = dba.InsertEdge(v[4], new_vertex, edge_type);
edge.PropsSet(prop.second, -10); // negative weight
EXPECT_THROW(ExpandWShortest(EdgeAtom::Direction::BOTH, 1000, LITERAL(true),
GraphView::NEW),
QueryRuntimeException);
}
{
// negative upper bound
EXPECT_THROW(ExpandWShortest(EdgeAtom::Direction::BOTH, -1, LITERAL(true),
GraphView::NEW),
QueryRuntimeException);
}
}
TEST(QueryPlan, ExpandOptional) {
database::SingleNode db;
database::GraphDbAccessor dba(db);