使用neo4j构建一个地铁旅程计划器

In my previous tutorial, I have covered on Build a Restaurant Recommendation Engine Using Neo4j. In this tutorial, we are going to explore a little more on the user-defined procedures and functions. Such implementations are usually implemented in Java and can be called directly via Cypher. This provides a convenient way for you to create a custom implementation of any graph algorithms that you preferred and use it when querying the dataset in Neo4j.

在之前的教程中,我介绍了使用Neo4j构建餐厅推荐引擎 。 在本教程中,我们将进一步探讨用户定义的过程和函数。 此类实现通常以Java实现,可以直接通过Cypher调用。 这为您提供了一种方便的方法,使您可以创建自己喜欢的任何图形算法的自定义实现,并在Neo4j中查询数据集时使用它。

Since version 4.1.1, Neo4j comes with its own APOC (Awesome Procedure On Cypher) Library. There are two versions available:

从4.1.1版开始,Neo4j附带了自己的APOC(Aysome Procedure on Cypher)库 。 有两个版本可用:

  • APOC Core — procedures and functions that don’t have external dependencies or require configuration

    APOC Core -不具有外部依赖性或需要配置的过程和功能

  • APOC Full —includes everything in APOC Core in addition to extra procedures and functions.

    APOC Full除了额外的过程和功能,还包括APOC Core中的所有内容。

Moreover, Neo4j also provides its own GDSL (Graph Data Science Library) for developers working on machine learning workflows. Some of the algorithms inside this library are still in alpha phase at the time of this writing.

此外,Neo4j还为从事机器学习工作流程的开发人员提供了自己的GDSL(图形数据科学库) 。 在撰写本文时,该库中的某些算法仍处于Alpha阶段。

Our example project will be about a journey planner for metro/subway/mass rapid transit. Behind the scene, it will use a path-finding algorithm provided by APOC Core later on to find the shortest path from a starting station to the end destination.

我们的示例项目将涉及地铁/地铁/快速公交的行程计划器。 在幕后,它将使用稍后由APOC Core提供的寻路算法来查找从起点到终点的最短路径。

Let’s proceed to the next section and start installing the necessary modules.

让我们继续下一节并开始安装必要的模块。

1.设定 (1. Setup)

Before you continue, it is highly recommended to go through the following guide on The Beginner’s Guide to the Neo4j Graph Platform if you are new to Neo4j.

如果您是Neo4j的新手 ,则在继续之前,强烈建议您仔细阅读Neo4j Graph平台初学者指南中的以下指南。

APOC核心 (APOC Core)

By default, the installation of Neo4j comes with the APOC Core jar file. You can easily find the jar file in the following directory. NEO4J_HOME refers to the main directory of Neo4j in your local machine.

默认情况下,Neo4j的安装随附APOC Core jar文件。 您可以在以下目录中轻松找到jar文件。 NEO4J_HOME引用本地计算机中Neo4j的主目录。

$NEO4J_HOME/labs

All you need to do is to copy and paste the jar file into the following directory

您需要做的就是将jar文件复制并粘贴到以下目录中

$NEO4J_HOME/plugins

Remember to restart Neo4j afterwards via the following command for it to take effect.

请记住,之后请通过以下命令重新启动Neo4j,以使其生效。

neo4j console

Use the following command if you are starting it as a background service.

如果您将其作为后台服务启动,请使用以下命令。

neo4j start

You can access Neo4j Browser via the following URL

您可以通过以下URL访问Neo4j浏览器

http://localhost:7474/browser/

Neo4j驱动程序 (Neo4j Driver)

In order to connect your web application to Neo4j graph database, you need to install one of the following drivers based on the programming languages that you use:

为了将您的Web应用程序连接到Neo4j图形数据库,您需要根据使用的编程语言安装以下驱动程序之一:

  • .NET — .NET Standard 2.0

    .NET — .NET Standard 2.0

  • Java —Java 8+ (latest patch releases).

    Java Java 8+(最新修补程序版本)。

  • JavaScript — All LTS versions of Node.JS, specifically the 4.x and 6.x series runtime(s).

    JavaScriptNode.JS的所有LTS版本 ,特别是4.x和6.x系列运行时。

  • Python — CPython 3.5 and above.

    Python -CPython 3.5及更高版本。

  • Go — Work in progress. There is no official release date at the moment.

    Go -进行中。 目前没有官方发布日期。

For this tutorial, I am going to use the Python driver for our web application in FastAPI. You can find the full installation steps for the rest of the drivers via the following link.

对于本教程,我将在FastAPI中为我们的Web应用程序使用Python驱动程序。 您可以通过以下链接找到其余驱动程序的完整安装步骤。

It is highly recommended to create a virtual environment before you install the package. Run the following command in the terminal.

强烈建议您在安装软件包之前创建一个虚拟环境。 在终端中运行以下命令。

pip install neo4j

FastAPI (FastAPI)

Our back-end server will be built on top of FastAPI. If you are a Flask user, feel free to modify it accordingly as you can always migrate it from Flask to FastAPI later on. Install it via pip install as follows:

我们的后端服务器将建立在FastAPI之上。 如果您是Flask用户,请随时进行相应的修改,因为以后您始终可以将其从Flask迁移到FastAPI 。 通过pip install进行安装,如下所示:

pip install fastapi

Besides, you will need an ASGI server as well. I am going to use the recommended ASGI server called Uvicorn. In the same terminal, run the following command to install it

此外,您还将需要一台ASGI服务器。 我将使用推荐的ASGI服务器Uvicorn 。 在同一终端中,运行以下命令进行安装

pip install uvicorn

数据集 (Dataset)

I am going to use the following network map as the dataset for my use case. It is based on the actual network map by one of the the public transport operator in Singapore.

我将使用以下网络地图作为用例的数据集。 它基于新加坡公共交通运营商之一的实际网络地图。

Image for post
SMRT Corporation website SMRT Corporation网站

The network map consists of 6 MRT lines and 3 LRT lines as shown in the figure below:

网络图由6条MRT线路和3条LRT线路组成,如下图所示:

Image for post

In order to simplify our project, our dataset will only contain a strip-down version of the map above. Hence, I am going to ignore the rest of the lines and keep only the following 5 MRT lines.

为了简化我们的项目,我们的数据集将仅包含上面地图的简化版本。 因此,我将忽略其余的行,仅保留以下5条MRT行。

  • East-West Line (green)

    东西线(绿色)
  • North-South Line (red)

    南北线(红色)
  • North-East Line (purple)

    东北线(紫色)
  • Circle Line (circle)

    圆线(圆)
  • Downtown Line (blue)

    市区线(蓝色)

