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 configurationAPOC Core
-不具有外部依赖性或需要配置的过程和功能APOC Full
—includes everything inAPOC 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.0Java
—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).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.
我将使用以下网络地图作为用例的数据集。 它基于新加坡公共交通运营商之一的实际网络地图。

The network map consists of 6 MRT lines and 3 LRT lines as shown in the figure below:
网络图由6条MRT线路和3条LRT线路组成,如下图所示:

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
toStation B
will be always be the same regardless if you are traveling on different lines. In actual use case, the time taken to travel fromRaffles Place
toCity Hall
viaEast-West Line
is different from the time taken viaNorth-South Line
.无论您乘坐的是不同的线路,从
Station A
到Station B
将始终相同。 在实际使用案例中,通过East-West Line
从Raffles Place
到City 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 beew
whileCircle Line
will becc
. For interchanges, it will be marked asx
instead. This property will determine the color of the icon later on in React app.mrt
—代表桩号所在的线路。 我改用简称。 因此,East-West Line
将为ew
而Circle 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 A
到Station B
存在两种不同的关系,它将导致结果重复。 例如,您可以通过East-West Line
和North-South Line
从Raffles 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查询后,您应该会看到以下用户界面

得到所有 (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浏览器中获得以下结果。

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
附带了一些有用的寻路算法,例如dijkstra
和astar
。 在本教程中,我将使用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.
如果仅从算法中寻找最佳路径,则可以忽略defaultWeight
和numberOfWantedPaths
参数。
从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 East
到Dhoby 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
您应该获得以下输出

从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浏览器将显示以下结果

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 journeystart_node
—旅程的起点end_node
— End destination of the journeyend_node
—旅程的终点count
— Number of paths to be returnedcount
—要返回的路径数
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.
当您从任何前端应用程序进行AJAX
或fetch
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()
函数链接起来。 每行包含一个字典对象,其中包含path
和weight
参数。 需要注意的一件事是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 East
到Dhoby 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 East
到Bouna 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
。 与第一个旅程不同,它建议我们与Orchard
到Dhoby 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 East
到Dhoby Ghaut
的示例输出。

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。

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.
对于集成,您可以通过AJAX
或fetch
调用轻松实现。 在本教程中,我将向您展示如何对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