GraphGists

领域

描述

我们喜欢跑步,并互相挑战。因此,我们设立了一个小型的比赛。选择了一些跑步组织。为每个组织分配一个主要比赛,而其他比赛则是副比赛。

对于我们小型比赛中的参赛者,主要比赛的第一名得 50 分,第二名得 45 分,第三名得 40 分,然后是 39 分、38 分……​ 依次递减。
所有参加副比赛的参赛者获得相同的积分。积分数与他们在主要比赛中排名最后一名之后的积分相同。

例如,参加主要比赛的 5 名参赛者将获得 50、45、40、39 和 38 分。然后,参加副比赛的每位参赛者将获得 37 分。如果主要比赛中没有参赛者,那么副比赛中的每个人都将获得 50 分。

有时,一些人会参加比赛,但不是比赛的一部分。这种情况适用于那些有兴趣明年加入俱乐部的人,或者那些会员费支付过期的。我们想记住他们的比赛成绩,以便在每周的俱乐部公报中展示,或者在他们成为俱乐部成员后更改他们的状态并重新计算积分。

相反,有时俱乐部成员选择参加比赛,但不参加比赛,因此放弃高分(50 - 45),让其他人获得这些积分。可以通过在比赛中不穿俱乐部制服来做到这一点。

最好有一个网站,可以在比赛结束后尽快显示比赛结果(与我们的比赛相关)和总体排名。计划使用 Graphgist 建立和验证模型,然后使用 Neo4J 和 Flask 建立网站。使用 Flask 和 Neo4j 构建 Web 应用程序培训对此进行了令人信服的介绍。

模型

每个组织都表示为一个节点,连接到位置和日期。然后,每个组织都有一个主要比赛(例如,10 英里)和副比赛(10 公里、5 公里、21 公里……​)。

该模型有一个比赛节点(“2015 年夏季比赛”、“2014 年夏季比赛”……​)。当俱乐部成员的年龄超过 12 岁时,他们会自动参加比赛。

人员由人员节点表示。在大多数情况下,人员节点将连接到“姓名”节点。对于我们可能不知道确切姓名的 newcomers,将使用人员节点的姓名属性。姓名节点必须正确,而人员节点的姓名属性是目前已知的最准确的表示。

对于参加我们比赛并在比赛中完赛的每位参赛者和每位同情者,都将创建一个完赛节点。最好将完赛节点设置为比赛中的位置和结束时间,但真正需要的信息只是相对于其他参赛者的相对位置。这足以计算比赛中获得的积分。这种方法允许在比赛结束后立即输入数据,并在享受啤酒并等待官方结果时讨论新的排名。

这个 graphgist 将专注于配置一些带有比赛的组织,将比赛添加到类别(主要比赛 - 副比赛),定义一些人并将他们添加为比赛中的完赛者。

此练习的主要目的是提取按到达顺序排列的完赛者列表。然后,Flask(Python)应用程序可以轻松计算每位参赛者的比赛积分,并得出总体排名。

Schema

添加一些数据

参赛者

首先,我们将定义一些组织、比赛和参赛者。一旦我们在数据库中添加了一些数据,就更容易了解正在发生的事情。

请注意,对于每个完赛者,将显示其在比赛中的位置。但是,这并非必需的信息,因此不会用于处理。

使用数据

获取完赛者

现在,让我们使用模式来获取特定组织的所有完赛者列表。使用模式可以提高查询的可读性。

MATCH races_in_Lier = (organization:Organization {name: 'Lier - Halve Marathon'})-[:has_race]->(race),
race_types = (race)-[:race_category]->(type:RaceType),
finishers = (race)<-[:runs_in]-(finisher)<-[:participates]-(person)
RETURN organization, race, type, finisher, person

现在,我们需要获取足够的信息来计算完赛者的积分。因此,我们需要首先按到达顺序获取主要比赛的完赛者。

第一次尝试提供了过多的信息

MATCH races_in_Lier = (organization:Organization {name: 'Lier - Halve Marathon'})-[:has_race]->(race),
main_race = (race)-[:race_category]->(g:RaceType {label: 'Main'}),
finishers = (finisher)<-[:after*]-()
WHERE (race)<-[:runs_in]-(finisher)
RETURN finishers