2. Neo4j数据库 (2. Neo4j Database)

In this section, we will be executing the graph query language (Cypher) for inserting data to and querying data from Neo4j graph database. You can use an existing or a new database for it.

在本节中,我们将执行图查询语言(Cypher),用于向Neo4j图数据库插入数据和从中查询数据。 您可以使用现有数据库或新数据库。

Before we continue, let’s list down all of the nodes and relationships for our use case. We are going to use it to model our domain and create the Cypher query later on.

在继续之前,让我们列出用例的所有节点和关系。 我们将使用它来为我们的域建模并稍后创建Cypher查询。

假设条件 (Assumption)

To keep things simple and short, I am going to make the following assumptions for our use case.

为了使事情简单明了,我将对我们的用例进行以下假设。

  • There will be no waiting time in between each stop. In actual use case, there should be a waiting time for the passengers to align and board the train.

    每个站点之间将没有等待时间。 在实际使用情况下,应该有一个等待时间,让乘客对齐并上车。
  • There will be no traveling time when changing lines between interchanges. In actual use case, you have to walk for some time from one platform to another when changing lines.

    在换乘处换线时将没有旅行时间。 在实际用例中,换线时必须从一个平台走到另一个平台一段时间。
  • The time taken from Station A to Station B will be always be the same regardless if you are traveling on different lines. In actual use case, the time taken to travel from Raffles Place to City Hall via East-West Line is different from the time taken via North-South Line.

    无论您乘坐的是不同的线路,从Station AStation B将始终相同。 在实际使用案例中,通过East-West LineRaffles PlaceCity Hall所需的时间与通过North-South Line所需的时间有所不同。

  • The metric used for our journey planner is based on just total travel time in between stations. In actual use case, you have to consider the need to tap in or out of station which affects the travel fares.

    我们的行程计划者使用的指标仅基于站点之间的总行程时间。 在实际使用情况下,您必须考虑需要进站或出站,这会影响旅行票价。

Feel free to modify and model the domain based on your own use cases.

可以根据自己的用例随意修改和建模域。

站(节点) (Station (Node))

Each station represent a single Node with the following properties:

每个工作站代表具有以下属性的单个Node

  • name — Name of the station. All of the names will be in small-case.

    name工作站名称。 所有名称都用小写字母表示。

  • mrt — Represent the line where the station is located. I am using the short-hand name instead. Hence, East-West Line will be ew while Circle Line will be cc. For interchanges, it will be marked as x instead. This property will determine the color of the icon later on in React app.

    mrt —代表桩号所在的线路。 我改用简称。 因此, East-West Line将为ewCircle Line将为cc 。 对于互换,它将被标记为x 。 此属性将稍后在React应用程序中确定图标的颜色。

TRAVEL_TO(关系) (TRAVEL_TO (Relationship))

The relationship between two Station is denoted by TRAVEL_TO with the following property:

两个Station之间的关系由TRAVEL_TO表示,具有以下属性:

  • time — Represent the time taken to travel from one station to another. It exclude the walking time for interchanges when changing from one MRT line to another. The dataset for the time in between stations is based on the output results from TransitLink website. It will be used as the cost function for a path-finding algorithm later on.

    time -表示从一个站点到另一个站点所花费的时间。 从一条MRT线换成另一条MRT线时,它不包括换乘的步行时间。 站点之间时间的数据集基于TransitLink网站的输出结果。 稍后,它将用作寻路算法的成本函数。

清除资料库 (Clear database)

You can run the following Cypher to clean up the database. Any existing nodes and their relationships will be removed completely from your database.

您可以运行以下Cypher清理数据库。 任何现有的节点及其关系都将从数据库中完全删除。

MATCH (n) DETACH DELETE n

创建数据集 (Create dataset)

You can create two Station nodes and link them up with a relationship as follows. I am using lower-case as the name to standardize the input parameters for the API call later on.

您可以创建两个Station节点,并按如下所示将它们链接起来。 我使用小写字母作为名称,以稍后标准化API调用的输入参数。

CREATE (tuaslink:Station {name:"tuas link", mrt:"ew"})-[:TRAVEL_TO {time: 2}]->(tuaswestroad:Station {name:"tuas west road", mrt:"ew"})

It will create two independent Nodes with the Station label and build TRAVEL_TO relationship between both of them. By now, you should notice that the relationship is created as one-direction. This is the default behaviour as Neo4j only allows you to create one-direction relationship between nodes. However, you can specify to ignore the direction when querying to get the desired bi-directional results.

它将创建两个带有Station标签的独立节点,并在两个节点之间建立TRAVEL_TO关系。 现在,您应该注意到该关系已创建为单向。 这是默认行为,因为Neo4j仅允许您在节点之间创建单向关系。 但是,您可以指定在查询时忽略方向以获得所需的双向结果。

Subsequently, you can reuse the old Station node via the declared name and link it to a new Station node as follow.

随后,您可以通过声明的名称重用旧的Station节点,并将其链接到新的Station节点,如下所示。

(tuaswestroad)-[:TRAVEL_TO {time: 8}]->(tuascrescent:Station {name:"tuas crescent", mrt:"ew"})

You need to be careful when linking the nodes as it will cause duplication in the result if there exist two different relationships from Station A to Station B. For example, you can connect from Raffles Place to City Hall via both East-West Line and North-South Line. Once you have declared the following Cypher for East-West Line.

链接节点时需要小心,因为如果从Station AStation B存在两种不同的关系,它将导致结果重复。 例如,您可以通过East-West LineNorth-South LineRaffles Place连接到City Hall 。 一旦您为East-West Line声明了以下Cypher。

(rafflesplace:Station {name:"raffles place", mrt:"x"})-[:TRAVEL_TO {time: 2}]->(cityhall:Station {name:"city hall", mrt:"x"})

You must not declare it again for North-South Line.

您不能再对North-South Line进行声明。

(rafflesplace)-[:TRAVEL_TO {time: 2}]->(cityhall)

If you intend to model both stations as different entities, simply create two different Nodes for the same station and link them properly.

如果要将两个站点建模为不同的实体,只需为同一站点创建两个不同的节点并正确链接它们。

Let’s combine all of our dataset into a single query. The following gist contains the complete Cypher query for this project. You can run it directly in Neo4j console.

