Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Implementing a lightning fast route search engine in Typescript

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.

364d59ac0b4b4e5eee8aeb27a127d176?s=128

Titouan Galopin

April 05, 2018
Tweet

More Decks by Titouan Galopin

Other Decks in Technology

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 Gate Bridge to

    LinkedIn Headquarters leaving at 6:58”
  7. The routing problem

  8. This problem is not new, mathematicians already thought about it

  9. This problem is not new, mathematicians already thought about it

    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
  21. What about more specific problems like public transports?

  22. Main difference : the bus/train/… has specific timetables => additional

    constraint => 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<Connection> = {}; // Map of earliest arrival time per node // node name => arrival time // (418 = 6 * 60 + 58) let arrivalTimes: StringMap<number> = { 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 paths

    in road networks http://project-osrm.org
  43. Search leaving at time T Prepare in advance the connections

    Load the connections CSA Result 1 Query Search leaving at time T + 1 Search leaving at time T + 2 ... Select best results User Prepare in advance 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?