ArangoDB Graphs 系列——05 Pattern Matching

376 阅读6分钟

本文继续使用机场航班数据集,通过一步步构建复杂的模式匹配,来寻找两个机场间飞行总时间最短的航班方案。

为何不使用最短路径查询?

先回顾一下之前用过的最短路径查询:

# BIS机场到JFK机场的最短路径查询,返回3

RETURN LENGTH(
  FOR v IN OUTBOUND
  SHORTEST_PATH 'airports/BIS' TO 'airports/JFK' flights
  RETURN v
)

在ArangoDB中,最短路径查询无法使用过滤条件,上述查询返回结果3,减去1,可以当做另外一次查询的遍历深度。

依旧从BIS机场开始,使用遍历深度2,你可以加入额外的过滤条件来匹配搜索相同的终止节点JFK。

总结一下,通过使用带过滤条件的图遍历来找某些路径,这种过程被称之为模式匹配

补充: 变量中存储结果

使用LET关键字,像通常的编程语言一样,可以定义变量,例如:


# DepTime的格式类似1245,分别使用整除和取余操作,可以获得小时和分钟
LET h = FLOOR(f.DepTime / 100)
LET m = f.DepTime % 100
LET formatted = CONCAT(h, ':', m)
RETURN { hours: h, minutes: m, formatted }

# RETURN { myVariable: myVariable } 等价于 RETURN { myVariable }

下面的例子中,起飞时间的小时和分钟提前计算好,并保存在变量h和m中,随后要用于ISO timestamp (DATE_ISO8601() documentation),从而得到一个忽略掉时区和夏令时的时间戳

不过后续的计算中,我们会使用csv文件中自带的UTC时间戳


FOR f IN flights
  FILTER f._from == 'airports/BIS'
  LIMIT 100
  LET h = FLOOR(f.DepTime / 100)
  LET m = f.DepTime % 100
  RETURN {
    year: f.Year,
    month: f.Month,
    day: f.Day,
    time: f.DepTime,
    iso: DATE_ISO8601(f.Year, f.Month, f.Day, h, m)
  }

截屏2022-04-15 上午10.09.38.png 图:iso时间戳查询结果

寻找最优航班

要回答的真实业务问题是:

若考虑总飞行时间最少,则BIS机场到JFK机场最优的航班方案是?

第一步 从BIS到JFK的航班

我们只关注从BIS到JFK的航班,这一步还不考虑时间,日,月等信息。因为从最短路径查询中,我们已经知道了需要深度2可从BIS到到JFK,所以语句中IN后面的min max遍历深度设置为2:

FOR v, e, p IN 2 OUTBOUND 'airports/BIS' flights
    FILTER v._id == 'airports/JFK'
    LIMIT 5
    return p

结果会以图的形式展现,我们切换到JSON视图显示,可以看到5条不同的飞行计划(仔细看会发现,这5条里会包含一些不合理的飞行计划,比如 2008-01-01T14:06:00.000Z 到达丹佛DEN机场,然后从DEN 在2008-01-01T07:53:00.000Z飞往JFK,很明显赶不上这趟航班。这种飞行计划会出现在结果中,是因为这一步我们只是过滤出可以连接BIS和JFK两个顶点的所有边,并不考虑是否合理)

第二步 只要1月1日的航班

第一步的结果得到后,我们想限制两段飞行都在同一天,比如1月1日

# 使用 ALL == ,这是一个数组比较操作符
FOR v, e, p IN 2 OUTBOUND 'airports/BIS' flights
    FILTER v._id == 'airports/JFK'
    FILTER p.edges[*].Month ALL == 1
    FILTER p.edges[*].Day ALL == 1
    LIMIT 5
    return p
 

这次查询结果有两种可能,除了经过DEN, 还可以经过**MSP(Minneapolis)**机场,也是两跳。

截屏2022-04-15 上午10.52.34.png 图: 从BIS到JFK的两种飞行路线

第三步 计算飞行用时

使用DATE_DIFF() function 计算一趟航班的飞行用了多少分钟

FOR v, e, p IN 2 OUTBOUND 'airports/BIS' flights
    FILTER v._id == 'airports/JFK'
    FILTER p.edges[*].Month ALL == 1
    FILTER p.edges[*].Day ALL == 1
    LET filghtTime = DATE_DIFF(p.edges[0].DepTimeUTC, p.edges[1].ArrTimeUTC, 'i')
    SORT filghtTime ASC
    LIMIT 5
    return {flight: p, time: filghtTime}

DATE_DIFF()的第三个参数是返回的单位,i表示以分钟计算.

截屏2022-04-15 上午11.02.23.png 图: DATE_DIFF()的官方解释

观察返回结果,会发现有的用时是负数,这就是之前提到的不合理的飞行计划。

第四步 排除掉不合理计划

这一步,我们假设转机需要至少20min, 通过额外的过滤条件:即前一班飞机降落后,加上转机时间20min, 下一班飞机必须在此时间之后起飞才算合理飞行计划。

使用如下语句,找到最终答案:

FOR v, e, p IN 2 OUTBOUND 'airports/BIS' flights
    FILTER v._id == 'airports/JFK'
    FILTER p.edges[*].Month ALL == 1
    FILTER p.edges[*].Day ALL == 1
    FILTER DATE_ADD(p.edges[0].ArrTimeUTC, 20, 'minutes') < p.edges[1].DepTimeUTC
    LET filghtTime = DATE_DIFF(p.edges[0].DepTimeUTC, p.edges[1].ArrTimeUTC, 'i')
    SORT filghtTime ASC
    LIMIT 5
    return {flight: p, time: filghtTime}
   

查看最终结果,我们会发现,总用时最短的飞行方案是从MSP机场转机,而不是最短路径查询返回的从丹佛DEN转机(多个最短跳数中任意返回一条)

优化

在此数据集上运行以上查询,遍历器需要检查很多边,我们可以 使用以顶点为中心的索引(Vertex-Centric Indexes)进行优化,仅仅遍历相关日期的边,从而极大地改善查询性能。

  • 在ArangoDB Web界面的左侧菜单栏里选择COLLECTIONS
  • 打开flights collection
  • 点击 Indexes 页签 (如下图)
  • 点击绿色按钮,新增索引
  • 设置类型为 Hash Index,(亲自验证,3.9版本之后已经没有Hash Index,设置为 Persistent Index)
  • 在 Fields中输入 _from,Month,Day
  • 其他选项保持不变
  • 点击绿色创建按钮

截屏2022-04-22 上午9.29.37.png 图:点击绿色按钮,添加新索引

截屏2022-04-22 上午9.42.52.png

截屏2022-04-22 上午9.36.32.png 图: 官方说明,3.9及以后版本,Hash index就是persistent index的别名

此时,会花一点时间创建索引,随后可以看到新创建好的索引,因为刚才创建时没有起名字,所以系统会自动生成一个索引名称,这里的例子生成了名为 idx_1730262752809713664的索引,如下图:

截屏2022-04-22 上午9.46.02.png

此时回到查询编辑窗口,再次执行刚才的语句,发现执行时间缩短了,点击 Explain 查看执行计划,能看到新建的索引被使用了,如下图:

截屏2022-04-22 上午9.50.58.png

在不使用vertex-centric索引时,使用的是标准边索引,对起始地机场的所有发出边(outgoing edges)都要检查,检查是否满足我们的过滤条件(在某一天,到达指定目的地)。

新建的索引,根据_from, Month, Day属性,帮助遍历器快速查找想要的发出边,这样就不再是对所有的边进行检查,而是一个快速的查找过程,从而节约时间。

结论

本文中,我们构建了一个复杂的模式匹配查询来查找两个特定机场之间的最佳航班。 从这里开始,你可以进一步动手试验。 例如,尝试其他机场或条件,可能使用到数据集中包含的其他一些属性。

参考

[1]www.arangodb.com/learn/graph…