让我们将所有数据集合并到一个查询中。 以下要点包含此项目的完整Cypher查询。 您可以直接在Neo4j控制台中运行它。

CREATE (tuaslink:Station {name:"tuas link", mrt:"ew"})-[:TRAVEL_TO {time: 2}]->(tuaswestroad:Station {name:"tuas west road", mrt:"ew"}),
       (tuaswestroad)-[:TRAVEL_TO {time: 8}]->(tuascrescent:Station {name:"tuas crescent", mrt:"ew"}),
       (tuascrescent)-[:TRAVEL_TO {time: 3}]->(gulcircle:Station {name:"gul circle", mrt:"ew"}),
       (gulcircle)-[:TRAVEL_TO {time: 3}]->(jookoon:Station {name:"joo koon", mrt:"ew"}),
       (jookoon)-[:TRAVEL_TO {time: 4}]->(pioneer:Station {name:"pioneer", mrt:"ew"}),
       (pioneer)-[:TRAVEL_TO {time: 2}]->(boonlay:Station {name:"boon lay", mrt:"ew"}),
       (boonlay)-[:TRAVEL_TO {time: 3}]->(lakeside:Station {name:"lakeside", mrt:"ew"}),
       (lakeside)-[:TRAVEL_TO {time: 2}]->(chinesegarden:Station {name:"chinese garden", mrt:"ew"}),
       (chinesegarden)-[:TRAVEL_TO {time: 3}]->(jurongeast:Station {name:"jurong east", mrt:"x"}),
       (jurongeast)-[:TRAVEL_TO {time: 4}]->(clementi:Station {name:"clementi", mrt:"ew"}),
       (clementi)-[:TRAVEL_TO {time: 3}]->(dover:Station {name:"dover", mrt:"ew"}),
       (dover)-[:TRAVEL_TO {time: 3}]->(bounavista:Station {name:"bouna vista", mrt:"x"}),
       (bounavista)-[:TRAVEL_TO {time: 2}]->(commonwealth:Station {name:"commonwealth", mrt:"ew"}),
       (commonwealth)-[:TRAVEL_TO {time: 2}]->(queenstown:Station {name:"queenstown", mrt:"ew"}),
       (queenstown)-[:TRAVEL_TO {time: 3}]->(redhill:Station {name:"red hill", mrt:"ew"}),
       (redhill)-[:TRAVEL_TO {time: 2}]->(tiongbahru:Station {name:"tiong bahru", mrt:"ew"}),
       (tiongbahru)-[:TRAVEL_TO {time: 3}]->(outrampark:Station {name:"outram park", mrt:"x"}),
       (outrampark)-[:TRAVEL_TO {time: 2}]->(tanjongpagar:Station {name:"tanjong pagar", mrt:"ew"}),
       (tanjongpagar)-[:TRAVEL_TO {time: 3}]->(rafflesplace:Station {name:"raffles place", mrt:"x"}),
       (rafflesplace)-[:TRAVEL_TO {time: 2}]->(cityhall:Station {name:"city hall", mrt:"x"}),
       (cityhall)-[:TRAVEL_TO {time: 3}]->(bugis:Station {name:"bugis", mrt:"x"}),
       (bugis)-[:TRAVEL_TO {time: 2}]->(lavender:Station {name:"lavender", mrt:"ew"}),
       (lavender)-[:TRAVEL_TO {time: 2}]->(kallang:Station {name:"kallang", mrt:"ew"}),
       (kallang)-[:TRAVEL_TO {time: 3}]->(aljunied:Station {name:"aljunied", mrt:"ew"}),
       (aljunied)-[:TRAVEL_TO {time: 2}]->(payalebar:Station {name:"paya lebar", mrt:"x"}),
       (payalebar)-[:TRAVEL_TO {time: 2}]->(eunos:Station {name:"eunos", mrt:"ew"}),
       (eunos)-[:TRAVEL_TO {time: 3}]->(kembangan:Station {name:"kembangan", mrt:"ew"}),
       (kembangan)-[:TRAVEL_TO {time: 3}]->(bedok:Station {name:"bedok", mrt:"ew"}),
       (bedok)-[:TRAVEL_TO {time: 3}]->(tanahmerah:Station {name:"tanah merah", mrt:"ew"}),
       (tanahmerah)-[:TRAVEL_TO {time: 3}]->(simei:Station {name:"simei", mrt:"ew"}),
       (simei)-[:TRAVEL_TO {time: 3}]->(tampines:Station {name:"tampines", mrt:"x"}),
       (tampines)-[:TRAVEL_TO {time: 3}]->(pasirris:Station {name:"pasir ris", mrt:"ew"}),
       (tanahmerah)-[:TRAVEL_TO {time: 3}]->(expo:Station {name:"expo", mrt:"x"}),
       (expo)-[:TRAVEL_TO {time: 4}]->(changiairport:Station {name:"changi airport", mrt:"ew"}),


       (jurongeast)-[:TRAVEL_TO {time: 3}]->(bukitbatok:Station {name:"bukit batok", mrt:"ns"}),
       (bukitbatok)-[:TRAVEL_TO {time: 2}]->(bukitgombak:Station {name:"bukit gombak", mrt:"ns"}),
       (bukitgombak)-[:TRAVEL_TO {time: 4}]->(choachukang:Station {name:"choa chu kang", mrt:"ns"}),
       (choachukang)-[:TRAVEL_TO {time: 3}]->(yewtee:Station {name:"yew tee", mrt:"ns"}),
       (yewtee)-[:TRAVEL_TO {time: 5}]->(kranji:Station {name:"kranji", mrt:"ns"}),
       (kranji)-[:TRAVEL_TO {time: 3}]->(marsiling:Station {name:"marsiling", mrt:"ns"}),
       (marsiling)-[:TRAVEL_TO {time: 2}]->(woodlands:Station {name:"woodlands", mrt:"ns"}),
       (woodlands)-[:TRAVEL_TO {time: 3}]->(admiralty:Station {name:"admiralty", mrt:"ns"}),
       (admiralty)-[:TRAVEL_TO {time: 3}]->(sembawang:Station {name:"sembawang", mrt:"ns"}),
       (sembawang)-[:TRAVEL_TO {time: 3}]->(canberra:Station {name:"canberra", mrt:"ns"}),
       (canberra)-[:TRAVEL_TO {time: 3}]->(yishun:Station {name:"yishun", mrt:"ns"}),
       (yishun)-[:TRAVEL_TO {time: 2}]->(khatib:Station {name:"khatib", mrt:"ns"}),
       (khatib)-[:TRAVEL_TO {time: 6}]->(yiochukang:Station {name:"yio chu kang", mrt:"ns"}),
       (yiochukang)-[:TRAVEL_TO {time: 2}]->(angmokio:Station {name:"ang mo kio", mrt:"ns"}),
       (angmokio)-[:TRAVEL_TO {time: 4}]->(bishan:Station {name:"bishan", mrt:"x"}),
       (bishan)-[:TRAVEL_TO {time: 2}]->(braddell:Station {name:"braddell", mrt:"ns"}),
       (braddell)-[:TRAVEL_TO {time: 2}]->(toapayoh:Station {name:"toa payoh", mrt:"ns"}),
       (toapayoh)-[:TRAVEL_TO {time: 3}]->(novena:Station {name:"novena", mrt:"ns"}),
       (novena)-[:TRAVEL_TO {time: 2}]->(newton:Station {name:"newton", mrt:"x"}),
       (newton)-[:TRAVEL_TO {time: 3}]->(orchard:Station {name:"orchard", mrt:"ns"}),
       (orchard)-[:TRAVEL_TO {time: 2}]->(somerset:Station {name:"somerset", mrt:"ns"}),
       (somerset)-[:TRAVEL_TO {time: 2}]->(dhobyghaut:Station {name:"dhoby ghaut", mrt:"x"}),
       (dhobyghaut)-[:TRAVEL_TO {time: 3}]->(cityhall),
       (rafflesplace)-[:TRAVEL_TO {time: 2}]->(marinabay:Station {name:"marina bay", mrt:"ns"}),
       (marinabay)-[:TRAVEL_TO {time: 3}]->(marinasouthpier:Station {name:"marina south pier", mrt:"ns"}),
       
       (harbourfront:Station {name:"harbourfront", mrt:"cc"})-[:TRAVEL_TO {time: 2}]->(telokblangah:Station {name:"telok blangah", mrt:"cc"}),
       (telokblangah)-[:TRAVEL_TO {time: 2}]->(labradorpark:Station {name:"labrador park", mrt:"cc"}),
       (labradorpark)-[:TRAVEL_TO {time: 3}]->(pasirpanjang:Station {name:"pasir panjang", mrt:"cc"}),
       (pasirpanjang)-[:TRAVEL_TO {time: 2}]->(hawparvilla:Station {name:"haw par villa", mrt:"cc"}),
       (hawparvilla)-[:TRAVEL_TO {time: 2}]->(kentridge:Station {name:"kent ridge", mrt:"cc"}),
       (kentridge)-[:TRAVEL_TO {time: 2}]->(onenorth:Station {name:"one-north", mrt:"cc"}),
       (onenorth)-[:TRAVEL_TO {time: 2}]->(bounavista),
       (bounavista)-[:TRAVEL_TO {time: 2}]->(hollandvillage:Station {name:"holland village", mrt:"cc"}),
       (hollandvillage)-[:TRAVEL_TO {time: 3}]->(farrerroad:Station {name:"farrer road", mrt:"cc"}),
       (farrerroad)-[:TRAVEL_TO {time: 2}]->(botanicgardens:Station {name:"botanic gardens", mrt:"x"}),
       (botanicgardens)-[:TRAVEL_TO {time: 5}]->(caldecott:Station {name:"caldecott", mrt:"cc"}),
       (caldecott)-[:TRAVEL_TO {time: 2}]->(marymount:Station {name:"marymount", mrt:"cc"}),
       (marymount)-[:TRAVEL_TO {time: 3}]->(bishan),
       (bishan)-[:TRAVEL_TO {time: 2}]->(lorongchuan:Station {name:"lorong chuan", mrt:"cc"}),
       (lorongchuan)-[:TRAVEL_TO {time: 2}]->(serangoon:Station {name:"serangoon", mrt:"x"}),
       (serangoon)-[:TRAVEL_TO {time: 3}]->(bartley:Station {name:"bartley", mrt:"cc"}),
       (bartley)-[:TRAVEL_TO {time: 2}]->(taiseng:Station {name:"tai seng", mrt:"cc"}),
       (taiseng)-[:TRAVEL_TO {time: 2}]->(macpherson:Station {name:"macpherson", mrt:"x"}),
       (macpherson)-[:TRAVEL_TO {time: 2}]->(payalebar),
       (payalebar)-[:TRAVEL_TO {time: 2}]->(dakota:Station {name:"dakota", mrt:"cc"}),
       (dakota)-[:TRAVEL_TO {time: 2}]->(mountbatten:Station {name:"mountbatten", mrt:"cc"}),
       (mountbatten)-[:TRAVEL_TO {time: 2}]->(stadium:Station {name:"stadium", mrt:"cc"}),
       (stadium)-[:TRAVEL_TO {time: 2}]->(nicollhighway:Station {name:"nicoll highway", mrt:"cc"}),
       (nicollhighway)-[:TRAVEL_TO {time: 2}]->(promenade:Station {name:"promenade", mrt:"x"}),
       (promenade)-[:TRAVEL_TO {time: 2}]->(bayfront:Station {name:"bayfront", mrt:"x"}),
       (promenade)-[:TRAVEL_TO {time: 2}]->(esplanade:Station {name:"esplanade", mrt:"cc"}),
       (esplanade)-[:TRAVEL_TO {time: 2}]->(brasbasah:Station {name:"bras basah", mrt:"cc"}),
       (brasbasah)-[:TRAVEL_TO {time: 2}]->(dhobyghaut),


       (harbourfront)-[:TRAVEL_TO {time: 3}]->(outrampark),
       (outrampark)-[:TRAVEL_TO {time: 2}]->(chinatown:Station {name:"chinatown", mrt:"x"}),
       (chinatown)-[:TRAVEL_TO {time: 2}]->(clarkequay:Station {name:"clarkequay", mrt:"ne"}),
       (clarkequay)-[:TRAVEL_TO {time: 2}]->(dhobyghaut),
       (dhobyghaut)-[:TRAVEL_TO {time: 2}]->(littleindia:Station {name:"little india", mrt:"x"}),
       (littleindia)-[:TRAVEL_TO {time: 2}]->(farrerpark:Station {name:"farrer park", mrt:"ne"}),
       (farrerpark)-[:TRAVEL_TO {time: 2}]->(boonkeng:Station {name:"boon keng", mrt:"ne"}),
       (boonkeng)-[:TRAVEL_TO {time: 3}]->(potongpasir:Station {name:"potong pasir", mrt:"ne"}),
       (potongpasir)-[:TRAVEL_TO {time: 1}]->(woodleigh:Station {name:"woodleigh", mrt:"ne"}),
       (woodleigh)-[:TRAVEL_TO {time: 2}]->(serangoon),
       (serangoon)-[:TRAVEL_TO {time: 3}]->(kovan:Station {name:"kovan", mrt:"ne"}),
       (kovan)-[:TRAVEL_TO {time: 2}]->(hougang:Station {name:"hougang", mrt:"ne"}),
       (hougang)-[:TRAVEL_TO {time: 2}]->(buangkok:Station {name:"buangkok", mrt:"ne"}),
       (buangkok)-[:TRAVEL_TO {time: 2}]->(sengkang:Station {name:"sengkang", mrt:"ne"}),
       (sengkang)-[:TRAVEL_TO {time: 3}]->(punggol:Station {name:"punggol", mrt:"ne"}),


       (bukitpanjang:Station {name:"bukit panjang", mrt:"dt"})-[:TRAVEL_TO {time: 2}]->(cashew:Station {name:"cashew", mrt:"dt"}),
       (cashew)-[:TRAVEL_TO {time: 3}]->(hillview:Station {name:"hillview", mrt:"dt"}),
       (hillview)-[:TRAVEL_TO {time: 2}]->(beautyworld:Station {name:"beauty world", mrt:"dt"}),
       (beautyworld)-[:TRAVEL_TO {time: 2}]->(kingalbertpark:Station {name:"king albert park", mrt:"dt"}),
       (kingalbertpark)-[:TRAVEL_TO {time: 2}]->(sixthavenue:Station {name:"sixth avenue", mrt:"dt"}),
       (sixthavenue)-[:TRAVEL_TO {time: 2}]->(tankahkee:Station {name:"tan kah kee", mrt:"dt"}),
       (tankahkee)-[:TRAVEL_TO {time: 2}]->(botanicgardens),
       (botanicgardens)-[:TRAVEL_TO {time: 2}]->(stevens:Station {name:"stevens", mrt:"dt"}),
       (stevens)-[:TRAVEL_TO {time: 3}]->(newton),
       (newton)-[:TRAVEL_TO {time: 1}]->(littleindia),
       (littleindia)-[:TRAVEL_TO {time: 2}]->(rochor:Station {name:"rochor", mrt:"dt"}),
       (rochor)-[:TRAVEL_TO {time: 2}]->(bugis),
       (bugis)-[:TRAVEL_TO {time: 2}]->(promenade),
       (bayfront)-[:TRAVEL_TO {time: 1}]->(downtown:Station {name:"downtown", mrt:"dt"}),
       (downtown)-[:TRAVEL_TO {time: 2}]->(telokayer:Station {name:"telok ayer", mrt:"dt"}),
       (telokayer)-[:TRAVEL_TO {time: 2}]->(chinatown),
       (chinatown)-[:TRAVEL_TO {time: 2}]->(fortcanning:Station {name:"fort canning", mrt:"dt"}),
       (fortcanning)-[:TRAVEL_TO {time: 2}]->(bencoolen:Station {name:"bencoolen", mrt:"dt"}),
       (bencoolen)-[:TRAVEL_TO {time: 1}]->(jalanbesar:Station {name:"jalan besar", mrt:"dt"}),
       (jalanbesar)-[:TRAVEL_TO {time: 2}]->(bendemeer:Station {name:"bendemeer", mrt:"dt"}),
       (bendemeer)-[:TRAVEL_TO {time: 2}]->(geylangbahru:Station {name:"geylang bahru", mrt:"dt"}),
       (geylangbahru)-[:TRAVEL_TO {time: 2}]->(mattar:Station {name:"mattar", mrt:"dt"}),
       (mattar)-[:TRAVEL_TO {time: 2}]->(macpherson),
       (macpherson)-[:TRAVEL_TO {time: 2}]->(ubi:Station {name:"ubi", mrt:"dt"}),
       (ubi)-[:TRAVEL_TO {time: 2}]->(kakibukit:Station {name:"kaki bukit", mrt:"dt"}),
       (kakibukit)-[:TRAVEL_TO {time: 2}]->(bedoknorth:Station {name:"bedok north", mrt:"dt"}),
       (bedoknorth)-[:TRAVEL_TO {time: 2}]->(bedokreservior:Station {name:"bedok reservior", mrt:"dt"}),
       (bedokreservior)-[:TRAVEL_TO {time: 3}]->(tampineswest:Station {name:"tampines west", mrt:"dt"}),
       (tampineswest)-[:TRAVEL_TO {time: 2}]->(tampines),
       (tampines)-[:TRAVEL_TO {time: 2}]->(tampineseast:Station {name:"tampines east", mrt:"dt"}),
       (tampineseast)-[:TRAVEL_TO {time: 3}]->(upperchangi:Station {name:"upper changi", mrt:"dt"}),
       (upperchangi)-[:TRAVEL_TO {time: 2}]->(expo)

Once you execute the Cypher query, you should see the following user interface

执行Cypher查询后,您应该会看到以下用户界面

Image for post

得到所有 (Get all)

In fact, you can run the following query to get all of the Nodes and their relationships

实际上,您可以运行以下查询来获取所有节点及其关系

MATCH (n) RETURN (n)

You should get the following result in your Neo4j Browser.

您应该在Neo4j浏览器中获得以下结果。

Image for post

Dijkstra(路径查找算法) (Dijkstra (Path-finding algorithm))

APOC Core comes with a few useful path-finding algorithms such as dijkstra and astar. In this tutorial, I am going to use dijkstra since we only have one cost function. Based on the official documentation, it accepts the following input parameters

APOC Core附带了一些有用的寻路算法,例如dijkstraastar 。 在本教程中,我将使用dijkstra因为我们只有一个成本函数。 根据官方文档 ,它接受以下输入参数

  • startNode — Starting node for the path-finding algorithm.

    startNode —路径查找算法的起始节点。

  • endNode — End destination node for the path-finding algorithm.

    endNode —路径查找算法的最终目标节点。

  • relationshipTypesAndDirections — A string represents the relationship between the nodes. You can specify the directions as well.

    relationshipTypesAndDirections —一个字符串,表示节点之间的关系。 您也可以指定方向。

  • weightPropertyName — A string represent the name of the property for the cost function.

    weightPropertyName —一个字符串,表示成本函数的属性名称。

  • defaultWeight — A default weight for the property if it is not present in the node.

    defaultWeight —属性的默认权重(如果节点中不存在)。

  • numberOfWantedPaths — The number of paths to be returned. The default value is 1.

    numberOfWantedPaths —要返回的路径数。 预设值为1。