图是可以的,但是表为部分路径“36 after 7”和“72 after 36”提供了行。这不是必需的。我们只希望在 Web 应用程序中处理完整路径。因此,查询被扩展为包含所有路径的集合,获取路径的最大长度,然后仅根据最长路径筛选路径。在这个模型中只有一个最长路径,那就是所需的路径。

MATCH races_in_Lier = (organization:Organization {name: 'Lier - Halve Marathon'})-[:has_race]->(race),
main_race = (race)-[:race_category]->(g:RaceType {label: 'Main'}),
finishers = (finisher)<-[:after*]-()
WHERE (race)<-[:runs_in]-(finisher)
WITH COLLECT(finishers) AS results, MAX(length(finishers)) AS maxLength
WITH FILTER(result IN results WHERE length(result) = maxLength) AS result_coll
UNWIND result_coll AS result
RETURN nodes(result)

好的,这提供了一行数据,如预期的那样,因此 Web 应用程序将能够处理它。但是,neo4j 浏览器对(完赛者)节点上的笛卡尔积并不满意。需要重新配置查询以添加参与者的匹配行。这将链接比赛和完赛者

MATCH races_in_Lier = (organization:Organization {name: 'Lier - Halve Marathon'})-[:has_race]->(race),
main_race = (race)-[:race_category]->(g:RaceType {label: 'Main'}),
participants = (race)<-[:runs_in]-(finisher),
finishers = (finisher)<-[:after*]-()
WITH COLLECT(finishers) AS results, MAX(length(finishers)) AS maxLength
WITH FILTER(result IN results WHERE length(result) = maxLength) AS result_coll
UNWIND result_coll AS result
RETURN nodes(result)

添加下一场比赛的数据

现在,将为下一家组织的主要比赛(10 公里)添加参赛者。使用合并来创建或匹配每个参赛者。

MERGE (johan:Person {name: 'Johan'})
MERGE (luc:Person {name: 'Luc'})
MERGE (gert:Person {name: 'Gert'})
MERGE (stefan:Person {name: 'Stefan'})
WITH johan, luc, gert, stefan
MATCH (organization:Organization {name: 'Berchem - Bollekesloop'})-[:has_race]->(race),
(race)-[:race_category]->(g:RaceType {label: 'Main'})
CREATE (race)<-[:runs_in]-(f_johan:Finisher {place: 14})<-[:participates]-(johan),
(race)<-[:runs_in]-(f_luc:Finisher {place: 15})<-[:participates]-(luc),
(race)<-[:runs_in]-(f_gert:Finisher {place: 62})<-[:participates]-(gert),
(race)<-[:runs_in]-(f_stefan:Finisher {place: 114})<-[:participates]-(stefan),
(f_stefan)-[:after]->(f_gert)-[:after]->(f_luc)-[:after]->(f_johan)
RETURN johan,luc,gert,stefan,f_johan,f_luc,f_gert,f_stefan,race,organization

我想验证获取所有完赛者是否可以使用上面的查询。唯一需要更改的是组织的名称

MATCH races_in_Berchem = (organization:Organization {name: 'Berchem - Bollekesloop'})-[:has_race]->(race),
main_race = (race)-[:race_category]->(g:RaceType {label: 'Main'}),
participants = (race)<-[:runs_in]-(finisher),
finishers = (finisher)<-[:after*]-()
WITH COLLECT(finishers) AS results, MAX(length(finishers)) AS maxLength
WITH FILTER(result IN results WHERE length(result) = maxLength) AS result_coll
UNWIND result_coll AS result
RETURN nodes(result)

特殊情况

单个完赛者

需要验证一些特殊情况:只有一名参赛者的主要比赛和没有参赛者的主要比赛。下一个查询将为德伦的主要比赛添加一名参赛者

MERGE (luc:Person {name: 'Luc'})
WITH luc
MATCH (organization:Organization {name: 'Deurne - Rivierenhofloop'})-[:has_race]->(race),
(race)-[:race_category]->(racetype:RaceType {label: 'Main'})
CREATE (race)<-[:runs_in]-(f_luc:Finisher {place: 18})<-[:participates]-(luc)
RETURN luc, f_luc, race, racetype, organization

所以 Luc 应该在这场比赛中获得 50 分。但他会在查询中显示吗?

MATCH races_in_Berchem = (organization:Organization {name: 'Deurne - Rivierenhofloop'})-[:has_race]->(race),
main_race = (race)-[:race_category]->(g:RaceType {label: 'Main'}),
participants = (race)<-[:runs_in]-(finisher),
finishers = (finisher)<-[:after*]-()
WITH COLLECT(finishers) AS results, MAX(length(finishers)) AS maxLength
WITH FILTER(result IN results WHERE length(result) = maxLength) AS result_coll
UNWIND result_coll AS result
RETURN nodes(result)

在这个查询中没有。显然,关系 [:after*] 默认情况下至少需要一个链接。将关系修改为 [:after*0..] 更好

MATCH races_in_Berchem = (organization:Organization {name: 'Deurne - Rivierenhofloop'})-[:has_race]->(race),
main_race = (race)-[:race_category]->(g:RaceType {label: 'Main'}),
participants = (race)<-[:runs_in]-(finisher),
finishers = (finisher)<-[:after*0..]-()
WITH COLLECT(finishers) AS results, MAX(length(finishers)) AS maxLength
WITH FILTER(result IN results WHERE length(result) = maxLength) AS result_coll
UNWIND result_coll AS result
RETURN nodes(result)

好的,这是预期结果。

没有完赛者

如果没有人完赛,我希望有一个空的结果集。此检查是为了找出是否会显示任何意外错误。因此,将添加另一场比赛。请注意,neo4j 浏览器会警告关于断开模式之间的笛卡尔积,但在这种情况下,这是我们想要的。

MATCH (main: RaceType {label: 'Main'}),
	  (side: RaceType {label: 'Side'})
CREATE (organization:Organization {name: 'Mechelen - RAM'}),
	(mainrace:Race {label: '10k'}),
	(siderace:Race {label: '5k'}),
	(organization)-[:has_race]->(mainrace),
	(organization)-[:has_race]->(siderace),
	(mainrace)-[:race_category]->(main),
	(siderace)-[:race_category]->(side)
RETURN organization, mainrace, siderace, main, side

现在检查获取完赛者列表的查询是否返回一个空数据集

MATCH races_in_Mechelen = (organization:Organization {name: 'Mechelen - RAM'})-[:has_race]->(race),
main_race = (race)-[:race_category]->(g:RaceType {label: 'Main'}),
participants = (race)<-[:runs_in]-(finisher),
finishers = (finisher)<-[:after*0..]-()
WITH COLLECT(finishers) AS results, MAX(length(finishers)) AS maxLength
WITH FILTER(result IN results WHERE length(result) = maxLength) AS result_coll
UNWIND result_coll AS result
RETURN nodes(result)

好的,空数据集是预期结果。

这是练习结束时的数据库

结论

到达顺序是使用 "after" 关系确定的。在每场比赛结束后的立即,这是可用的信息。但是,这足以计算新的总排名。稍后,当更多信息(例如每个完赛者在比赛中的名次和时间)可用时,可以将其添加到数据库中。此数据模型是开始在 Flask 中构建 Web 应用程序的概念证明。

Web 应用程序计划进行一些扩展

  • 添加女性/男性类别。

  • 进一步扩展越野跑类别

    • 16 岁以下

    • 23 岁以下

    • 高级

    • 大师

    • 大师 +50

  • 添加其他田径项目…​

此 graphgist 文档也应该扩展到

  • 为完赛者添加姓名。这对 Web 应用程序不是必需的,但作为展示会很好。

  • 任何其他查询改进。

每次比赛的排名和总排名将存储在 SQL(sqlite)表中。目前尚不清楚是否可以使用图形数据库表示它。我也不能确定对于这种特定用途,图形数据库是否比 SQL 表更容易。