图模型重构

简介

本指南以 Cypher® 基础概念为基础,提供了一个更改图模型的工作示例。完成本指南后,您应该能够根据不断变化的需求来演化您的图模型。

机场数据集

本指南使用一个机场数据集,其中包含 2008 年 1 月美国机场之间的连接。数据以 CSV 文件形式呈现。下面您可以看到数据库的图模型

在导入任何数据之前,您应该在 Airport 标签和 code 属性上创建唯一约束,以确保您不会意外导入重复的机场。以下查询创建约束

CREATE CONSTRAINT airport_id
FOR (airport:Airport) REQUIRE airport.code IS UNIQUE
表 1. 结果

86 毫秒后可用 0 行,在另外 0 毫秒后消耗。添加了 1 个约束

以下查询使用 LOAD CSV 工具从 CSV 文件加载数据

LOAD CSV WITH HEADERS FROM "https://raw.githubusercontent.com/neo4j-contrib/training/master/modeling/data/flights_1k.csv" AS row
MERGE (origin:Airport {code: row.Origin})
MERGE (destination:Airport {code: row.Dest})
MERGE (origin)-[connection:CONNECTED_TO {
  airline: row.UniqueCarrier,
  flightNumber: row.FlightNum,
  date: date({year: toInteger(row.Year), month: toInteger(row.Month), day: toInteger(row.DayofMonth)}),
  cancelled: row.Cancelled,
  diverted: row.Diverted}]->(destination)
ON CREATE SET connection.departure = localtime(apoc.text.lpad(row.CRSDepTime, 4, "0")),
              connection.arrival = localtime(apoc.text.lpad(row.CRSArrTime, 4, "0"))

此查询

  • 创建一个带有 Airport 标签的节点,该节点具有一个 code 属性,该属性的值来自 CSV 文件中的 Origin 列。

  • 创建一个带有 Airport 标签的节点,该节点具有一个 code 属性,该属性的值来自 CSV 文件中的 Dest 列。

  • 创建一个类型为 CONNECTED_TO 的关系,该关系根据 CSV 文件中的列具有多个属性。

如果运行此查询,您将看到以下输出

表 2. 结果

添加了 62 个标签,创建了 62 个节点,设置了 7062 个属性,创建了 1000 个关系,在 376 毫秒后完成。

这是一个起始模型,但您可以进行一些改进。

将属性转换为布尔值

CONNECTED_TO 关系上的 divertedcancelled 属性包含 10 的字符串值。由于这些值代表布尔值,因此您可以使用 apoc.refactor.normalizeAsBoolean 过程将值从字符串转换为布尔值。

以下查询对 diverted 属性执行转换

MATCH (:Airport)-[connectedTo:CONNECTED_TO]->(:Airport)
CALL apoc.refactor.normalizeAsBoolean(connectedTo, "diverted", ["1"], ["0"])
RETURN count(*)
表 3. 结果
count(*)

1000

以下查询对 cancelled 属性执行转换

MATCH (origin:Airport)-[connectedTo:CONNECTED_TO]->(departure)
CALL apoc.refactor.normalizeAsBoolean(connectedTo, "cancelled", ["1"], ["0"])
RETURN count(*)
表 4. 结果
count(*)

1000

如果要更新大量关系,在尝试在一个事务中重构所有关系时,可能会收到 OutOfMemory 异常。因此,您可以使用 apoc.periodic.iterate 过程按批处理关系。以下查询对 cancelledreverted 属性在同一查询中执行此操作

UNWIND ["cancelled", "reverted"] AS propertyToDelete
CALL apoc.periodic.iterate(
  "MATCH (:Airport)-[connectedTo:CONNECTED_TO]->(:Airport) RETURN connectedTo",
  "CALL apoc.refactor.normalizeAsBoolean(connectedTo, $propertyToDelete, ['1'], ['0'])
   RETURN count(*)",
  {params: {propertyToDelete: propertyToDelete}, batchSize: 100})
YIELD batches
RETURN propertyToDelete, batches

有关 UNWIND 子句的更多详细信息,请参阅 Cypher 手册 → UNWIND 页面

先前查询中的 apoc.periodic.iterate 过程接受三个参数

  • 一个外部 Cypher 查询,它找到并返回要处理的 CONNECTED_TO 关系流。

  • 一个内部 Cypher 查询,它处理这些 CONNECTED_TO 关系,通过使用 apoc.refactor.normalizeAsBoolean 过程将指定属性上的所有值转换为布尔值,该过程本身接受多个参数

    • 属性存在的实体

    • 要规范化的属性的名称

    • 应该被认为是 true 的值列表

    • 应该被认为是 false 的值列表

  • 过程的配置值,包括

    • params - 传递给这些 Cypher 查询的参数。

    • batchSize - 控制单个事务中运行的内部语句数。

运行查询后,您将看到以下输出

表 5. 结果
propertyToDelete batches

"cancelled"

10

"reverted"

10

完成此操作后,您可以编写以下查询以返回所有取消的连接

MATCH (origin:Airport)-[connectedTo:CONNECTED_TO]->(destination)
WHERE connectedTo.cancelled
RETURN origin.code AS origin,
       destination.code AS destination,
       connectedTo.date AS date,
       connectedTo.departure AS departure,
       connectedTo.arrival AS arrival
表 6. 结果
origin destination date departure arrival

"LAS"

"OAK"

2008-01-03

07:00

08:30

"LAX"

"SFO"

2008-01-03

09:05

10:25

"LAX"

"OAK"

2008-01-03

11:00

12:15

"LAX"

"SJC"

2008-01-03

19:30

20:35

"LAX"

"SFO"

