Neo4j 高效添加多个节点和边
Neo4j Adding Multiple Nodes and Edges Efficiently
我有下面的例子。
我想知道在单个事务中添加节点和边列表的最佳和最快方法是什么?我使用标准的 C# Neo4j .NET 包,但打开 Neo4jClient,因为我读到它更快。老实说,任何支持 .NET 和 4.5 的东西。
我有大约 60000 个 FooA 对象的列表需要添加到 Neo4j 中,这可能需要几个小时!
首先,FooB 对象几乎没有变化,所以我不必每天都添加它们。性能问题是每天两次添加新的 FooA 对象。
每个 FooA 对象都有一个 FooB 对象列表,其中有两个列表包含我需要添加的关系; RelA 和 RelB(见下文)。
public class FooA
{
public long Id {get;set;} //UniqueConstraint
public string Name {get;set;}
public long Age {get;set;}
public List<RelA> ListA {get;set;}
public List<RelB> ListB {get;set;}
}
public class FooB
{
public long Id {get;set;} //UniqueConstraint
public string Prop {get;set;}
}
public class RelA
{
public string Val1 {get;set;}
pulic NodeTypeA Node {get;set;
}
public class RelB
{
public FooB Start {get;set;}
public FooB End {get;set;}
public string ValExample {get;set;}
}
目前,我通过 ID 匹配来检查节点 'A' 是否存在。如果是这样,那么我将完全跳过并转到下一个项目。如果没有,我将创建具有自己属性的节点 'A'。然后我用自己独特的属性创建边缘。
每件商品的交易量相当多。按 Id 匹配节点 -> 添加节点 -> 添加边。
foreach(var ntA in FooAList)
{
//First transaction.
MATCH (FooA {Id: ntA.Id)})
if not exists
{
//2nd transaction
CREATE (n:FooA {Id: 1234, Name: "Example", Age: toInteger(24)})
//Multiple transactions.
foreach (var a in ListA)
{
MATCH (n:FooA {Id: ntA.Id}), (n2:FooB {Id: a.Id }) with n,n2 LIMIT 1
CREATE (n)-[:RelA {Prop: a.Val1}]-(n2)
}
foreach (var b in Listb)
{
MATCH (n:FooB {Id: b.Start.Id}), (n2:FooB {Id: b.End.Id }) with n,n2 LIMIT 1
CREATE (n)-[:RelA {Prop: b.ValExample}]-(n2)
}
}
如何使用 Neo4jClient 和 UNWIND 或除 CSV 导入之外的任何其他方式添加 FooA 列表。
希望这是有道理的,谢谢!
最大的问题是嵌套列表,这意味着您必须执行 foreach
循环,因此您最终执行了 最少 4 个查询 per FooA
,60,000 - 好吧 - 很多!
快速说明回复:索引
首先 - 您需要在 FooA
和 FooB
节点的 Id
属性 上建立索引,这将大大加快您的查询速度。
我玩过这个,在我老旧的计算机上,它存储了 60,000 个 FooA 条目,并在大约 12-15 秒内创建了 96,000 个 RelB 实例。
解决方案
我将它分成两部分 - FooA 和 RelB:
FooA
我不得不 规范化 FooA
class 到我可以在 Neo4jClient
中使用的东西 - 所以让我们介绍一下:
public class CypherableFooA
{
public CypherableFooA(FooA fooA){
Id = fooA.Id;
Name = fooA.Name;
Age = fooA.Age;
}
public long Id { get; set; }
public string Name { get; set; }
public long Age { get; set; }
public string RelA_Val1 {get;set;}
public long RelA_FooBId {get;set;}
}
我添加了 RelA_Val1
和 RelA_FooBId
属性以便能够在 UNWIND
中访问它们。我使用辅助方法转换您的 FooA
:
public static IList<CypherableFooA> ConvertToCypherable(FooA fooA){
var output = new List<CypherableFooA>();
foreach (var element in fooA.ListA)
{
var cfa = new CypherableFooA(fooA);
cfa.RelA_FooBId = element.Node.Id;
cfa.RelA_Val1 = element.Val1;
output.Add(cfa);
}
return output;
}
结合:
var cypherable = fooAList.SelectMany(a => ConvertToCypherable(a)).ToList();
展平 FooA
个实例,所以我最终得到 1 CypherableFooA
for each ListA
属性 中的项目一个 FooA
。例如如果您在每个 FooA
上的 ListA
中有 2 个项目并且您有 5,000 个 FooA
实例 - 您最终会得到包含 10,000 个项目的 cypherable
。
现在,使用 cypherable
我调用我的 AddFooAs
方法:
public static void AddFooAs(IGraphClient gc, IList<CypherableFooA> fooAs, int batchSize = 10000, int startPoint = 0)
{
var batch = fooAs.Skip(startPoint).Take(batchSize).ToList();
Console.WriteLine($"FOOA--> {startPoint} to {batchSize + startPoint} (of {fooAs.Count}) = {batch.Count}");
if (batch.Count == 0)
return;
gc.Cypher
.Unwind(batch, "faItem")
.Merge("(fa:FooA {Id: faItem.Id})")
.OnCreate().Set("fa = faItem")
.Merge("(fb:FooB {Id: faItem.RelA_FooBId})")
.Create("(fa)-[:RelA {Prop: faItem.RelA_Val1}]->(fb)")
.ExecuteWithoutResults();
AddFooAs(gc, fooAs, batchSize, startPoint + batch.Count);
}
这会将查询分成 10,000 个批次(默认情况下)- 这对我来说大约需要 5-6 秒 - 与我一次尝试所有 60,000 个差不多。
RelB
您在示例中使用 FooA
存储 RelB
,但您正在编写的查询根本不使用 FooA
,所以我所做的是提取并展平 ListB
属性:
中的所有 RelB
个实例
var relBs = fooAList.SelectMany(a => a.ListB.Select(lb => lb));
然后我像这样将它们添加到 Neo4j 中:
public static void AddRelBs(IGraphClient gc, IList<RelB> relbs, int batchSize = 10000, int startPoint = 0)
{
var batch = relbs.Select(r => new { StartId = r.Start.Id, EndId = r.End.Id, r.ValExample }).Skip(startPoint).Take(batchSize).ToList();
Console.WriteLine($"RELB--> {startPoint} to {batchSize + startPoint} (of {relbs.Count}) = {batch.Count}");
if(batch.Count == 0)
return;
var query = gc.Cypher
.Unwind(batch, "rbItem")
.Match("(fb1:FooB {Id: rbItem.StartId}),(fb2:FooB {Id: rbItem.EndId})")
.Create("(fb1)-[:RelA {Prop: rbItem.ValExample}]->(fb2)");
query.ExecuteWithoutResults();
AddRelBs(gc, relbs, batchSize, startPoint + batch.Count);
}
同样,批处理默认为 10,000。
显然,时间会根据 ListB
和 ListA
中的 rels 数量而有所不同 - 我的测试在 ListA
中有一个项目,在 ListB
中有 2 个项目。
我有下面的例子。
我想知道在单个事务中添加节点和边列表的最佳和最快方法是什么?我使用标准的 C# Neo4j .NET 包,但打开 Neo4jClient,因为我读到它更快。老实说,任何支持 .NET 和 4.5 的东西。
我有大约 60000 个 FooA 对象的列表需要添加到 Neo4j 中,这可能需要几个小时!
首先,FooB 对象几乎没有变化,所以我不必每天都添加它们。性能问题是每天两次添加新的 FooA 对象。
每个 FooA 对象都有一个 FooB 对象列表,其中有两个列表包含我需要添加的关系; RelA 和 RelB(见下文)。
public class FooA
{
public long Id {get;set;} //UniqueConstraint
public string Name {get;set;}
public long Age {get;set;}
public List<RelA> ListA {get;set;}
public List<RelB> ListB {get;set;}
}
public class FooB
{
public long Id {get;set;} //UniqueConstraint
public string Prop {get;set;}
}
public class RelA
{
public string Val1 {get;set;}
pulic NodeTypeA Node {get;set;
}
public class RelB
{
public FooB Start {get;set;}
public FooB End {get;set;}
public string ValExample {get;set;}
}
目前,我通过 ID 匹配来检查节点 'A' 是否存在。如果是这样,那么我将完全跳过并转到下一个项目。如果没有,我将创建具有自己属性的节点 'A'。然后我用自己独特的属性创建边缘。
每件商品的交易量相当多。按 Id 匹配节点 -> 添加节点 -> 添加边。
foreach(var ntA in FooAList)
{
//First transaction.
MATCH (FooA {Id: ntA.Id)})
if not exists
{
//2nd transaction
CREATE (n:FooA {Id: 1234, Name: "Example", Age: toInteger(24)})
//Multiple transactions.
foreach (var a in ListA)
{
MATCH (n:FooA {Id: ntA.Id}), (n2:FooB {Id: a.Id }) with n,n2 LIMIT 1
CREATE (n)-[:RelA {Prop: a.Val1}]-(n2)
}
foreach (var b in Listb)
{
MATCH (n:FooB {Id: b.Start.Id}), (n2:FooB {Id: b.End.Id }) with n,n2 LIMIT 1
CREATE (n)-[:RelA {Prop: b.ValExample}]-(n2)
}
}
如何使用 Neo4jClient 和 UNWIND 或除 CSV 导入之外的任何其他方式添加 FooA 列表。
希望这是有道理的,谢谢!
最大的问题是嵌套列表,这意味着您必须执行 foreach
循环,因此您最终执行了 最少 4 个查询 per FooA
,60,000 - 好吧 - 很多!
快速说明回复:索引
首先 - 您需要在 FooA
和 FooB
节点的 Id
属性 上建立索引,这将大大加快您的查询速度。
我玩过这个,在我老旧的计算机上,它存储了 60,000 个 FooA 条目,并在大约 12-15 秒内创建了 96,000 个 RelB 实例。
解决方案
我将它分成两部分 - FooA 和 RelB:
FooA
我不得不 规范化 FooA
class 到我可以在 Neo4jClient
中使用的东西 - 所以让我们介绍一下:
public class CypherableFooA
{
public CypherableFooA(FooA fooA){
Id = fooA.Id;
Name = fooA.Name;
Age = fooA.Age;
}
public long Id { get; set; }
public string Name { get; set; }
public long Age { get; set; }
public string RelA_Val1 {get;set;}
public long RelA_FooBId {get;set;}
}
我添加了 RelA_Val1
和 RelA_FooBId
属性以便能够在 UNWIND
中访问它们。我使用辅助方法转换您的 FooA
:
public static IList<CypherableFooA> ConvertToCypherable(FooA fooA){
var output = new List<CypherableFooA>();
foreach (var element in fooA.ListA)
{
var cfa = new CypherableFooA(fooA);
cfa.RelA_FooBId = element.Node.Id;
cfa.RelA_Val1 = element.Val1;
output.Add(cfa);
}
return output;
}
结合:
var cypherable = fooAList.SelectMany(a => ConvertToCypherable(a)).ToList();
展平 FooA
个实例,所以我最终得到 1 CypherableFooA
for each ListA
属性 中的项目一个 FooA
。例如如果您在每个 FooA
上的 ListA
中有 2 个项目并且您有 5,000 个 FooA
实例 - 您最终会得到包含 10,000 个项目的 cypherable
。
现在,使用 cypherable
我调用我的 AddFooAs
方法:
public static void AddFooAs(IGraphClient gc, IList<CypherableFooA> fooAs, int batchSize = 10000, int startPoint = 0)
{
var batch = fooAs.Skip(startPoint).Take(batchSize).ToList();
Console.WriteLine($"FOOA--> {startPoint} to {batchSize + startPoint} (of {fooAs.Count}) = {batch.Count}");
if (batch.Count == 0)
return;
gc.Cypher
.Unwind(batch, "faItem")
.Merge("(fa:FooA {Id: faItem.Id})")
.OnCreate().Set("fa = faItem")
.Merge("(fb:FooB {Id: faItem.RelA_FooBId})")
.Create("(fa)-[:RelA {Prop: faItem.RelA_Val1}]->(fb)")
.ExecuteWithoutResults();
AddFooAs(gc, fooAs, batchSize, startPoint + batch.Count);
}
这会将查询分成 10,000 个批次(默认情况下)- 这对我来说大约需要 5-6 秒 - 与我一次尝试所有 60,000 个差不多。
RelB
您在示例中使用 FooA
存储 RelB
,但您正在编写的查询根本不使用 FooA
,所以我所做的是提取并展平 ListB
属性:
RelB
个实例
var relBs = fooAList.SelectMany(a => a.ListB.Select(lb => lb));
然后我像这样将它们添加到 Neo4j 中:
public static void AddRelBs(IGraphClient gc, IList<RelB> relbs, int batchSize = 10000, int startPoint = 0)
{
var batch = relbs.Select(r => new { StartId = r.Start.Id, EndId = r.End.Id, r.ValExample }).Skip(startPoint).Take(batchSize).ToList();
Console.WriteLine($"RELB--> {startPoint} to {batchSize + startPoint} (of {relbs.Count}) = {batch.Count}");
if(batch.Count == 0)
return;
var query = gc.Cypher
.Unwind(batch, "rbItem")
.Match("(fb1:FooB {Id: rbItem.StartId}),(fb2:FooB {Id: rbItem.EndId})")
.Create("(fb1)-[:RelA {Prop: rbItem.ValExample}]->(fb2)");
query.ExecuteWithoutResults();
AddRelBs(gc, relbs, batchSize, startPoint + batch.Count);
}
同样,批处理默认为 10,000。
显然,时间会根据 ListB
和 ListA
中的 rels 数量而有所不同 - 我的测试在 ListA
中有一个项目,在 ListB
中有 2 个项目。