Статьи

Определение графичности с помощью генератора графиков Neo4j

На чемпионате США по воздушной гитаре участники используют свои таланты, чтобы раздражать «невидимую» гитару, чтобы раскачивать живую толпу и демонстрировать исполнение, которое выходит за рамки имитации настоящей гитары и само по себе становится формой искусства. Ключевым фактором, который определяет победителя, является неуловимое качество « Воздушность ». При рассмотрении использования Neo4j в проекте одним из ключевых соображений является наличие доменной модели, которая поддается представлению графа. Другими словами, есть ли у ваших данных « графичность»«. Тем не менее, до недавнего времени до меня не дошло, что, когда вы начинаете доказательство концепции, у вас, вероятно, нет этих данных (или их достаточно), или, возможно, ваши сотрудники службы безопасности не позволят вам в пределах 100 миль от производства компании. данные с этим новомодным nosql thingamajig.

Таким образом, чтобы проверить наши идеи и создать доказательство концепции, нам нужно будет сгенерировать пример данных и протестировать наши алгоритмы (иначе как запросы Cypher и Gremlin). Я покажу вам, как построить генератор элементарных графов, вам придется настроить его в соответствии с вашим доменом, но это только начало. Мы также будем использовать Batch Importer для быстрой загрузки наших данных в Neo4j.

Если вы помните, у меня было три сообщения в блоге о Batch Importer. В первом я показал вам, как установить Batch Importer , во втором я показал вам, как использовать данные в вашей реляционной базе данных для генерации CSV-файлов для создания вашего графика , и совсем недавно я показал вам, как быстро индексировать ваши данные .

Пакетный импортер ожидает серию разделенных табуляцией файлов для ввода. Итак, давайте сгенерируем эти файлы. Мы создадим граф с 6 типами узлов. Вот они с количеством каждого, которое мы собираемся создать:

  # Nodes
  #   Users             21,000
  #   Companies          4,000
  #   Activity            1.2M
  #   Item                1.3M 
  #   Entity              3.5M
  #   Tags              20,000

Мы свяжем их вместе с набором отношений:

  # Relationships
  #  Users            -[:belongs_to]-> Companies
  #  User             -[:performs]->   Activity
  #  Activity         -[:belongs_to]-> Item
  #  Item             -[:references]-> Entity
  #  Item             -[:tagged]->     Tags
  #

Чтобы упростить этот пример, мы создадим всего два индекса. Полнотекстовый индекс узла, называемый «вершинами», и точный индекс отношения, называемый «ребрами». Возможно, вы захотите создать несколько индексов для каждого типа узла или отношения.

Я хочу сделать это прямо сейчас, поэтому мы сделаем это с помощью ряда команд rake:

rake neo4j:install
rake neo4j:create
rake neo4j:load
rake neo4j:start

Если вы следили за моим блогом, вы знаете, что делают установка и запуск, но нам нужно создать метод, который будет обрабатывать создание и загрузку. Мы можем быстро создать Rakefile для них:

require 'neography/tasks'
require './neo_generate.rb'

namespace :neo4j do
  task :create do
    create_graph
  end
  task :load do
    load_graph
  end
end

Теперь мы можем начать с create_graph. Если вы помните, пакетный импортер ищет серию разделенных табуляцией файлов. Один, который содержит узлы, другой для отношений и, необязательно, другие файлы для каждого индекса, который вы хотите создать. Каждый файл имеет заголовок с некоторыми свойствами, поэтому наш метод create_graph будет выглядеть так:

  def create_graph
    create_node_properties
    create_nodes
    create_nodes_index
    create_relationship_properties
    create_relationships
    create_relationships_index
  end  

Я собираюсь здесь произвольно решить, что каждый из моих узлов будет иметь два свойства, и мы назовем их property1 и property2, потому что я супер креативен, когда дело доходит до именования вещей.

  def create_node_properties
    @node_properties = ["type", "property1", "property2"]
    generate_node_properties(@node_properties)  
  end

Я сказал два? Я имел в виду три. Просто для собственного здравого смысла я хотел бы дать узлам свойство типа и указать тип узла, которым они являются, поэтому мы включим «тип» в качестве первого свойства.

  #  Recreate nodes.csv and set the node properties 
  #  
  def generate_node_properties(args)
    File.open("nodes.csv", "w") do |file|
      file.puts properties.join("\t")
    end
  end

С нашим заголовком мы можем обратить наше внимание на фактическое создание этих узлов. Мы будем использовать хеш, который будет иметь тип узла, начальный и конечный идентификаторы узлов и некоторые свойства. Но какими свойствами должны обладать наши узлы? Какими должны быть их ценности? Это немного сложно. Самое простое решение состоит в том, чтобы просто сгенерировать gobbledygook со случайными строками:

  # Generate random lowercase text of a given length 
  # 
  # Args
  #  length - Integer (default = 8)
  #
  def generate_text(length=8)
    chars = 'abcdefghjkmnpqrstuvwxyz'
    key = ''
    length.times { |i| key << chars[rand(chars.length)] }
    key
  end

Другой возможностью является использование одного из драгоценных камней генератора случайных данных, таких как подделка, для создания более интеллектуальных и конкретных случайных данных (например, имен женщин ). В этот раз мы выберем легкий путь и просто дадим каждому узлу два случайных свойства, за исключением пользователя и компании, которые получат свойства от Подделки.

  def create_nodes
    # Define Node Property Values
    node_values    = [lambda { generate_text              }, lambda { generate_text              }]
    user_values    = [lambda { Forgery::Name.full_name    }, lambda { Forgery::Personal.language }]
    company_values = [lambda { Forgery::Name.company_name }, lambda { Forgery::Name.industry     }]
    
   @nodes = {"user"     => { "start" => 1,
                             "end"   => 21000, 
                             "props" => user_values},
             "company"  => { "start" => 21001,
                             "end"   => 25000, 
                             "props" => company_values},
             "activity" => { "start" => 25001,
                             "end"   => 1225000, 
                             "props" => node_values},
             "item"     => { "start" => 1225001,
                             "end"   => 2525000, 
                             "props" => node_values},
             "entity"   => { "start" => 2525001,
                             "end"   => 6025000, 
                             "props" => node_values},
             "tag"      => { "start" => 6025001,
                             "end"   => 6045000, 
                             "props" => node_values}
    }
    
    # Write nodes to file
    @nodes.each{ |node| generate_nodes(node[0], node[1])}
  end

Отлично, теперь, чтобы наконец сгенерировать эти узлы, мы напишем в node.csv тип узла и будем называть нашу лямбду, чтобы каждый узел получал различную случайную строку.

  # Generate nodes given a type and hash
  #
  def generate_nodes(type, hash)
    puts "Generating #{(1 + hash["end"] - hash["start"])} #{type} nodes..."
    nodes = File.open("nodes.csv", "a")

    (1 + hash["end"] - hash["start"]).times do |t|
        properties = [type] + hash["props"].collect{|l| l.call}
        nodes.puts properties.join("\t")
    end
    nodes.close
  end

Наш файл node.csv будет выглядеть следующим образом:

type    property1       property2
user    Helen Harvey    Kashmiri
user    Sean Matthews   Afrikaans
user    William Harper  Haitian Creole
user    Bruce Hill      Macedonian
user    Chris Riley     Swahili

С узлами в стороне, пришло время для отношений. Мы будем простыми и скажем, что у каждого отношения также есть два свойства.

  def create_relationship_properties
    @rel_properties = ["property1", "property2"]
    generate_rel_properties(@rel_properties)
  end

Я имел в виду три свойства. Еще раз я добавляю тип, но это отличается от типа узла выше, поскольку каждое отношение в Neo4j ДОЛЖНО иметь тип, это не необязательное свойство. «\ T», который вы видите ниже, помещает вкладки между каждым полем, извините, если я не упомянул об этом раньше, и вы были как, черт возьми, это?

  #  Recreate rels.csv and set the relationship properties 
  #  
  def generate_rel_properties(properties)
    File.open("rels.csv", "w") do |file|
      header = ["start", "end", "type"] + properties
      file.puts header.join("\t")
    end
  end

Я показал вам, как создавать хорошие поддельные данные для узлов, поэтому мы будем делать это просто и просто делать случайные 8-символьные строки. Я использую числовое поле, чтобы установить, сколько из этих отношений будет создано, их тип обязателен и некоторые свойства. Вы также заметите, что у меня есть ключ «подключения»: последовательный или случайный. Я объясню это немного позже.

  def create_relationships
    # Define Relationsihp Property Values
    rel_values = [lambda { generate_text }, lambda { generate_text }]

    rels = {"user_to_company"  => { "from"  => @nodes["user"],
                                    "to"     => @nodes["company"],
                                    "number" => 21000,
                                    "type"   => "belongs_to",
                                    "props"  => rel_values,
                                    "connection" => :sequential },
            "user_to_activity" => { "from"  => @nodes["user"],
                                    "to"    => @nodes["activity"],
                                    "number" => 1200000,
                                    "type"   => "performs",
                                    "props"  => rel_values,
                                    "connection" => :random },
            "activity_to_item" => { "from"  => @nodes["activity"],
                                    "to"    => @nodes["item"],
                                    "number" => 3000000,
                                    "type"   => "belongs",
                                    "props"  => rel_values,
                                    "connection" => :random },
            "item_to_entity"   => { "from"  => @nodes["item"],
                                    "to"    => @nodes["entity"],
                                    "number" => 6000000,
                                    "type"   => "references",
                                    "props"  => rel_values,
                                    "connection" => :random },
            "item_to_tag"      => { "from"  => @nodes["item"],
                                    "to"    => @nodes["tag"],
                                    "number" => 250000,
                                    "type"   => "tagged",
                                    "props"  => rel_values,
                                    "connection" => :random }                                   
    }
  
    # Write relationships to file
    rels.each{ |rel| generate_rels(rel[1])}  
  end

Я использую «соединение», чтобы решить, как соединить эти узлы вместе. Я генерирую либо случайные соединения между узлами, либо генерирую последовательные соединения (так как в каждом «от узла» подключается к одному «к узлу», пока не будет больше соединений, и если будет больше соединений, чем узлов, мы зациклимся).

Не стесняйтесь объединять их или создавать новые типы соединений (например, кластеризованные)

  def generate_rels(hash)
    puts "Generating #{hash["number"]} #{hash["type"]} relationships..."
    File.open("rels.csv", "a") do |file|
    
      case hash["connection"]
        when :random      
          hash["number"].times do |t|
            file.puts "#{rand(hash["from"]["start"]..hash["from"]["end"])}\t#{rand(hash["to"]["start"]..hash["to"]["end"])}\t#{hash["type"]}\t#{hash["props"].collect{|l| l.call}.join("\t")}" 
          end
        when :sequential
          from_size = hash["from"]["end"] - hash["from"]["start"]
          to_size = hash["to"]["end"] - hash["to"]["start"]
          hash["number"].times do |t|
            file.puts "#{hash["from"]["start"] + (t % from_size)}\t#{hash["to"]["start"]  + (t % to_size)}\t#{hash["type"]}\t#{hash["props"].collect{|l| l.call}.join("\t")}" 
          end
      end
    end
  end

Наш файл rels.csv будет выглядеть следующим образом:

start   end     type    property1       property2
1       21001   belongs_to      sjqwkvag        vpxahvcr
2       21002   belongs_to      pfxnxznu        vrprnpky
3       21003   belongs_to      gcyxumgy        nrxepdzb
4       21004   belongs_to      aayyejkw        xpenqebd
5       21005   belongs_to      hvhjexas        kmyqucmn

Чтобы создать наш индекс узла, мы просто откроем node.csv и выведем его, добавив идентификатор узла в качестве первого столбца. Майкл работает над использованием заголовков node.csv, чтобы сказать Batch Importer индексировать узлы, но пока эта работа не будет выполнена, это будет работать.

  def create_nodes_index
    puts "Generating Node Index..."
    nodes = File.open("nodes.csv", "r")
    nodes_index = File.open("nodes_index.csv","w")
    counter = 0
    
    while (line = nodes.gets)
      nodes_index.puts "#{counter}\t#{line}"
      counter += 1
    end
    
    nodes.close
    nodes_index.close
  end

Следовательно, node_index.csv будет выглядеть так:

0       type    property1       property2
1       user    Helen Harvey    Kashmiri
2       user    Sean Matthews   Afrikaans
3       user    William Harper  Haitian Creole
4       user    Bruce Hill      Macedonian
5       user    Chris Riley     Swahili

Мы сделаем нечто подобное со связями, но пропустим начальный и конечный узлы, а также тип связи.

  def create_relationships_index
    puts "Generating Relationship Index..."
    rels = File.open("rels.csv", "r")
    rels_index = File.open("rels_index.csv","w")
    counter = -1
    
    while (line = rels.gets)
      size ||= line.split("\t").size
      rels_index.puts "#{counter}\t#{line.split("\t")[3..size].join("\t")}"
      counter += 1
    end
    
    rels.close
    rels_index.close
  end

Наш файл rels_index.csv будет выглядеть так:

-1      property1       property2
0       nwjsbmgg        gnsnefrf
1       szqqygra        maumqtnp
2       pdtamztw        uvcserrp
3       wewdtztx        bkezsmva
4       gynprabv        eszjgmfs
5       drcaxsse        ungxbzzm

Давайте запустим neo4j: create для генерации этих файлов. Сейчас самое подходящее время для быстрого растягивания, биоразрыва и т. Д., Так как это может занять пару минут.

Generating 21000 user nodes...
Generating 4000 company nodes...
Generating 1200000 activity nodes...
Generating 1300000 item nodes...
Generating 3500000 entity nodes...
Generating 20000 tag nodes...
Generating Node Index...
Generating 21000 belongs_to relationships...
Generating 1200000 performs relationships...
Generating 3000000 belongs relationships...
Generating 6000000 references relationships...
Generating 250000 tagged relationships...
Generating Relationship Index...

С возвращением, теперь у нас сгенерированы эти четыре CSV-файла, нам нужно запустить пакетный импортер, чтобы поместить их в Neo4j. Поэтому мы запустим rake neo4j: load, чтобы это произошло, что, как вы помните, вызывает метод load_graph. Это выглядит так:

  # Execute the command needed to import the generated files
  #
  def load_graph
    puts "Running the following:"
    command ="java -server -Xmx4G -jar ../batch-import/target/batch-import-jar-with-dependencies.jar neo4j/data/graph.db nodes.csv rels.csv node_index vertices fulltext nodes_index.csv rel_index edges exact rels_index.csv" 
    puts command
    exec command    
  end </pre></p>

Пакетный импортер теперь сделает свое дело:

............................................................
Importing 6045000 Nodes took 39 seconds 
....................................................................................................377369 ms for 10000000
....
Importing 10471000 Relationships took 476 seconds 
............................................................
Importing 6045000 Nodes into vertices Index took 226 seconds 
....................................................................................................261031 ms for 10000000
....
Importing 10471000 Relationships into edges Index took 266 seconds 
1153 seconds 

Теперь мы можем запустить rake neo4j: начать видеть наш график в Neo4j.

Давайте перейдем к консоли и убедимся, что наши данные там есть:

START me = node:vertices(type="user") 
RETURN me 
LIMIT 5

Успех!

==> +-------------------------------------------------------------------------------+
==> | me                                                                            |
==> +-------------------------------------------------------------------------------+
==> | Node[1]{property2->"Kashmiri",property1->"Helen Harvey",type->"user"}         |
==> | Node[2]{property2->"Afrikaans",property1->"Sean Matthews",type->"user"}       |
==> | Node[3]{property2->"Haitian Creole",property1->"William Harper",type->"user"} |
==> | Node[4]{property2->"Macedonian",property1->"Bruce Hill",type->"user"}         |
==> | Node[5]{property2->"Swahili",property1->"Chris Riley",type->"user"}           |
==> +-------------------------------------------------------------------------------+
==> 5 rows, 111 ms