and return two output parameters

并返回两个输出参数

  • path — Paths for the path-finding journeys.

    path -路径,路径寻找的旅程。

  • weight — Total cost for the path-finding journeys.

    weight -寻路之旅的总成本。

You can ignore both the defaultWeight and numberOfWantedPaths parameters if you are looking for just the best path from the algorithm.

如果仅从算法中寻找最佳路径,则可以忽略defaultWeightnumberOfWantedPaths参数。

从A到B的最佳路径 (Best path from A to B)

The following example illustrates the Cypher to get the best path from Jurong East to Dhoby Ghaut.

以下示例说明了Cypher从Jurong EastDhoby Ghaut的最佳路径。

MATCH (start:Station {name: 'jurong east'}), (end:Station {name: 'dhoby ghaut'})
CALL apoc.algo.dijkstra(start, end, 'TRAVEL_TO', 'time') YIELD path, weight
RETURN path, weight

You should get the following output

您应该获得以下输出

Image for post

从A到B的三大路径 (Top three paths from A to B)

Let’s say you wanted to get the top 3 best paths from the algorithm. You should use the following Cypher

假设您想从算法中获得3条最佳路径。 您应该使用以下Cypher

MATCH (start:Station {name: 'jurong east'}), (end:Station {name: 'dhoby ghaut'})
CALL apoc.algo.dijkstra(start, end, 'TRAVEL_TO', 'time', 5, 3) YIELD path, weight
RETURN path, weight

Upon execution, Neo4j Browser will display the following result

执行后,Neo4j浏览器将显示以下结果

Image for post

3. Python驱动程序 (3. Python Driver)

In this section, we are going to create a simple FastAPI back-end that connects to Neo4j database via its Python driver. In your working directory, create a new Python file. I am going to call it journey.py.

在本节中,我们将创建一个简单的FastAPI后端,该后端通过其Python驱动程序连接到Neo4j数据库。 在您的工作目录中,创建一个新的Python文件。 我将其称为journey.py

进口 (Import)

Add the following import declaration at the top of your Python file.

在Python文件顶部添加以下导入声明。

from neo4j import GraphDatabase
import logging
from neo4j.exceptions import ServiceUnavailable

旅程班 (Journey class)

Next, create a new class and initialize the following functions inside it

接下来,创建一个新类并在其中初始化以下函数

class Journey:
def __init__(self, uri, user, password):
self.driver = GraphDatabase.driver(uri, auth=(user, password)) def close(self):
self.driver.close()

This class is responsible for the following functionalities:

此类负责以下功能:

  • return all of the Stations’ name list in the database

    返回数据库中所有站点的名称列表
  • return the best path based on a starting point and ending destination using dijkstra path-finding algorithm

    使用dijkstra路径查找算法,根据起点和终点返回最佳路径

获取电台名称 (Get Stations’ Name)

Continue by appending the following code inside the Journey class. The first function initialize a session via context manager. Inside the function, we are going to call the read_transaction() method and pass in the second function which will return a dictionary as result.

通过在Journey类中附加以下代码,继续进行操作。 第一个功能通过上下文管理器初始化会话。 在函数内部,我们将调用read_transaction()方法并传入第二个函数,该函数将返回字典作为结果。

As for the second function, it is mainly responsible for executing the query string via run() function. I am using dict comprehension and title() function to return the names as Title case. It is recommended to declare this function as staticmethod based on the official documentation.

至于第二个函数,它主要负责通过run()函数执行查询字符串。 我正在使用dict comprehension和title()函数以Title大小写形式返回Title 。 建议根据官方文档将此函数声明为staticmethod

def find_all(self):
    with self.driver.session() as session:
        result = session.read_transaction(self._find_all)
        return result


@staticmethod
def _find_all(tx):
    query = (
        '''MATCH (n:Station)
        RETURN n.name AS name
        ORDER BY n.name'''
    )
    result = tx.run(query)
    try:
        return {row["name"]:row["name"].title() for row in result}
    except ServiceUnavailable as exception:
        logging.error("{query} raised an error: \n {exception}".format(query=query, exception=exception))
        raise

获得最佳路径 (Get Best Paths)

Let’s create two more functions for getting the best paths using dijkstra algorithm. It accepts the following parameters:

让我们再创建两个函数,以使用dijkstra算法获得最佳路径。 它接受以下参数:

  • start_node — Starting point of the journey

    start_node —旅程的起点

  • end_node — End destination of the journey

    end_node —旅程的终点

  • count — Number of paths to be returned

    count —要返回的路径数

def find_journey(self, start_node, end_node, count):
    with self.driver.session() as session:
        result = session.read_transaction(self._find_and_return_journey, start_node, end_node, count)
        return result


@staticmethod
def _find_and_return_journey(tx, start_node, end_node, count):
    query = (
        '''MATCH (start:Station {name: $start_node}), (end:Station {name: $end_node})
        CALL apoc.algo.dijkstra(start, end, 'TRAVEL_TO', 'time', 3, $count) YIELD path, weight
        RETURN path, weight'''
    )
    result = tx.run(query, start_node=start_node, end_node=end_node, count=count)
    try:
        return [{"path": row["path"], "weight": row["weight"]} for row in result]
    except ServiceUnavailable as exception:
        logging.error("{query} raised an error: \n {exception}".format(query=query, exception=exception))
        raise

4. FastAPI服务器 (4. FastAPI Server)

As I have mentioned earlier, our back-end server is based on FastAPI. Create a new Python file called myapp.py in the same directory as journey.py.

如前所述,我们的后端服务器基于FastAPI。 创建一个名为一个新的Python文件myapp.py在同一目录journey.py

进口 (Import)

At the top of the file, add the following import statements.

在文件顶部,添加以下导入语句。

from fastapi import FastAPI
import journey
import atexit
from fastapi.middleware.cors import CORSMiddleware

journey is the name of the module that we have created earlier. If both of the files are not in the same directory, kindly modify it accordingly.

journey是我们之前创建的模块的名称。 如果两个文件都不在同一目录中,请相应地对其进行修改。

