Titouan Galopin
April 05, 2018
260

# Implementing a lightning fast route search engine in Typescript

In the context of building a mobile app for public transport, I had to implement a route search engine for the region of Oise, France. It was a more complex journey than I expected and I'd like to give a glimpse on how I implemented the Connection Scan Algorithm in Typescript, heavily leveraging the async features of node.

April 05, 2018

## Transcript

1. Implementing a lightning fast route
search engine in Typescript
SFNode April 2018

2. Symfony since 2015 (PHP framework)
Titouan Galopin
@tgalopin
Created bus.io in 2015
(public transport app : https://getbus.io)

3. Agenda
1. Goal
2. The routing problem
3. Historic solutions
4. Connection Scan Algorithm
5. Implementation

4. Goal

5. Let users find the quickest way to
go from a point A (coordinates) to
point B by bus

6. “I want to go from the Golden

7. The routing problem

8. This problem is not new,

9. This problem is not new,
Fortunately, in Computer Science
we love Mathematics!

10. Equivalent problem in Maths:
Shortest Path Problem (SPP)

11. Graphs

12. Graphs
Invented in 1735 by Euler

13. Graphs
Invented in 1735 by Euler
Powerful model for Computer Science
(from AI to blockchain, compilers, ...)

14. Non-oriented Oriented
Source : https://fr.wikipedia.org/wiki/Théorie_des_graphes

15. Source : https://en.wikipedia.org/wiki/Shortest_path_problem
SPP : weighted oriented graphs

16. Historic solutions

17. Djikstra algorithm: 1956
O(n * log n)
n: number of nodes m : number of edges

18. Djikstra algorithm: 1956
O(n * log n)
Bellman–Ford algorithm: 1958
O(n * m)
n: number of nodes m : number of edges

19. Djikstra algorithm: 1956
O(n * log n)
Bellman–Ford algorithm: 1958
O(n * m)
A* algorithm: 1968
Extension of Djikstra, O(n * log n) but much
better in real life, most used today
n: number of nodes m : number of edges

20. Still today, the best generic algorithm
to solve SPP is in
O(n * log n)
n: number of nodes m : number of edges

problems like public transports?

22. Main difference :
the bus/train/… has specific timetables
=> better complexity?

23. Connection Scan Algorithm

24. Connection Scan Algorithm
Developed in Germany in 2012

25. Connection Scan Algorithm
Developed in Germany in 2012
Targets SPP with timetables

26. Connection Scan Algorithm
Developed in Germany in 2012
Targets SPP with timetables
Overall complexity: O(c * log c)
Runtime complexity: O(c) Linear!

27. Connection
A to B
7:42 to 7:45

28. A to B
7:00 to 7:05
B to C
7:06 to 7:09
B to D
7:07 to 7:11
C to E
7:10 to 7:15
B to E
7:11 to 7:18
G to D
7:08 to 7:11
D to E
7:12 to 7:17
E to F
7:20 to 7:29
List of connections instead of Graph
...
...

29. Implementation

30. Preparation (before the query)
Create a list of all the connections
ordered by ascending departing time
O(c * log c)

31. Runtime (during the query)

32. // Map of the the fastest ways to go to each stop
// node name => fastest connection to go to the node
let inConnection: StringMap = {};
// Map of earliest arrival time per node
// node name => arrival time
// (418 = 6 * 60 + 58)
let arrivalTimes: StringMap = { A: 418 };

33. for (let i in connections) {
const connection = connections[i];
// If we never arrived to this connection’s departure stop,
// we never will as connections are ordered by departure time
if (typeof arrivalTimes[connection.departure_stop] === 'undefined') {
continue;
}
// If the connection leaves before we arrive at its departure, it can't be used
if (arrivalTimes[connection.departure_stop] > connection.departure_time) {
continue;
}
// If there already was a connection but the previous connection arrived
// earlier, keep the previous one
if (arrivalTimes[connection.arrival_stop] < connection.arrival_time) {
continue;
}
// Otherwise, we know for sure this connection is better than what we had
arrivalTimes[connection.arrival_stop] = connection.arrival_time;
inConnection[connection.arrival_stop] = connection;
}
A to B
7:00 to 7:05
B to C
7:06 to 7:09
B to D
7:07 to 7:11
C to E
7:10 to 7:15
B to E
7:11 to 7:18
G to D
7:08 to 7:11
D to E
7:12 to 7:17

34. for (let i in connections) {
const connection = connections[i];
// If we never arrived to this connection’s departure stop,
// we never will as connections are ordered by departure time
if (typeof arrivalTimes[connection.departure_stop] === 'undefined') {
continue;
}
// If the connection leaves before we arrive at its departure, it can't be used
if (arrivalTimes[connection.departure_stop] > connection.departure_time) {
continue;
}
// If there already was a connection but the previous connection arrived
// earlier, keep the previous one
if (arrivalTimes[connection.arrival_stop] < connection.arrival_time) {
continue;
}
// Otherwise, we know for sure this connection is better than what we had
arrivalTimes[connection.arrival_stop] = connection.arrival_time;
inConnection[connection.arrival_stop] = connection;
}
A to B
7:00 to 7:05
B to C
7:06 to 7:09
B to D
7:07 to 7:11
C to E
7:10 to 7:15
B to E
7:11 to 7:18
G to D
7:08 to 7:11
D to E
7:12 to 7:17

35. for (let i in connections) {
const connection = connections[i];
// If we never arrived to this connection’s departure stop,
// we never will as connections are ordered by departure time
if (typeof arrivalTimes[connection.departure_stop] === 'undefined') {
continue;
}
// If the connection leaves before we arrive at its departure, it can't be used
if (arrivalTimes[connection.departure_stop] > connection.departure_time) {
continue;
}
// If there already was a connection but the previous connection arrived
// earlier, keep the previous one
if (arrivalTimes[connection.arrival_stop] < connection.arrival_time) {
continue;
}
// Otherwise, we know for sure this connection is better than what we had
arrivalTimes[connection.arrival_stop] = connection.arrival_time;
inConnection[connection.arrival_stop] = connection;
}
A to B
7:00 to 7:05
B to C
7:06 to 7:09
B to D
7:07 to 7:11
C to E
7:10 to 7:15
B to E
7:11 to 7:18
G to D
7:08 to 7:11
D to E
7:12 to 7:17

36. for (let i in connections) {
const connection = connections[i];
// If we never arrived to this connection’s departure stop,
// we never will as connections are ordered by departure time
if (typeof arrivalTimes[connection.departure_stop] === 'undefined') {
continue;
}
// If the connection leaves before we arrive at its departure, it can't be used
if (arrivalTimes[connection.departure_stop] > connection.departure_time) {
continue;
}
// If there already was a connection but the previous connection arrived
// earlier, keep the previous one
if (arrivalTimes[connection.arrival_stop] < connection.arrival_time) {
continue;
}
// Otherwise, we know for sure this connection is better than what we had
arrivalTimes[connection.arrival_stop] = connection.arrival_time;
inConnection[connection.arrival_stop] = connection;
}
A to B
7:00 to 7:05
B to C
7:06 to 7:09
B to D
7:07 to 7:11
C to E
7:10 to 7:15
B to E
7:11 to 7:18
G to D
7:08 to 7:11
D to E
7:12 to 7:17

37. for (let i in connections) {
const connection = connections[i];
// If we never arrived to this connection’s departure stop,
// we never will as connections are ordered by departure time
if (typeof arrivalTimes[connection.departure_stop] === 'undefined') {
continue;
}
// If the connection leaves before we arrive at its departure, it can't be used
if (arrivalTimes[connection.departure_stop] > connection.departure_time) {
continue;
}
// If there already was a connection but the previous connection arrived
// earlier, keep the previous one
if (arrivalTimes[connection.arrival_stop] < connection.arrival_time) {
continue;
}
// Otherwise, we know for sure this connection is better than what we had
arrivalTimes[connection.arrival_stop] = connection.arrival_time;
inConnection[connection.arrival_stop] = connection;
}
A to B
7:00 to 7:05
B to C
7:06 to 7:09
B to D
7:07 to 7:11
C to E
7:10 to 7:15
B to E
7:11 to 7:18
G to D
7:08 to 7:11
D to E
7:12 to 7:17

38. A to B
7:00 to 7:05
B to C
7:06 to 7:09
B to D
7:07 to 7:11
C to E
7:10 to 7:15
B to E
7:11 to 7:18
G to D
7:08 to 7:11
D to E
7:12 to 7:17
// We know these times are
// the earliest possible arrival
// times for each stop
arrivalTimes:
A: 6:58
B: 7:05
C: 7:09
D: 7:11
E: 7:15
// Moreover, we get a list of the
// fastest way to get to each node
inConnection:
B:
C:
D:
E:
A to B
7:00 to 7:05
B to C
7:06 to 7:09
B to D
7:07 to 7:11
C to E
7:10 to 7:15

39. inConnection:
B:
C:
D:
E:
A to B
7:00 to 7:05
B to C
7:06 to 7:09
B to D
7:07 to 7:11
C to E
7:10 to 7:15
let steps = [];
let current = 'E';
while (current !== 'A') {
let step = inConnection[current];
current = step['departure_stop'];
steps.push(step);
}
// Result: A -> B -> C -> E

40. More features!
Handle walks between stops and at the
start/end of the journey
Select the 5 best results in the next minutes

41. But how to keep performance?
Async!

42. OSRM for walks
Modern C++ routing engine for shortest
http://project-osrm.org

43. Search leaving at time T
the connections
connections
CSA Result 1
Query
Search leaving at time T + 1
Search leaving at time T + 2
...
Select best
results
User
the walks between
stops
Find walks from
start point
Find walks to
end point
CSA Result 2
Search leaving at time T + 3

44. In production
https://getbus.io
370 nodes
10998 connections
20 results computed, 10 bests selected
150ms per query

45. Thanks!
Questions?