2008-01-03

16:20

17:40

"MDW"

"STL"

2008-01-03

11:10

12:15

"MDW"

"BDL"

2008-01-03

08:45

11:40

"MDW"

"DTW"

2008-01-03

06:00

08:05

"MDW"

"STL"

2008-01-03

14:45

15:50

"MDW"

"BNA"

2008-01-03

19:25

20:45

"OAK"

"BUR"

2008-01-03

13:10

14:15

"OAK"

"BUR"

2008-01-03

17:05

18:10

从关系创建节点

使用现有数据模型,编写一个查询来查找特定航班可能是一项复杂的任务。这是因为这里的航班以关系的形式表示。但是,您可以通过从 CONNECTED_TO 关系中存储的属性创建 Flight 节点来更改模型

以下查询执行此重构

CALL apoc.periodic.iterate(
  "MATCH (origin:Airport)-[connected:CONNECTED_TO]->(destination:Airport) RETURN origin, connected, destination",
  "CREATE (flight:Flight {
     date: connected.date,
     airline: connected.airline,
     number: connected.flightNumber,
     departure: connected.departure,
     arrival: connected.arrival,
     cancelled: connected.cancelled,
     diverted: connected.diverted
   })
   MERGE (origin)<-[:ORIGIN]-(flight)
   MERGE (flight)-[:DESTINATION]->(destination)
   DELETE connected",
  {batchSize: 100})

此查询使用 apoc.periodic.iterate 过程,以便您可以按批处理而不是在一个事务中执行重构。该过程接受三个参数

  • 一个外部 Cypher 查询,它找到并返回要处理的 CONNECTED_TO 关系流以及需要处理的始发机场和目的机场。

  • 一个内部 Cypher 查询,它处理这些实体,创建一个带有 Flight 标签的节点,并从该节点到始发机场和目的机场创建关系。

  • batchSize 配置,它将单个事务中运行的内部语句数设置为 100

如果执行查询,您将看到以下输出

表 7. 结果
batches total timeTaken committedOperations failedOperations failedBatches retries errorMessages batch operations wasTerminated

10

1000

0

1000

0

0

0

{}

{total: 10, committed: 10, failed: 0, errors: {}}

{total: 1000, committed: 1000, failed: 0, errors: {}}

FALSE

您也可以使用 apoc.refactor.extractNode 过程执行此重构。

CALL apoc.periodic.iterate(
  "MATCH (origin:Airport)-[connected:CONNECTED_TO]->(destination:Airport)
   RETURN origin, connected, destination",
  "CALL apoc.refactor.extractNode([connected], ['Flight'], 'DESTINATION', 'ORIGIN')
   YIELD input, output, error
   RETURN input, output, error",
  {batchSize: 100});

这与前面的查询相同,但外部 Cypher 查询使用 apoc.refactor.extractNode 过程创建 Flight 节点,并创建到始发机场和目的机场的关系。如果运行此查询,我们将看到以下输出

表 8. 结果
batches total timeTaken committedOperations failedOperations failedBatches retries errorMessages batch operations wasTerminated

10

1000

0

1000

0

0

0

{}

{total: 10, committed: 10, failed: 0, errors: {}}

{total: 1000, committed: 1000, failed: 0, errors: {}}

FALSE

从属性创建节点

目前,航空公司名称存储在 Flight 节点上的 airline 属性中。这意味着,如果您想返回所有航空公司的流,您必须扫描每个航班,并检查每个航班的 airline 属性。

您可以通过为每个航空公司创建一个带有 Airline 标签的节点来使此任务更简单、更高效

首先,在 Airline 标签和 name 属性上创建一个约束,以避免重复的航空公司节点

CREATE CONSTRAINT airline_id
FOR (airline:Airline) REQUIRE airline.name IS UNIQUE
表 9. 结果

107 毫秒后可用 0 行,在另外 0 毫秒后消耗。添加了 1 个约束

现在,您可以运行以下查询来执行重构

CALL apoc.periodic.iterate(
   'MATCH (flight:Flight) RETURN flight',
   'MERGE (airline:Airline {name:flight.airline})
    MERGE (flight)-[:AIRLINE]->(airline)
    REMOVE flight.airline',
   {batchSize:10000, iterateList:true, parallel:false}
)

您再次使用 apoc.periodic.iterate 过程,并使用以下参数

  • 一个外部 Cypher 语句,它返回要处理的 Flight 节点流。

  • 这是一个内部 Cypher 语句,用于处理Flight节点并根据airline属性创建Airline节点。它还从Flight节点到Airline节点创建了一个AIRLINE关系。之后,您可以从Flight节点中删除airline属性。

如果您运行此查询,输出将如下所示:

表 10. 结果
batches total timeTaken committedOperations failedOperations failedBatches retries errorMessages batch operations wasTerminated

1

1000

0

1000

0

0

0

{}

{total: 1, committed: 1, failed: 0, errors: {}}

{total: 1000, committed: 1000, failed: 0, errors: {}}

FALSE

然后,您可以编写以下查询来查找航空公司和涉及每家航空公司的航班数量

MATCH (airline:Airline)<-[:AIRLINE]-(:Flight)
RETURN airline.name AS airline, count(*) AS numberOfFlights

此查询与之前查询的功能相同,但外部 Cypher 查询使用apoc.refactor.extractNode过程创建Flight节点并创建到始发机场和目的机场的关系。如果您运行此查询,您将获得以下输出:

表 11. 结果
航空公司 航班数

"WN"

1000

资源

本指南展示了如何借助 APOC 库中的过程来重构图模型。以下是一些关于在 Neo4j 中重构的更多学习资源: