Языковой буфет: использование Neo4j с GraalVM, часть 2

Мы продолжаем наше путешествие по различным вариантам использования GraalVM и Neo4j. В последнем сообщении блога мы рассмотрели несколько различных способов совместного использования Neo4j и GraalVM. Напоминаем, что список представлен ниже.

  1. Клиенты Polyglot - доступ к Neo4j с языков с помощью официального драйвера (например, драйвера Java). Используйте библиотеки на исходном языке для подключения к Neo4j из целевых реализаций языка GraalVM.
  2. Совместное использование библиотек - доступ к библиотекам, не относящимся к Java, для использования в программах на других языках. Например, вставьте библиотеку Python ratelimit или цвета Javascript в программу Java, которая взаимодействует с Neo4j.
  3. Процедуры Polyglot - расширьте Neo4j и язык запросов Cypher, написав процедуры и функции на любом языке и упаковав их в качестве подключаемого модуля базы данных Neo4j. Пишите расширения на языках JVM (Java, Kotlin, Scala, Groovy и т. Д.) И на языках, отличных от JVM. Выполнение кода, зависящего от языка, в процедуре Cypher (т. Е. Запуск Python внутри инструкции Cypher).
  4. Polyglot Cypher - используйте Cypher в качестве языка запросов в различных программах (путем реализации Cypher в языковой структуре Truffle GraalVM). Встраивайте код Cypher в свою программу Python или Javascript для выполнения в Neo4j.

В предыдущем посте также был рассмотрен первый сценарий подключения к Neo4j с разных языков с использованием многоязычных возможностей GraalVM и официального Java-драйвера Neo4j. В этом раунде мы пойдем немного сложнее и рассмотрим третий пункт для создания расширения базы данных Neo4j, которое будет запускать код на разных языках в вызове процедуры Cypher.

Давайте начнем!

Расширьте Neo4j и Cypher с помощью настраиваемых процедур и функций

Многие базы данных предоставляют возможность писать собственный код для обработки функций, которые не предоставляются "из коробки", и Neo4j не является исключением. Когда определенные возможности недоступны или сложны в Cypher, пользователи могут писать свои собственные процедуры и функции, упаковать их и добавить в качестве подключаемого модуля в базу данных. Таким образом были созданы такие вещи, как библиотека APOC, GDS и другие!

Однако традиционно эти расширения написаны на языке, основанном на JVM (виртуальная машина Java), таком как Java, Groovy, Scala и т.д. понимать цель, не относящуюся к JVM (например, Python). GraalVM переводит различные языки, которые были реализованы с их языковой структурой Truffle, обеспечивая взаимодействие между языками.

Настраивать

Во-первых, если у вас его еще нет, нам понадобится Neo4j. К сожалению, Neo4j Desktop и Sandbox имеют предопределенные среды, которые затрудняют одновременный запуск среды GraalVM, поэтому самый простой подход - загрузить Neo4j server community edition для этого примера.

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

GraalVM - еще одна установка JDK (Java Development Kit), представляющая собой набор инструментов для разработки приложений Java. Если вы знакомы с этим, не стесняйтесь использовать, однако вам удобнее всего работать с версиями Java и JDK. Если вы новичок в JDK, я нашел статью, объясняющую компоненты среды Java. Для управления всеми опциями на моей машине мне очень нравится SDKMAN!. Он автоматически синхронизирует пути к классам и позволяет легко менять версии и поставщиков с помощью одной или двух команд. Команды для установки GraalVM JDK с SDKMAN! перечислены ниже.

#List available Java vendors and versions in SDKMAN!
% sdk list java
#Install one for GraalVM (my current version)
% sdk install java 20.3.0.r11-grl
#Switch Java versions
% sdk use java 20.3.0.r11-grl
#(optional) Set it as the default JDK for your system
% sdk default java 20.3.0.r11-grl
#Verify Java version on your system (and results for my environment)
% java -version
openjdk version “11.0.9” 2020–10–20
OpenJDK Runtime Environment GraalVM CE 20.3.0 (build 11.0.9+10-jvmci-20.3-b06)
OpenJDK 64-Bit Server VM GraalVM CE 20.3.0 (build 11.0.9+10-jvmci-20.3-b06, mixed mode, sharing)

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