I am using atexit to execute the close() function when quitting the web server. For more information, have a look at the following tutorial on How to Create Exit Handlers for Your Python App.

退出Web服务器时,我正在使用atexit执行close()函数。 有关更多信息,请参见以下有关如何为Python应用程序创建退出处理程序的教程。

CORSMiddleware is required to prevent issue when you are making an AJAX or fetch call from any front-end applications.

当您从任何前端应用程序进行AJAXfetch CORSMiddleware ,需要使用CORSMiddleware来防止问题。

初始化 (Initialization)

Initialize the following variables which are the credentials for authenticating to Neo4j database.

初始化以下变量,这些变量是用于向Neo4j数据库进行身份验证的凭据。

uri = "neo4j://localhost:7687"
user = "neo4j"
password = "neo4j"neo_db = journey.Journey(uri, user, password)

退出处理程序 (Exit Handler)

After that, add the following code that serves to close the connection to our Neo4j database.

之后,添加以下代码以关闭与Neo4j数据库的连接。

def exit_application():
neo_db.close()atexit.register(exit_application)

FastAPI (FastAPI)

Once you are done with it, create a new instance of FastAPI. Besides that, let’s specify a variable for origins and pass it as input prameters when calling the add_middleware() function.

完成后,创建一个新的FastAPI实例。 除此之外,让我们为origins指定一个变量,并在调用add_middleware()函数时将其作为输入参数传递。

app = FastAPI()origins = [
"http://localhost:3000"
]app.add_middleware(
CORSMiddleware,
allow_origins=origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)

origins consists of a list of URLs that will call your FastAPI server. Let’s say you have a React application that run locally on port 3000. You should add the following URL to the list

origins由将调用您的FastAPI服务器的URL列表组成。 假设您有一个在端口3000本地运行的React应用程序。您应该将以下URL添加到列表中

http://localhost:3000

Create a new GET route which returns all of the Stations’ name.

创建一个新的GET路由,该路由返回所有Station的名称。

@app.get('/get-station')
async def get_station():
result = neo_db.find_all()return result

After that, create another GET route for getting the best path. We are going to link it up with the find_journey() function which accepts three parameters. Each row consists of a dictionary object which contains path and weight parameter. One thing to note is that the nodes inside path are not in order and you can link them via id. The first node can be the starting point or the end destination.

之后,创建另一个GET路径以获得最佳路径。 我们将其与接受三个参数的find_journey()函数链接起来。 每行包含一个字典对象,其中包含pathweight参数。 需要注意的一件事是path内的节点顺序不正确,您可以通过id链接它们。 第一个节点可以是起点或终点。

@app.get('/get-journey')
async def get_journey(start: str, end: str, count: int):
    result = neo_db.find_journey(start, end, count)
    
    journey = []
    
    # loop over the result, each row contains path and weight
    for row in result:
        paths = []
        # used to store the last node information for synchronization
        last_node_id = -1
        last_node_mrt = "x"


        for path in row['path']:
            # get all the nodes in the path
            nodes = [n for n in path.nodes]


            # append the result if it is the first node, mostly for visualization purpose
            if(last_node_id == -1):
                id = 0
                if(nodes[0]['name'] != start):
                    id = 1
                paths.append({"name": nodes[id]['name'].title(), "mrt": nodes[id]['mrt'], "time": "start here"})
                last_node_id = nodes[id].id
                last_node_mrt = nodes[id]['mrt']


            # flag to determine is we should use the first element or the second element as they are marked by id and might not be in order
            id = 0
            if(last_node_id != nodes[1].id):
                id = 1


            # use information from the previous node if it is an interchange
            mrt = nodes[id]['mrt']
            if(nodes[id]['mrt'] == 'x'):
                mrt = last_node_mrt


            paths.append({"name": nodes[id]['name'].title(), "mrt": mrt,"time": "%s minutes" % (path['time'])})
            last_node_id = nodes[id].id
            last_node_mrt = mrt
        journey.append({"path": paths, "weight": row['weight']})


    return journey

运行FastAPI (Running FastAPI)

Run the following code to start your FastAPI server:

运行以下代码以启动您的FastAPI服务器:

uvicorn myapp:app

It should start in a few seconds time. Let’s test our API for getting the best paths from Jurong East to Dhoby Ghaut. Head over to the following URL in your browser.

它应该在几秒钟的时间内开始。 让我们测试一下我们的API,以获得从Jurong EastDhoby Ghaut的最佳路径。 在浏览器中转到以下URL。

http://localhost:8000/get-journey?start=jurong%20east&end=dhoby%20ghaut&count=3

You should get the following result.

您应该得到以下结果。

[{"path":[{"name":"Jurong East","mrt":"x","time":"start here"},{"name":"Clementi","mrt":"ew","time":"4 minutes"},{"name":"Dover","mrt":"ew","time":"3 minutes"},{"name":"Bouna Vista","mrt":"ew","time":"3 minutes"},{"name":"Holland Village","mrt":"cc","time":"2 minutes"},{"name":"Farrer Road","mrt":"cc","time":"3 minutes"},{"name":"Botanic Gardens","mrt":"cc","time":"2 minutes"},{"name":"Stevens","mrt":"dt","time":"2 minutes"},{"name":"Newton","mrt":"dt","time":"3 minutes"},{"name":"Little India","mrt":"dt","time":"1 minutes"},{"name":"Dhoby Ghaut","mrt":"dt","time":"2 minutes"}],"weight":25.0},{"path":[{"name":"Jurong East","mrt":"x","time":"start here"},{"name":"Clementi","mrt":"ew","time":"4 minutes"},{"name":"Dover","mrt":"ew","time":"3 minutes"},{"name":"Bouna Vista","mrt":"ew","time":"3 minutes"},{"name":"Commonwealth","mrt":"ew","time":"2 minutes"},{"name":"Queenstown","mrt":"ew","time":"2 minutes"},{"name":"Red Hill","mrt":"ew","time":"3 minutes"},{"name":"Tiong Bahru","mrt":"ew","time":"2 minutes"},{"name":"Outram Park","mrt":"ew","time":"3 minutes"},{"name":"Chinatown","mrt":"ew","time":"2 minutes"},{"name":"Clarkequay","mrt":"ne","time":"2 minutes"},{"name":"Dhoby Ghaut","mrt":"ne","time":"2 minutes"}],"weight":28.0},{"path":[{"name":"Jurong East","mrt":"x","time":"start here"},{"name":"Clementi","mrt":"ew","time":"4 minutes"},{"name":"Dover","mrt":"ew","time":"3 minutes"},{"name":"Bouna Vista","mrt":"ew","time":"3 minutes"},{"name":"Holland Village","mrt":"cc","time":"2 minutes"},{"name":"Farrer Road","mrt":"cc","time":"3 minutes"},{"name":"Botanic Gardens","mrt":"cc","time":"2 minutes"},{"name":"Stevens","mrt":"dt","time":"2 minutes"},{"name":"Newton","mrt":"dt","time":"3 minutes"},{"name":"Orchard","mrt":"ns","time":"3 minutes"},{"name":"Somerset","mrt":"ns","time":"2 minutes"},{"name":"Dhoby Ghaut","mrt":"ns","time":"2 minutes"}],"weight":29.0}]

