首页 > 解决方案 > Neo4j 高效地添加多个节点和边

问题描述

我有下面的例子。

例子

我想知道在单个事务中添加节点和边列表的最佳和最快的方法是什么?我使用标准 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 列表。

希望这是有道理的,谢谢!

标签: neo4jneo4jclient

解决方案


最大的问题是嵌套列表,这意味着您必须执行foreach循环,因此您最终每个执行至少4 个查询,这对于 60,000 - 嗯 - 很多! FooA

快速说明 RE:索引

首先也是最重要的 - 你需要一个关于你和节点Id属性的索引,这将大大加快你的查询速度。FooAFooB

我对此进行了一些尝试,让它存储了 60,000 个 FooA 条目,并在我老化的计算机上大约 12-15 秒内创建了 96,000 个 RelB 实例。

解决方案

我把它分成 2 个部分 - FooA 和 RelB:

FooA

我不得不将类规范化FooA为我可以使用的东西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_Val1RelA_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实例,所以我最终为 a 的属性中的CypherableFooA每个项目得到1 。例如,如果您每个都有 2 个项目并且您有 5,000个实例 - 您最终将包含 10,000 个项目。ListAFooAListAFooAFooAcypherable

现在,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,但是您正在编写的查询根本不使用FooA,所以我所做的是提取并展平属性RelB中的所有实例ListB

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。

显然,时间会根据 rels 的数量而有所不同ListB-ListA我的测试有一项 inListA和 2 in ListB


推荐阅读