Neo4j Эффективное добавление нескольких узлов и ребер - PullRequest
0 голосов
/ 09 июня 2018

У меня есть приведенный ниже пример.

Example

Мне было интересно, как лучше всего и быстрее добавить список узлов и ребер в одной транзакции?Я использую стандартные пакеты 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;} 

}

В настоящее время я проверяю, существует ли узел 'A' путем сопоставления по Id.Если это так, то я полностью пропускаю и перехожу к следующему пункту.Если нет, я создаю узел «А» со своими собственными свойствами.Затем я создаю ребра с их собственными уникальными свойствами.

Это довольно много транзакций на элемент.Совпадение узлов по 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)
            }
         }

Как можно было бы добавить список FooA с использованием, например, Neo4jClient и UNWIND или любым другим способом, кроме импорта CSV.

Надеюсь, что это имеет смысл, и спасибо!

1 Ответ

0 голосов
/ 13 июня 2018

Самая большая проблема - это вложенные списки, что означает, что вы должны выполнить свои циклы foreach, поэтому вы в конечном итоге выполните минимум из 4 запросов за FooA,что для 60 000 - ну, это много!

Краткое примечание RE: Индексирование

Прежде всего - вам нужен индекс для свойства Id ваших FooA и FooBузлы, это значительно ускорит ваши запросы.

Я немного поиграл с этим, сохранив 60 000 записей FooA и создав 96 000 экземпляров RelB примерно за 12-15 секунд на моем устаревшем компьютере.

Решение

Я разделил его на 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_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 для каждый элемент в ListA свойстве FooA.например, если у вас было 2 элемента в ListA на каждые FooA, и у вас есть 5000 FooA экземпляров - в итоге вы получите cypherable, содержащий 10 000 элементов.

Теперь с cypherable я звонюmy AddFooAs method:

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

Вы сохраняете 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);
}

Снова пакетная обработка по умолчанию10000.

Очевидно, что время будет варьироваться в зависимости от количества rel в ListB и ListA - Мои тесты имеют один элемент в ListA и 2 в ListB.

...