Хорошо, это основные требования для установки - GraalVM и Neo4j. Есть несколько других настроек, необходимых для работы с этим на разных языках. Хотя вы можете использовать стандартные языковые среды, я выбрал встроенные языки GraalVM, поскольку предполагаю, что они требуют меньше затрат на настройку. Чтобы установить каждый из языков, поддерживаемых GraalVM, мы можем использовать инструмент GraalVM Updater (gu). Команды для использования gu для установки каждого языка показаны ниже.

#See what’s there already
gu list
#Python
gu install python
#Javascript (included)
#R
gu install r
#Ruby
gu install ruby

Примечание: gu входит в базовую установку GraalVM. Если вы не установили какие-либо другие языки до того, как запустите команду gu list, показанную первой в блоке кода выше, вы можете заметить, что кое-что уже есть. Это потому, что они встроены в общую установку GraalVM.

Для установки R есть еще пара необходимых зависимостей, перечисленных в документации. На моем Mac они уже были установлены в моей системе, но в зависимости от вашей операционной системы и версии вы можете захотеть их проверить.

В Ruby также необходимо установить несколько дополнительных зависимостей. Большинство из них уже было установлено на моем Mac, но вы можете проверить их для своей операционной системы и версии. После их завершения первая команда в блоке кода ниже запускает сценарий для соединения openssl и libssl.

У меня также были проблемы с рекомендацией использовать менеджер Ruby. Это переместило путь туда, где я не мог выполнить Руби. В итоге я удалил свой менеджер Ruby и переназначил TruffleRuby. В конце концов, две приведенные ниже команды должны помочь вам увидеть, похожа ли ваша среда на мою. Обратите внимание, что SDKMAN! на моем пути к TruffleRuby.

#After installing deps, make the Ruby openssl C extension work with your system libssl)
<path to your GraalVM JDK>/languages/ruby/lib/truffle/post_install_hook.sh
% truffleruby -v
truffleruby 20.3.0, like ruby 2.6.6, GraalVM CE Native [x86_64-darwin]
% which truffleruby
/Users/jenniferreif/.sdkman/candidates/java/current/bin/truffleruby

Вы можете проверить, установлены ли все нужные языки, снова запустив команду gu list, чтобы увидеть все языки, которые у вас сейчас есть. Давайте запустим Neo4j с помощью команды bin/neo4j start, и, наконец, мы готовы запустить наш проект!

Расширенный проект

Я использовал Maven для этого проекта, но вы можете использовать Gradle или что-то еще, если хотите. Зависимости довольно просты, нам просто нужно включить Neo4j и GraalVM SDK в pom.xml файл. У нас также есть несколько интересных дополнений в разделе сборки помпона, так что давайте посмотрим на них.

<build>
  <plugins>
    <plugin>
      <artifactId>maven-dependency-plugin</artifactId>
      <executions>
        <execution>
          <phase>prepare-package</phase>
          <goals>
            <goal>copy-dependencies</goal>
          </goals>
          <configuration>
   <outputDirectory>${project.build.directory}/lib</outputDirectory>
          </configuration>
        </execution>
      </executions>
    </plugin>
    <plugin>
      <artifactId>maven-shade-plugin</artifactId>
      <version>3.2.2</version>
      <configuration>
   <createDependencyReducedPom>false</createDependencyReducedPom>
      </configuration>
      <executions>
        <execution>
          <phase>package</phase>
          <goals>
            <goal>shade</goal>
          </goals>
        </execution>
      </executions>
    </plugin>
  </plugins>
</build>

Если вы читали предыдущий пост в блоге, возможно, вы вспомнили первый плагин зависимости. Это упаковывает зависимости на этапе подготовки пакета сборки и помещает их в папку /target/lib нашего проекта.

Второй зависимости нет в предыдущем примере. Он генерирует файл jar с нашим пользовательским кодом вместе с зависимостями, указанными в <scope>compile</scope>. Но прежде чем мы перейдем к упаковке, давайте посмотрим на остальной код!

Пользовательские процедуры и функции

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

