neo4j 3.5 reduce的使用
在 neo4j 中,reduce
函数可以用来对一个集合中的元素进行迭代处理,最终返回一个值。官方文档地址:https://neo4j.com/docs/cypher-manual/3.5/functions/list/
语法
reduce(acc = init, x IN list | expression)
项目中的使用1
我需要匹配某个人物关联的所有文章,并把所有文章的标题作为该人物的content,使用分号分隔。具体的 cypher 语句如下:
MATCH (p:Person)
OPTIONAL MATCH (p)-[:content]->(a:Article)
WITH p, collect(DISTINCT a.title) as titles
SET p.content = reduce(acc = "", x IN titles | acc + x + ";")
项目中的使用2
有一个比较复杂的需求,在 neo4j 中存储了实体(Entity)、属性值(Value)等节点,通过 Entity 与 Value 之间的 HAS 关系表示实体拥有这个属性。现在要求根据实体的名称或者属性值的名称查询实体,允许同时查询实体名称以及多个属性值的名称,允许分页查询,并且允许对某个属性值进行排序。先给出最终的 cypher 语句:
@Query("MATCH (t:BaseNode:Task {nodeId: {taskId}})-[:INCLUDE]->(d:Document)<-[:LOCATION]-(e:Entity)-[:TYPE]->(:BaseNode:Label {nodeId: {labelId}}) WHERE e.name CONTAINS {entityName}\n" +
"AND (SIZE({attributeList}) = 0 OR ALL(attr IN {attributeList} WHERE ANY(a1 IN [(e)-[:HAS]->(v:Value {attributeId: attr.attributeId}) | v.value] WHERE toString(a1) CONTAINS attr.searchContent)))\n" +
"WITH e\n" +
"OPTIONAL MATCH (e)-[r:HAS]->(v:Value)\n" +
"WITH e, COLLECT({key: r.name, value: v.value}) AS values\n" +
"WITH e, apoc.coll.sortMaps(values, 'key') AS sortedValues\n" +
"WITH e, REDUCE(acc = [], v IN sortedValues | acc + CASE\n" +
" WHEN v.key = acc[SIZE(acc) - 1][0] THEN [[v.key, acc[SIZE(acc) - 1][1] + ' ' + v.value]]\n" +
" ELSE [[v.key, v.value]] END) AS values\n" +
"WITH e, apoc.map.fromPairs(values) AS mergedProperties\n" +
"WITH apoc.map.merge(e, mergedProperties) AS entity\n" +
"WITH entity,\n" +
"CASE WHEN {orderMethod} = 'asc' THEN apoc.map.get(entity, {orderBy}, '') ELSE '' END AS ascParam,\n" +
"CASE WHEN {orderMethod} = 'desc' THEN apoc.map.get(entity, {orderBy}, '') ELSE '' END AS descParam\n" +
"RETURN entity, ascParam, descParam\n" +
"ORDER BY ascParam ASC, descParam DESC\n" +
"SKIP {start} LIMIT {length}")
List<Map<String, Object>> searchEntityByAttribute(Long taskId, Long labelId, String entityName, Integer start, Integer length, String orderBy, String orderMethod, List<AttributeSearchVO> attributeList);
下面是对代码的部分说明以及难点分析:
-
其他说明:labelId, taskId等参数是该项目的具体需求,用于确定查询的范围以及实体的类别等,这里不过多解释。
-
因为需要对若干属性值进行查询,所以传入了一个
List<AttributeSearchVO> attributeList
来表示需要查询的属性,其中AttributeSearchVO
是一个自定义的类,存储了属性id、名称等信息。根据属性进行查询优两种情况,如果不想对属性查询,则传入空列表,在 cypher 中使用SIZE({attributeList}) = 0
来判断;如果需要对属性进行查询,则使用ALL(attr IN {attributeList} WHERE ANY(a1 IN [(e)-[:HAS]->(v:Value {attributeId: attr.attributeId}) | v.value] WHERE toString(a1) CONTAINS attr.searchContent))
来判断,这是一个比较复杂的查询条件,先用了[(e)-[:HAS]->(v:Value {attributeId: attr.attributeId}) | v.value]来获取实体拥有的所有属性值,然后用ANY
来判断是否有属性值的值包含了搜索内容,并使用ALL
来判断是否所有属性值都满足条件。 -
虽然在数据库中实体与属性值是分别在不同节点存储并使用 HAS 关系连接的,但是在查询返回给前端时需要将实体与属性值合并为一个对象。
先将属性值合并成一个 map 类型。但是并没有什么方法能够一步到位。参考了 apoc 的官网后,打算使用apoc.map.fromPairs
来将属性值合并成一个 map 类型。但是在使用时发现,apoc.map.fromPairs
只能将一个二维数组转换为 map 类型,那么这就需要先将属性值转换为二维数组。由于数据的特殊性,一个实体即使是属性名相同的属性,也可能会有多个。比如一个实体可能会有两个”释义”属性,那么这两个属性值就需要合并。
正因为可能有多个同名属性的原因,所以需要先对属性的 key 进行排序,让相邻的 key 放在一起,这样就方便在后面的REDUCE
函数中判断是否需要合并属性值。这里就分成了几步:先使用COLLECT
将属性值收集到一个数组中,然后使用apoc.coll.sortMaps
对属性值进行排序,最后使用REDUCE
函数对排序后的属性值进行合并。在REDUCE
函数中,通过acc[SIZE(acc) - 1]
来获取上一个元素,0和1分别表示 key 和 value,如果当前 key 与上一个 key 相同,则将当前 value 通过空格拼接到上一个 value 后面,否则直接添加到数组中。
最后使用apoc.map.fromPairs
将合并后的属性值转换为 map 类型。这一步完成后,再使用apoc.map.merge
将实体与属性值合并。 -
最后根据排序方式对实体进行排序。但是在 spring-data-neo4j 中,并不能直接以参数的方式传入排序方式,所以这里采用了一个比较巧妙的方式,预先把升序和降序排序语句都放在了 cypher 语句中,
ORDER BY ascParam ASC, descParam DESC
,然后只需要使用CASE WHEN
来设置升序和降序的参数即可,如果不需要排序,则返回空字符串。