Exploring the European road network

Exploring the European road network

This article is a part of a series intended to show users how to use Memgraph on real-world data and, by doing so, retrieve some interesting and useful information.

Introduction

This particular article outlines how to use some of Memgraph's built-in graph algorithms. More specifically, the article shows how to use breadth-first search graph traversal algorithm, and Dijkstra's algorithm for finding weighted shortest paths between nodes in the graph.

Data model

One of the most common applications of graph traversal algorithms is driving route computation, so we will use European road network graph as an example. The graph consists of 999 major European cities from 39 countries in total. Each city is connected to the country it belongs to via an edge of type :In_. There are edges of type :Road connecting cities less than 500 kilometers apart. Distance between cities is specified in the length property of the edge.

Road network

Exploring the dataset

You have two options for exploring this dataset. If you just want to take a look at the dataset and try out a few queries, open Memgraph Playground (opens in a new tab) and continue with the tutorial there. Note that you will not be able to execute write operations.

On the other hand, if you would like to add changes to the dataset, download the Memgraph Platform (opens in a new tab). Once you have it up and running, open Memgraph Lab web application within the browser on localhost:3000 (opens in a new tab) and navigate to Datasets in the sidebar. From there, choose the dataset Europe road network and continue with the tutorial.

Example queries

1. Let's list all of the countries in our road network.

MATCH (c:Country)
RETURN c.name
ORDER BY c.name;

2. Which Croatian cities are in our road network?

MATCH (c:City)-[:In_]->(:Country {name: "Croatia"})
RETURN c.name
ORDER BY c.name;

3. Which cities in our road network are less than 200 km away from Zagreb?

MATCH (:City {name: "Zagreb"})-[r:Road]->(c:City)
WHERE r.length < 200
RETURN c.name
ORDER BY c.name;

Now let's try some queries using Memgraph's graph traversal capabilities.

4. Say you want to drive from Zagreb to Paris. You might wonder, what is the least number of cities you have to visit if you don't want to drive more than 500 kilometers between stops. Since the edges in our road network don't connect cities that are more than 500 km apart, this is a great use case for the breadth-first search (BFS) algorithm.

MATCH p = (:City {name: "Zagreb"})
          -[:Road * bfs]->
          (:City {name: "Paris"})
RETURN nodes(p);

5. What if we want to bike to Paris instead of driving? It is unreasonable (and dangerous!) to bike 500 km per day. Let's limit ourselves to biking no more than 200 km in one go.

MATCH p = (:City {name: "Zagreb"})
          -[:Road * bfs (e, v | e.length <= 200)]->
          (:City {name: "Paris"})
RETURN nodes(p);

"What is this special syntax?", you might wonder.

(e, v | e.length <= 200) is called a filter lambda. It's a function that takes an edge symbol e and a vertex symbol v and decides whether this edge and vertex pair should be considered valid in breadth-first expansion by returning true or false (or Null). In the above example, lambda is returning true if edge length is not greater than 200, because we don't want to bike more than 200 km in one go.

6. Let's say we also don't want to visit Vienna on our way to Paris, because we have a lot of friends there and visiting all of them would take up a lot of our time. We just have to update our filter lambda.

MATCH p = (:City {name: "Zagreb"})
          -[:Road * bfs (e, v | e.length <= 200 AND v.name != "Vienna")]->
          (:City {name: "Paris"})
RETURN nodes(p);

As you can see, without the additional restriction we could visit 11 cities. If we want to avoid Vienna, we must visit at least 12 cities.

7. Instead of counting the cities visited, we might want to find the shortest paths in terms of distance travelled. This is a textbook application of Dijkstra's algorithm. The following query will return the list of cities on the shortest path from Zagreb to Paris along with the total length of the path.

MATCH p = (:City {name: "Zagreb"})
          -[:Road * wShortest (e, v | e.length) total_weight]->
          (:City {name: "Paris"})
RETURN nodes(p) AS cities, total_weight;

As you can see, the syntax is quite similar to breadth-first search syntax. Instead of a filter lambda, we need to provide a weight lambda and the total weight symbol. Given an edge and vertex pair, weight lambda must return the cost of expanding to the given vertex using the given edge. The path returned will have the smallest possible sum of costs and it will be stored in the total weight symbol. A limitation of Dijkstra's algorithm is that the cost must be non-negative.

8. We can also combine weight and filter lambdas in the shortest-path query. Let's say we're interested in the shortest path that doesn't require travelling more that 200 km in one go for our bike route.

MATCH p = (:City {name: "Zagreb"})
      -[:Road * wShortest (e, v | e.length) total_weight (e, v | e.length <= 200)]->
      (:City {name: "Paris"})
RETURN nodes(p) AS cities, total_weight;

9. Let's try and find 10 cities that are furthest away from Zagreb.

MATCH (:City {name: "Zagreb"})
      -[:Road * wShortest (e, v | e.length) total_weight]->
      (c:City)
RETURN c, total_weight
ORDER BY total_weight DESC
LIMIT 10;

It is not surprising to see that they are all in Siberia.

To learn more about these algorithms, we suggest you check out their Wikipedia pages: