跳转至
开发版采用单机部署,无需产品 License 文件。需要多机部署或大规模性能测试? 了解企业版

匹配计算语句

匹配计算语句用于在临时图上执行迭代计算。它允许匹配特定的图模式,然后在匹配的子图上执行计算或更新操作。

Note

匹配计算不同于普通 MATCH 查询:普通MATCH用于检索数据并通常通过RETURN返回结果;匹配计算使用PER PATH或PER NODE对匹配到的路径或点执行计算,并且只能在临时图上运行。

前提条件

语法

match_compute_statement
MATCH graph_pattern per_block active_set_operation active_set_operation
per_block
PER PATH { procedure_body } PER NODE ( node_variable ) { procedure_body } PER NODE ( node_variable ) { procedure_body }
active_set_operation
FINALLY { SET active_set_variable = |= node_variable }

match_compute_statement ::=
    'MATCH' graph_pattern (( per_block active_set_operation? ) | active_set_operation) 

per_block ::=
    ( ('PER PATH' '{' procedure_body '}') ( 'PER NODE' '(' node_variable ')' '{' procedure_body '}' )* )
    | ('PER NODE' '(' node_variable ')' '{' procedure_body '}')+

active_set_operation ::=
    'FINALLY' '{' ('SET' active_set_variable ('=' | '|=') node_variable)+ '}'

参数

graph_pattern

匹配计算语句中的图模式。仅支持 0 跳(例如(a))和 1 跳(例如(a)-[e]->(b))的图模式。不支持match_mode和path_mode_prefix。

在graph_pattern中,可以指定WHERE子句将待遍历的点的范围限制为活动集变量中的点。

例如,(a)-[e]->(b) WHERE a IN active_set_variable AND a.id > 10匹配模式(a)-[e]->(b),其中a必须是活动集变量active_set_variable中的点且其id大于 10。

注意在每个图模式中,不管有多少个点变量,只允许一个活动集变量对一个点变量施加限制。例如,以下图模式均无效:

  • (a) WHERE a IN active_set_1 AND a IN active_set_2
  • (a)-[e]->(b) WHERE a IN active_set_1 AND b IN active_set_2
  • (a)-[e]->(b) WHERE a IN active_set_1 AND b IN active_set_1

per_block

per_block子句,用于为MATCH graph_pattern结果集中的每条路径或每个点执行一组语句。per_block子句有两种类型:PER PATH和PER NODE。

对于在match_compute_statement外部定义的非聚合值变量(比如原始值变量和活动集变量),不能在per_block内部执行赋值(=)操作更新它们的值。例如,以下代码无效:

'''
// `#match_compute_movie`为基于`movie`示例数据集创建的临时图
// 详情见下文示例。
USE #match_compute_movie { 
  VALUE x = 1 
  MATCH (a) 
  PER NODE (a) { 
    SET x = 2 // 无效 
  } 
}
'''

但是,对于全局聚合值变量,可以在per_block内部执行聚合(+=)操作,但不能执行赋值操作(=)为它们赋值。例如:

'''
USE #match_compute_movie { 
  VALUE global_sum SumAgg<INT> = 0 
  SET @global_sum = 1  // 有效 

  MATCH (a) 
  PER NODE (a) { 
    SET @global_sum += 1  // 有效 
    SET @global_sum = 1   // 无效 
  } 
}
'''

对于点绑定聚合值变量,可以在PER PATH子句中执行聚合操作,但不能执行赋值操作。然而,可以在PER NODE子句中为每个点执行聚合和赋值操作。例如:

'''
USE #match_compute_movie { 
  NODE VALUE node_sum SumAgg<INT> = 0 
  MATCH (a)-[e]->(b) 
  PER PATH { 
    SET a.@node_sum += 1  // 有效 
    SET a.@node_sum = 0   // 无效 
  } 
  PER NODE (a) { 
    SET a.@node_sum += 0   // 有效 
    SET a.@node_sum = 1   // 有效 
  } 
}
'''

根据上述描述,下表总结了不同类型的变量在per_block中的使用限制:

变量类型 在PER PATH中的限制 在PER NODE中的限制
在match_compute_statement外部定义的非聚合值变量 ❌ 禁止赋值操作 (=)
❌ 禁止聚合操作 (+=)
❌ 禁止赋值操作 (=)
❌ 禁止聚合操作 (+=)
全局聚合值变量 ✅ 允许聚合操作 (+=)
❌ 禁止赋值语句 (=)
✅ 允许聚合操作 (+=)
❌ 禁止赋值语句 (=)
点绑定聚合值变量 ✅ 允许聚合操作 (+=)
❌ 禁止赋值语句 (=)
✅ 允许聚合操作 (+=)
✅ 允许赋值操作 (=)

PER PATH

该子句为MATCH graph_pattern结果集中的每条路径执行过程体(procedure_body)内语句。

注意:PER PATH不能和 0 跳图模式一起使用。

PER NODE

该子句为MATCH graph_pattern结果集中每个由node_variable指定的点执行过程体(procedure_body)内语句。

此时,通常使用点绑定聚合值变量来存储和操作每个点特定的值。

procedure_body

要执行的一组语句。注意,在per_block的过程体中只允许以下操作和语句:

在PER NODE和PER PATH块内,不支持以下语句:

  • RETURN、LIMIT、ORDER BY、GROUP BY等查询级子句。
  • MATCH、子查询、局部TABLE定义等通用查询语句。

node_variable

引用点的变量值表达式。node_variable必须在graph_pattern中已定义。

active_set_operation

FINALLY子句,其使用node_variable指定的点更新active_set_variable中的点。该子句可动态更新一个或多个活动集以便进一步计算。

active_set_operation有两种更新模式:=和|=:

  • =模式将active_set_variable当前持有的点集替换为node_variable指定的点集。
  • |=模式将node_variable指定的点集添加到active_set_variable当前持有的点集中。

active_set_variable

存储一组点的活动集变量。更多信息,请参见活动集变量。

匹配计算语句中聚合值更新的可见性规则

全局聚合值

在PER PATH和PER NODE中的聚合操作(SET @global_agg += ...)只有在整个匹配计算语句执行完成后才可见。例如:

'''
USE #match_compute_movie { 
  VALUE global_sum SumAgg<INT> = 0 
  MATCH (a@User{id:1})-[e]->(b) 
  PER PATH { 
    SET @global_sum += 1 
    LOG_INFO("@global_sum == ",@global_sum) // 此处 @global_sum 仍为 0 
  } 
  LOG_INFO("@global_sum == ",@global_sum)   // 此处可见 <per path clause> 的更新 
}
'''

点绑定聚合值

  • PER PATH中的聚合操作(SET node.@agg += ...)在该路径子句完成后才可见。
  • PER NODE中的赋值(=)和聚合(+=)操作都立即可见。

常见误区:在同一 PER PATH 中聚合后立即 EXPORT

由于PER PATH的聚合要等子句完成后才生效,如果在同一个PER PATH内先SET node.@agg += ...再EXPORT node.@agg,导出的将是聚合前的旧值。

-- ❌ 错误:score 导出的是聚合前的旧值(始终为初始值)
PER PATH {
    SET v.@score += e.rate
    EXPORT v.id, v.@score INTO result   -- @score 此时仍为旧值
}

-- ✅ 正确:拆成两个 PER PATH,先聚合再导出
MATCH ... PER PATH { SET v.@score += e.rate }
MATCH ... PER PATH { EXPORT v.id, v.@score INTO result }

例如:

'''
USE #match_compute_movie { 
  NODE VALUE node_sum SumAgg<INT> = 0 
  MATCH (a@User{id:1})-[e]->(b) 
  PER PATH { 
    SET a.@node_sum += 1 
    LOG_INFO("a.@node_sum == ",a.@node_sum) // 此处 a.@node_sum 仍为 0 
  } 
  PER NODE (a) { 
    LOG_INFO("a.@node_sum == ",a.@node_sum)  // 此处可见`PER PATH`的更新 
    SET a.@node_sum += 1  // (1) 
    LOG_INFO("(1) a.@node_sum == ",a.@node_sum)  // 更新 (1) 已生效 
    SET a.@node_sum = 0   // (2) 
    LOG_INFO("(2) a.@node_sum == ",a.@node_sum) // 更新 (2) 已生效 
  } 
}
'''

示例

以下示例以 Jaccard 相似度计算演示匹配计算语句。先基于示例数据集中的movie图创建临时图:

CREATE TEMPORARY GRAPH IF NOT EXISTS #match_compute_movie AS COPY OF movie

然后执行:

'''
USE #match_compute_movie {
  /* ====== 声明 ====== */
  VALUE source_id = 1                          // 用于相似度计算的源用户 ID
  VALUE source_edges    SumAgg<INT64> = 0      // 源用户喜欢的电影
  VALUE topk_similarity TopKAgg<5, similarity DOUBLE DESC, id INT DESC>  // 前 5 位相似用户
  NODE VALUE target_edges SumAgg<INT64> = 0    // 每个用户喜欢的电影
  NODE VALUE intersection SumAgg<INT64> = 0    // 与源用户的共同喜欢的电影
  VALUE v_set ACTIVE_SET                       // 源用户喜欢的电影

  /* ====== 步骤 1:统计用户电影偏好 ====== */
  // 统计所有用户喜欢的电影
  MATCH (u@User)-[e:Watch]->(m:Movie)
  PER PATH {
    SET u.@target_edges += 1                  // 每个用户的电影计数递增
  }

  /* ====== 步骤 2:收集源用户数据 ====== */
  // 处理源用户的偏好
  MATCH (u@User)-[:Watch]->(m:Movie) 
  WHERE u.id = source_id  // 筛选源用户
  PER NODE (u) {
    SET @source_edges += u.@target_edges     // 存储源用户的电影计数
  }
  FINALLY { SET v_set = m }                      // 用源用户的电影填充活动集

  /* ====== 步骤 3:计算用户相似度 ====== */
  // 查找源用户与其他用户的共同喜欢电影
  MATCH (m:Movie)<-[:Watch]-(t@User) 
  WHERE t.id <> source_id AND m IN v_set                  // 排除源用户,使用源用户的电影作为起点
  PER PATH {
    SET t.@intersection += 1                 // 统计共同电影
  }
  PER NODE (t) {
    // 计算源用户和其他用户的共同喜欢电影的并集
    VALUE union_size = t.@target_edges + @source_edges - t.@intersection

    IF union_size <> 0 THEN {
      VALUE similarity = CAST(t.@intersection AS DOUBLE) / union_size

      // 存储到前 k 聚合器
      SET @topk_similarity += RECORD{
        similarity: similarity, 
        id: t.id
      }
    }
  }

  /* ====== 步骤 4:返回结果 ====== */
  // 输出前 5 位相似用户
  FOR r IN @topk_similarity
    RETURN r.id AS user_id, 
          ROUND(r.similarity, 5) AS jaccard_similarity
}
'''

返回:

+---------+--------------------+
| user_id | jaccard_similarity |
+---------+--------------------+
| 409     | 0.55556            |
| 70      | 0.55556            |
| 282     | 0.54545            |
| 122     | 0.54545            |
| 39      | 0.54545            |
+---------+--------------------+