В папке src/main/java находится папка polyglotProcedures, содержащая две программы Java. Один - это класс PolyglotUtil, который определяет процедуру polyglot.run, а другой - TypeConverter (скрытый от аналогичного проекта Дэвида Аллена). Мы начнем с PolyglotUtil, а затем пройдемся по TypeConverter.

public class PolyglotUtil {
  @Context
  public GraphDatabaseService db;
  @Context
  public Log log;
  …
}

Класс PolyglotUtil начинает с определения контекста для подключения к нашей базе данных графов Neo4j и настраивает ведение журнала. Затем мы готовы определить любые процедуры и функции, которые мы хотим вызывать.

@Procedure(value = “polyglot.run”)
@Description(“polyglot.run(language, code) — Executes the code given. Throws things otherwise.”)
public Stream<Output> execute(@Name(“language”) String language, @Name(“code”) String code) throws IOException {
  
  try (var context = org.graalvm.polyglot.Context.newBuilder().allowAllAccess(true).build()) {
    var bindings = context.getPolyglotBindings();
    bindings.putMember(“db”, db);
    
    Value v = context.eval(language, code);
    log.info(“Check value equals “ + v);
    Object result = convert(v);
    //Map these to a generic output as a type hack around the uncertainty of what comes back
    //Neo4j procs require a stream of concrete types
    return Stream.of(new Output(result));
  } catch (Exception exc) {
    exc.printStackTrace();
    throw exc;
  }
}

Здесь я установил единственную процедуру под названием polyglot.run(), которая принимает 2 параметра: 1 для языка кода, который мы хотим запустить, 1 для фактического кода, который мы хотим выполнить. Во-первых, нам нужно пометить ее как процедуру (аннотация @Procedure) и определить документацию для нее (аннотация @Description). Нам нужно, чтобы возвращаемый тип был Stream типа Object (в данном случае Output), потому что Cypher нужен поток универсального типа объекта для обработки и возврата результатов. Мы поговорим об этом подробнее через минуту, когда перейдем к следующему блоку кода.

Внутри вызова процедуры мы поместим весь наш код в блок try/catch, который гарантирует, что GraalVM может построить контекст для остальной части нашего кода. Внутри try мы получаем привязки полиглотов из контекста и помещаем в них нашу базу данных (Neo4j). Это позволяет всему общаться. Следующая строка кода определяет переменную v, имеющую Value тип, который является типом GraalVM, который позволяет нам переводить между типами данных различных языков. Мы устанавливаем переменную равной оценке кода, переданного в процедуру (сама строка language и code). Следующая инструкция журнала просто выводит переменную, чтобы проверить, соответствует ли значение нашим ожиданиям.

Затем мы пытаемся преобразовать переменную значения в тип Object с именем result. Наш convert() метод находится в классе TypeConverter, который мы обсудим через минуту, так что подробнее об этом чуть позже. Затем последняя строка в блоке try отображает результат в поток Output, который объяснен в комментарии и определен в приведенном ниже коде. Наконец, мы делаем catch, чтобы захватить любые исключения и дать трассировку стека.

public class Output {
  public Object result;
  public Output(Object thingy) {
    result = thingy;
  }
}

Cypher ожидает, что процедуры вернут поток конкретных типов, поэтому нам нужно определить конкретный тип, который содержит наш общий объект, полученный в результате выполнения кода GraalVM. Поскольку возврат от выполнения нашего кода GraalVM может быть множеством вещей - Java Decimal, Python dict, Javascript hash и т. Д., Нам нужно принять все это и все же преобразовать их в конкретный тип, который может ожидать Cypher.

Cypher на самом деле укажет это с помощью полезного сообщения об ошибке, которое вы можете увидеть, закомментировав класс Output и настроив возврат процедуры так, чтобы он возвращал Stream из result вместо Output. При выполнении Cypher должен показать ошибку, ожидающую возврата результатов определенного типа, и даже рекомендует класс Output для решения проблемы (именно то, что мы использовали здесь).

Хорошо, теперь к нашему TypeConverter классу! Этот класс принимает нашу переменную GraalVM Value и проверяет, понятен ли Neo4j тип данных.

public class TypeConverter {
  public static final List<String> stringList = Arrays.asList(“class”, “constructor”, “caller”, “prototype”, “__proto__”);
  public static Object convert(Value v) {
    if (v == null || v.isNull()) return null;
    if (v.isProxyObject()) {
      System.err.println(“Warning: proxy objects are not yet supported from guest languages for neo4j serialization”);
      return null;
    }
    if (v.isHostObject()) {
      return v.asHostObject();
    }
    Set<String> memberKeys = v.getMemberKeys();
    if (!memberKeys.isEmpty()) {
      Map<String,Object> result = new HashMap<>();
      for(String key : memberKeys) {
        if (!stringList.contains(key) &&
            !v.getMember(key).canExecute()) {
          System.out.println(“Recursing on “ + key);
          result.put(key, convert(v.getMember(key)));
        }
      }
      return result;
    }
    if (v.isBoolean()) return v.asBoolean();
    if (v.isNumber()) return v.asDouble();
    if (v.isString()) return v.asString();
    System.err.println(“Unsupported guest language values cannot be mapped, and will be returned as null”);
    return null;
  }
}

В приведенном выше коде у нас есть серия if операторов, которые проверяют, является ли GraalVM Value Object (нулевым, прокси, хостом или картой), boolean, number или string, и пытается преобразовать его в тип Java.

Пришло время развернуть наш код и протестировать его!

Развернуть настраиваемый код как подключаемый модуль базы данных

В предпочитаемой вами среде IDE или в командной строке создайте свой проект (или его клон этот), а затем выполните команду mvn clean package. Это упаковывает все и помещает файл .jar нашего проекта в каталог target. Скопируйте созданный .jar в папку /plugins вашей базы данных Neo4j. Вам нужно будет найти, где вы установили Neo4j, и там должна быть папка плагинов.

Если Neo4j уже был запущен, нам нужно будет его перезапустить (bin/neo4j restart). В противном случае мы можем запустить базу данных с bin/neo4j start. Через несколько секунд он должен запуститься, но я предпочитаю проверить, открыв другое окно командной строки, перейдя в каталог Neo4j, затем cd logs и запустив tail -f neo4j.log, чтобы ничего не проверить, выдает код ошибки. Место, где я запускаю команду запуска, не всегда показывает четкое сообщение об ошибке / завершении работы, когда что-то идет не так, поэтому просмотр файла журнала делает это немного более заметным.

Теперь мы можем получить доступ к браузеру Neo4j, открыв веб-браузер и перейдя в localhost:7474. Если вы не знакомы с браузером Neo4j, то на верхней панели ввода мы вводим и запускаем запросы и процедуры Cypher. В этой командной строке выполните процедуру с CALL polyglot.run(arg1, arg2). Вы можете использовать различные языки и код для аргументов, но я привел несколько примеров ниже, чтобы вы начали.

//returns the string “hello” in a result pane
CALL polyglot.run(‘js’, ‘“hello”’)
//prints “hello” to neo4j.log output
CALL polyglot.run(‘js’,’print(“hello”)’)
//executes the math and returns the result
CALL polyglot.run(‘python’, ’CALL polyglot.run(‘python’, ‘import math; totalEntities = 3000; callsNeeded = int(math.ceil(totalEntities / 100)); callsNeeded’);’)

Языки, которые принимает первый параметр, включают все, что реализовано в GraalVM Truffle, включая llvm, R, js, python, ruby.

Подведение итогов!

Вы можете использовать этот проект как есть для запуска множества пользовательских процедур, функций или фрагментов кода в вызове Cypher. Или вы можете использовать это как шаблон для создания собственных процедур и функций, которые принимают файлы сценариев или написать дополнительный код на других языках. Не стесняйтесь попробовать и сообщите нам, что вам нужно или что вы хотели бы увидеть в этом проекте!

Как и в случае с предыдущим проектом GraalVM, мы ищем отзывы, чтобы лучше понять, что необходимо или используется в этой области проекта. Мы будем рады услышать от вас через Github (нравится проект или создавать проблемы / запросы функций) или через Сайт сообщества Neo4j (получить помощь или сообщить нам, что вам нравится / не нравится). Удачного кодирования!

Ресурсы