The first recommendation suggest us to go from Jurong East to Bouna Vista and take the Circle Line all the way to Botanic Gardens. After that, follow Downtown Line until you reach Little India and make an interchange to Dhoby Ghaut.

第一条建议建议我们从Jurong EastBouna Vista ,然后沿Circle Line一直到Botanic Gardens 。 之后,沿着Downtown Line直到到达Little India ,然后转乘至Dhoby Ghaut

The second path leads us from Jurong East to Outram Park. Then, make an interchange to North East Line and go all the way to Dhoby Ghaut.

第二条路线将我们从Jurong East引向Outram Park 。 然后,换乘至North East Line ,然后一直到Dhoby Ghaut

Furthermore, our third suggestion direct us to make interchange at Bouna Vista to reach Botanic Gardens. After that, continue to Newton via Downtown Line. Unlike the first journey, it recommends us to make interchange to North South Line go from Orchard to Dhoby Ghaut.

此外,我们的第三个建议指导我们在Bouna Vista进行互换,以到达Botanic Gardens 。 之后,通过Downtown Line继续前往Newton 。 与第一个旅程不同,它建议我们与OrchardDhoby Ghaut换乘North South Line

5.从前端呼叫 (5. Calling from Front-end)

All you need to do now is to connect it to any front-end application. You can use any frameworks or computer language based on your own preferences. For this tutorial, I am going to just provide a brief run-down on how to integrate React with FastAPI server. I will not cover the user interface to prevent it from being too lengthy.

您现在要做的就是将其连接到任何前端应用程序。 您可以根据自己的喜好使用任何框架或计算机语言。 在本教程中,我将简要介绍如何将React与FastAPI服务器集成。 我不会介绍用户界面,以防止它太长。

(Example)

If you are looking for inspiration, have a look at the functionalities provided by the following website. The following example shows the example output going from Jurong East to Dhoby Ghaut.

如果您正在寻找灵感,请查看以下网站提供的功能。 以下示例显示了从Jurong EastDhoby Ghaut的示例输出。

Image for post
SMRT Corporation website SMRT Corporation网站

As you can see, it only offers two options compared to our results. The first option is exactly the same path as the second recommendation made by our path-finding algorithms.

如您所见,与我们的结果相比,它仅提供两个选项。 第一个选项与我们的寻路算法提出的第二个建议完全相同。

You can create a React app with the following features:

您可以创建具有以下功能的React应用程序:

  • A Grid layout that separate the content into two parts. The left side contains the input while the right side will display the result.

    将内容分为两部分的网格布局。 左侧包含输入,而右侧将显示结果。
  • Two selection inputs for both starting point and end destination

    起点和终点都有两个选择输入
  • A confirmation button

    确认按钮
  • Results will be displayed as accordion together with the total time taken for the journey. Each journey will contain the name of the stations as well as the journey time in between each station.

    结果将显示为手风琴,以及整个旅程所花费的总时间。 每个旅程将包含站点的名称以及每个站点之间的旅程时间。

The final React app might look something like this. This user interface is based on React Material UI.

最终的React应用可能看起来像这样。 该用户界面基于React Material UI。

Image for post

AJAX电话 (AJAX Call)

For integration, you can easily implement it via AJAX or fetch call. In this tutorial, I am going to show how you can make an AJAX call to your FastAPI server.

对于集成,您可以通过AJAXfetch调用轻松实现。 在本教程中,我将向您展示如何对FastAPI服务器进行AJAX调用。

const url = "http://localhost:8000/get-journey?start=" + start + "&end=" + end + "&count=3";


let formData = null;
let xhr = new XMLHttpRequest();


xhr.addEventListener("readystatechange", function () {
  if (this.readyState === 4) {
    if(this.status === 200) {
      //call your function here, alternatively you can wrap it around a Promise and return the result
      let result = JSON.parse(this.responseText);
      console.log(result);
     }
  }
});


xhr.open("GET", url);
xhr.send(formData);

结论 (Conclusion)

Congratulations for completing this tutorial.

祝贺您完成本教程。

Let’s recap what you have learned today. We started off with a brief explanation on the Awesome Procedure on Cypher Library.

让我们回顾一下您今天学到的东西。 我们首先简要介绍了Cypher Library上的Awesome Procedure。

After that, we moved on with installing the necessary modules ranging from Neo4j Python driver to FastAPI. Besides, we crafted and modeled our domain for our journey planner application.

之后,我们继续安装必要的模块,从Neo4j Python驱动程序到FastAPI。 此外,我们为旅程计划者应用程序设计和建模了我们的领域。

Once we are done with it, we started Neo4j as console application and played around with the Cypher. We cleaned the database, created new records and attempted to run djikstra path-finding algorithm to get the best paths.

完成后,我们将Neo4j作为控制台应用程序启动,并使用Cypher。 我们清理了数据库,创建了新记录,并尝试运行djikstra路算法以获取最佳路径。

Subsequently, we built a new Journey class that serves as a module to connect to Neo4j database via the Python driver. We have created a FastAPI server as well which acts as the back-end server.

随后,我们构建了一个新的Journey类,作为通过Python驱动程序连接到Neo4j数据库的模块。 我们还创建了一个充当后端服务器的FastAPI服务器。

Lastly, we explored a little further on how to integrate it with React application via AJAX call.

最后,我们进一步探讨了如何通过AJAX调用将其与React应用程序集成。

Thanks for reading this piece. Hope to see you again in the next article!

感谢您阅读本文。 希望在下一篇文章中再见!

翻译自: https://towardsdatascience.com/build-a-subway-journey-planner-using-neo4j-566b1a53670a

  • 2
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值