Взгляните на новый формат HTTP Jolt, представленный в Neo4j 4.2

В этом посте мы рассмотрим некоторые обновления, внесенные в HTTP API Neo4j в последней версии Neo4j 4.2, в частности, новый формат сериализации результатов под названием Jolt (сокращение от JSON Bolt). Затем мы увидим, как использовать транзакционную конечную точку HTTP Cypher Neo4j в Cloudflare Worker для создания конечной точки результатов выборов с персонализированным местоположением.

Я затронул эту тему в недавнем Neo4j Livestream, поэтому, если вы предпочитаете посмотреть видеозапись, вы можете найти ее здесь:

Обзор HTTP API Neo4j

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

Однако, помимо конечной точки Bolt, существует также конечная точка транзакционного Cypher, которую можно использовать с Neo4j в тех случаях, когда Bolt не является жизнеспособным вариантом. Чтобы найти конечную точку HTTP-транзакции, вы можете использовать конечную точку HTTP обнаружения, чтобы перечислить все доступные конечные точки для вашего экземпляра Neo4j:

GET http://localhost:7474/
Accept: application/json

----------------------------

200 OK
Content-Type: application/json

{
  "bolt_direct": "bolt://localhost:7687",
  "bolt_routing": "neo4j://localhost:7687",
  "cluster": "http://localhost:7687/db/{databaseName}/cluster",
  "transaction": "http://localhost:7474/db/{databaseName}/tx",
  "neo4j_version": "4.0.0",
  "neo4j_edition": "enterprise"
}

Конечная точка HTTP транзакционного шифра

Транзакционная конечная точка Cypher HTTP позволяет нам открывать транзакции, выполнять операторы Cypher, фиксировать или откатывать транзакции и работать с результатами наших операторов Cypher через HTTP-запросы. Результаты сериализуются с использованием JSON или (начиная с Neo4j 4.2) Jolt.

Jolt = JSON Bolt

Jolt является сокращением от JSON Bolt и представляет собой формат на основе JSON, который кодирует тип значения ответа вместе со значением в одноэлементном объекте. Это позволяет нам представлять все типы из системы типов PackStream, используемой Bolt. Одна из проблем с использованием JSON для сериализации данных заключается в том, что мы можем изначально представить только подмножество типов PackStream (подумайте о типах datetime, геопространственном типе, 64-битных целых числах и т. Д.).

В таких форматах, как MessagePack и PackStream, маркер используется для кодирования типа значения.

В Jolt ключ одноэлементного объекта указывает тип, а значение сохраняет значение. Обратитесь к документации Jolt за полной документацией по кодированию, но давайте рассмотрим несколько примеров.

Обратите внимание, что значение для узлов и отношений - это кортеж (кортеж из 3-х узлов и 5-кортеж для отношений), поскольку в большинстве случаев индексация в кортеж более эффективна, чем работа с объектом.

Толчковый узел

Вот пример того, как Node сериализуется с помощью Jolt.

{
  "()": [
    9285,
    [
      "Airport"
    ],
    {
      "code": "REH",
      "name": "Rehoboth Airport",
      "location": {
        "@": "SRID=4326;POINT(-75.122 38.72)"
      }
    }
  ]
}

Он представляет узел с внутренним идентификатором 9285, одной меткой Airport и тремя свойствами, code, name и location, где location - это тип Point.

Взволнованные отношения

А вот пример отношений, представленных в Jolt. В частности, связь, соединяющая узел с идентификатором 5129 с узлом с идентификатором 9285, тип IN_STATE, внутренний идентификатор отношения 8 и без свойств.

{
  "->": [
    5129,
    9285,
    "IN_STATE",
    8,
    {}
  ]
}

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

Пример использования набора данных о результатах выборов

В ночь выборов несколько недель назад я подумал, что было бы забавно импортировать данные результатов прямых выборов в Neo4j, пока мы все ждали результатов. Я не буду рассказывать, как импортировать этот набор данных в этом посте, но вы можете получить доступ к базе данных по адресу https://elections.graph.zone, используя имя пользователя elections и пароль elections.

Давайте запросим эту базу данных и посмотрим, как выглядит Jolt для получения настоящего ответа Cypher. Вот простой запрос Cypher, который вернет узлы и отношения:

MATCH (state:State {name: $state})<-[rel:IN_STATE]-(cas) 
RETURN * LIMIT 1

Чтобы создать HTTP-запрос для отправки этого запроса Cypher, мне нравится использовать Postman, который упрощает добавление соответствующих заголовков авторизации и содержимого.

Но, конечно, мы также можем использовать такие инструменты, как curl:

curl --location --request POST 'https://elections.graph.zone/db/neo4j/tx/commit/' \
--header 'Accept: application/vnd.neo4j.jolt+json-seq' \
--header 'Content-Type: application/json' \
--header 'Authorization: Basic ZWxlY3Rpb25zOmVsZWN0aW9ucw==' \
--data-raw '{
  "statements" : [ {
    "statement" : "MATCH (state:State {name: $state})<-[rel:IN_STATE]-(cas) RETURN * LIMIT 1",
    "parameters" : {
      "state" : "Delaware"
    }
  } ]
}'

Обратите внимание на Accept заголовок application/vnd.neo4j.jolt+json-seq, который указывает, что мы хотим, чтобы Jolt возвращался из этого запроса. Вот как выглядит тело ответа:

{
    "header": {
        "fields": [
            "cas",
            "rel",
            "state"
        ]
    }
}
{
    "data": [
        {
            "()": [
                9285,
                [
                    "Airport"
                ],
                {
                    "code": "REH",
                    "name": "Rehoboth Airport",
                    "location": {
                        "@": "SRID=4326;POINT(-75.122 38.72)"
                    }
                }
            ]
        },
        {
            "->": [
                5129,
                9285,
                "IN_STATE",
                8,
                {}
            ]
        },
        {
            "()": [
                8,
                [
                    "State"
                ],
                {
                    "result": "winner",
                    "biden_winner": true,
                    "absentee_votes": 0,
                    "name": "Delaware",
                    "votes": 504010,
                    "id": "DE",
                    "electoral_votes": 3,
                    "trump_winner": false
                }
            ]
        }
    ]
}
{
    "summary": {}
}
{
    "info": {}
}

Этот ответ Jolt представляет собой путь на графике, узел аэропорта, подключенный к узлу состояния:

Если мы заменим заголовок Accept на application/json, мы увидим, как результат будет сериализован с использованием JSON. Обратите внимание, как различные типы кодируются через JSON.

{
  "results": [
    {
      "columns": ["cas", "rel", "state"],
      "data": [
        {
          "row": [
            {
              "code": "REH",
              "name": "Rehoboth Airport",
              "location": {
                "type": "Point",
                "coordinates": [-75.122, 38.72],
                "crs": {
                  "srid": 4326,
                  "name": "wgs-84",
                  "type": "link",
                  "properties": {
                    "href": "http://spatialreference.org/ref/epsg/4326/ogcwkt/",
                    "type": "ogcwkt"
                  }
                }
              }
            },
            {},
            {
              "result": "winner",
              "biden_winner": true,
              "absentee_votes": 0,
              "name": "Delaware",
              "votes": 504010,
              "id": "DE",
              "electoral_votes": 3,
              "trump_winner": false
            }
          ],
          "meta": [
            {
              "id": 9285,
              "type": "node",
              "deleted": false
            },
            {
              "id": 5129,
              "type": "relationship",
              "deleted": false
            },
            {
              "id": 8,
              "type": "node",
              "deleted": false
            }
          ]
        }
      ]
    }
  ],
  "errors": []
}

В большинстве случаев, когда мы создаем приложения с использованием Neo4j, мы используем один из языковых драйверов Neo4j, специфичных для того языка, который мы используем для создания нашего приложения. Эти драйверы используют протокол Bolt для работы с Cypher и дают нам специфичный для языка и идиоматический способ использования Cypher в наших приложениях без необходимости думать о базовом уровне транспорта или сериализации. Драйверы Neo4j доступны для многих языков и официально поддерживаются Neo4j. Так зачем нам вообще использовать транзакционную конечную точку HTTP Cypher для работы с Neo4j?

Есть несколько случаев, когда имеет смысл отправлять Cypher через HTTP в Neo4j вместо использования языковых драйверов Bolt. Возможно, мы используем один из немногих языков, на котором в настоящее время нет драйвера Neo4j. Или, возможно, мы используем систему, которая запускает веб-перехватчик, и мы хотим использовать запрос POST для вставки некоторых данных в нашу базу данных. Или, возможно, мы используем пограничные обработчики, такие как Cloudflare Workers. В Worker мы (в настоящее время) не можем делать произвольные TCP-запросы, что означает, что мы не можем использовать Bolt, но мы можем использовать транзакционный Cypher HTTP API.

Рабочие Cloudflare

Пограничные обработчики, такие как Cloudflare Workers, - это следующая итерация бессерверной технологии, которая мне очень нравится. Рабочие сопоставимы с предложениями «функция как услуга» (FaaS), такими как AWS Lambda, но устраняют многие недостатки FaaS, такие как обработка глобального развертывания и устранение проблемы холодного запуска, которая может повлиять на производительность FaaS. Cloudflare Worker развертывается в глобальной пограничной сети CDN и работает на тех же машинах, которым поручено доставлять статический контент как часть CDN.

Поскольку воркеры развернуты в глобальной сети CDN, это означает, что каждый воркер может знать местоположение, принимая во внимание местоположение пользователя, инициировавшего запрос, и обслуживая персонализированный ответ в зависимости от местоположения пользователя. Давайте посмотрим, как мы можем использовать Cloudflare Workers с нашим набором данных о выборах в Neo4j для создания конечной точки, которая будет обслуживать результаты выборов, релевантные для пользователя, в зависимости от их местоположения.

Поскольку Cloudflare Workers работают в настраиваемой среде выполнения в глобальной CDN Cloudflare, в настоящее время существуют некоторые ограничения. Одно из этих ограничений заключается в том, что в настоящее время Worker не может открыть произвольное TCP-соединение. Это означает, что мы не можем использовать Bolt для подключения к нашей базе данных Neo4j, однако мы можем использовать HTTP-запросы для подключения к Neo4j. Хорошо, что мы только что узнали о транзакционной конечной точке Cypher!

Конечная точка результатов выборов, персонализированная по местоположению

Если мы посмотрим на страницу примеров Cloudflare Workers, мы увидим пример worker для доступа к объекту Cloudflare. Объект Cloudflare прикреплен к объекту запроса, переданному нашему работнику, и будет содержать некоторую информацию, зависящую от местоположения.

addEventListener("fetch", event => {
  const data =
    event.request.cf !== undefined ?
      event.request.cf :
      { error: "The `cf` object is not available inside the preview." }

  return event.respondWith(
    new Response(JSON.stringify(data, null, 2), {
      headers: {
        "content-type": "application/json;charset=UTF-8"
      }
    })
  )
})

Вы можете подключиться к этой конечной точке и просмотреть соответствующую информацию на основе вашего запроса здесь: https://accessing-the-cloudflare-object.workers-sites-examples.workers.dev/

{
tlsExportedAuthenticator: {
 ...
},
tlsVersion: "TLSv1.3",
httpProtocol: "HTTP/2",
edgeRequestKeepAliveStatus: 1,
requestPriority: "weight=256;exclusive=1",
country: "US",
clientAcceptEncoding: "gzip, deflate, br",
clientTcpRtt: 27,
colo: "SEA",
tlsClientAuth: {
certIssuerDNLegacy: "",
certIssuerDN: "",
certIssuerDNRFC2253: "",
certSubjectDNLegacy: "",
certNotAfter: "",
certVerified: "NONE",
certFingerprintSHA1: "",
certSubjectDN: "",
certFingerprintSHA256: "",
certNotBefore: "",
certSerial: "",
certPresented: "0",
certSubjectDNRFC2253: ""
},
asn: 33588
}

Что нас больше всего интересует, так это значение colo. Согласно документации Cloudflare Workers, colo - это трехбуквенный код аэропорта центра обработки данных, в который попал запрос. Теперь вы понимаете, почему наша база данных Neo4j включает аэропорты!

Сейчас мы хотим найти аэропорт, который представляет центр обработки данных, в котором был обработан запрос, затем пройтись по нашему графику, чтобы найти узел состояния, пройти по всем округам в штате и вернуть данные результатов выборов для этих округов, чтобы мы Показываем пользователю результаты выборов только для его штата. Cloudflare также имеет службу geoip, поэтому мы могли бы включить ее, чтобы получить фактическую широту и долготу того, где был создан запрос, однако я использую бесплатный уровень Cloudflare и не имею доступа к этой функции.

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

MATCH (a:Airport {code: $colo})-[:IN_STATE]->(s:State)
RETURN {
    state: s.name, votes: s.votes, absenteeVotes: s.absentee_votes, bidenWin: s.biden_winner,
    trumpWin: s.trump_winner,
    counties:
    [(s)<-[:IN_STATE]-(c:County) | { name: c.name, trumpVotes: c.trump, bidenVotes: c.biden,
    pct_trump: (toFloat(c.trump) / (c.trump+c.biden)),
    pct_biden: (toFloat(c.biden) / (c.trump+c.biden))}]
} AS data

Выполняя этот запрос с использованием кода аэропорта ILG (для аэропорта Нью-Касл в Делавэре), мы видим результаты выборов для каждого округа Делавэр.

{
  "votes": 504010,
  "state": "Delaware",
  "trumpWin": false,
  "bidenWin": true,
  "absenteeVotes": 0,
  "counties": [
    {
      "pct_trump": 0.556867221214585,
      "name": "Sussex",
      "pct_biden": 0.44313277878541496,
      "bidenVotes": 56682,
      "trumpVotes": 71230
    },
    {
      "pct_trump": 0.31180177700618916,
      "name": "New Castle",
      "pct_biden": 0.6881982229938108,
      "bidenVotes": 195034,
      "trumpVotes": 88364
    },
    {
      "pct_trump": 0.47929547340493917,
      "name": "Kent",
      "pct_biden": 0.5207045265950608,
      "bidenVotes": 44552,
      "trumpVotes": 41009
    }
  ]
}

Теперь давайте создадим нового воркера с помощью консоли Cloudflare и выполним этот запрос Cypher для нашего экземпляра Neo4j с помощью HTTP API (помните, что в Cloudflare Worker мы в настоящее время не можем создавать произвольные TCP-соединения, поэтому с использованием одного из языков Bolt драйверы для Neo4j здесь не вариант)

Мы будем использовать fetch, чтобы сделать наш HTTP-запрос и вернуть объект JSON с соответствующими данными результатов выборов, в зависимости от местонахождения пользователя:

addEventListener('fetch', (event) => {
  const colo =
    (event.request && event.request.cf && event.request.cf.colo) || 'SFO';
  event.respondWith(handleRequest(event.request, colo));
});
/**
 * Respond to the request
 * @param {Request} request
 */
async function handleRequest(request, colo) {
  var myHeaders = new Headers();
  // add this header to enable Jolt format
  //myHeaders.append("Accept", "application/vnd.neo4j.jolt+json-seq");
  myHeaders.append('Accept', 'application/json');
  myHeaders.append('Content-Type', 'application/json');
  myHeaders.append('Authorization', 'Basic bmVvNGo6bGV0bWVpbg==');
  const statement = `MATCH (a:Airport {code: $colo})-[:IN_STATE]->(s:State)
    RETURN {
      state: s.name, votes: s.votes, absenteeVotes: s.absentee_votes, bidenWin: s.biden_winner, 
        trumpWin: s.trump_winner,
      counties: 
        [(s)<-[:IN_STATE]-(c:County) | { name: c.name, trumpVotes: c.trump, bidenVotes: c.biden, 
        pct_trump: (toFloat(c.trump) / (c.trump+c.biden)), 
        pct_biden: (toFloat(c.biden) / (c.trump+c.biden))}]
    } AS data`;
  var raw = JSON.stringify({
    statements: [{ statement, parameters: { colo } }]
  });
  var requestOptions = {
    method: 'POST',
    headers: myHeaders,
    body: raw,
    redirect: 'follow'
  };
  const response = await fetch(
    'https://elections.graph.zone/db/neo4j/tx/commit/',
    requestOptions
  );
  const result = await response.json();
  return new Response(
    JSON.stringify(
      (result &&
        result.results[0] &&
        result.results[0].data[0] &&
        result.results[0].data[0].row &&
        result.results[0].data[0].row[0]) || { colo },
      null,
      2
    ),
    {
      status: 200,
      headers: {
        'content-type': 'application/json;charset=UTF-8'
      }
    }
  );
}

Поскольку этот набор данных специфичен для США, если запрос попадает в край за пределами США, вместо результатов выборов мы просто возвращаем центр обработки данных этого края. Также обратите внимание, что в консоли тестирования Cloudflare объект cf недоступен по запросу, поэтому мы возвращаемся к коду аэропорта по умолчанию SFO.

Наш рабочий немедленно развертывается по всему миру. Если мы дойдем до конечной точки, теперь мы увидим данные о результатах выборов в зависимости от нашего местоположения. Вот как это будет выглядеть, если мы попадем в центр обработки данных в Сан-Франциско:

{
  "state": "California",
  "votes": 17175022,
  "absenteeVotes": 8420433,
  "trumpWin": false,
  "bidenWin": true,
  "counties": [
    {
      "pct_trump": 0.12156237491482494,
      "pct_biden": 0.8784376250851751,
      "name": "San Francisco",
      "bidenVotes": 656185,
      "trumpVotes": 90806
    }, ...
  ]
}

Вы можете попробовать это на этой конечной точке: https://shrill-bread-4677.graphstuff.workers.dev/, и вы должны увидеть результаты выборов, соответствующие вашему местоположению.

Ресурсы