Салют, хабровчане. В преддверии старта курса «AWS для разработчиков» подготовили перевод интересного материала.




Во многих юзкейсах, которые мы, как bakdata, видим на сайтах наших клиентов, релевантная информация сокрыта в связях между сущностями, например, при анализе отношений между пользователями, зависимостей между элементами или соединений между датчиками. Такие юзкейсы обычно моделируются на графе. Ранее в этом году Amazon выпустила новую графовую базу данных Neptune. В этом посте мы хотим поделиться нашими первыми идеями, хорошими практиками и тем, что может быть улучшено с течением времени.

Для чего нам понадобился Amazon Neptune


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

Набор данных MusicBrainz предоставляется в виде дампа CSV реляционной базы данных. Всего дамп содержит около 93 миллионов строк в 157 таблицах. В то время как некоторые из этих таблиц содержат основные данные, такие как исполнители, события, записи, релизы или треки, другие — таблицы связей — хранят отношения между исполнителями и записями, другими исполнителями или релизами и т.д… Они демонстрируют графовую структуру набора данных. При преобразовании набора данных в RDF-тройки мы получили примерно 500 миллионов экземпляров.

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

Настройка


Как и ожидалось, установка Amazon Neptune проста. Она довольно подробно задокументирована. Вы можете запустить графовую базу данных всего за несколько кликов. Тем не менее, когда дело доходит до более подробной конфигурации, нужную информацию найти трудно. Следовательно, мы хотим указать на один параметр конфигурации.


Скриншот конфигурации для групп параметров

Amazon утверждает, что Neptune фокусируется на транзакционных рабочих нагрузках с низкой задержкой, поэтому по умолчанию время ожидания запроса составляет 120 секунд. Мы, однако, протестировали множество аналитических юзкейсов, в которых мы регулярно достигали этого лимита. Это время ожидания можно изменить, создав новую группу параметров для Neptune и установив для neptune_query_timeout соответствующее ограничение.

Загрузка данных


Ниже мы подробно обсудим, как мы загружали данные MusicBrainz в Neptune.

Отношения в тройки


Во-первых, мы преобразовали данные MusicBrainz в RDF-тройки. Поэтому для каждой таблицы мы определили шаблон, который определяет, как каждый столбец представлен в тройке. В этом примере каждая строка из таблицы исполнителей сопоставлена с двенадцатью RDF-тройками.

<http://musicbrainz.foo/artist/${id}> <http://musicbrainz.foo/gid> "${gid}"^^<http://www.w3.org/2001/XMLSchema#string> .
 
 
<http://musicbrainz.foo/artist/${id}> <http://musicbrainz.foo/name> "${name}"^^<http://www.w3.org/2001/XMLSchema#string> .
 
<http://musicbrainz.foo/artist/${id}> <http://musicbrainz.foo/sort-name> "${sort_name}"^^<http://www.w3.org/2001/XMLSchema#string> .
 
<http://musicbrainz.foo/artist/${id}> <http://musicbrainz.foo/begin-date> "${begin_date_year}-${begin_date_month}-${begin_date_day}"^^xsd:<http://www.w3.org/2001/XMLSchema#date> .
 
<http://musicbrainz.foo/artist/${id}> <http://musicbrainz.foo/end-date> "${end_date_year}-${end_date_month}-${end_date_day}"^^xsd:<http://www.w3.org/2001/XMLSchema#date> .
 
<http://musicbrainz.foo/artist/${id}> <http://musicbrainz.foo/type> <http://musicbrainz.foo/artist-type/${type}> .
 
<http://musicbrainz.foo/artist/${id}> <http://musicbrainz.foo/area> <http://musicbrainz.foo/area/${area}> .
 
<http://musicbrainz.foo/artist/${id}> <http://musicbrainz.foo/gender> <http://musicbrainz.foo/gender/${gender}> .
 
<http://musicbrainz.foo/artist/${id}> <http://musicbrainz.foo/comment> "${comment}"^^<http://www.w3.org/2001/XMLSchema#string> .
 
<http://musicbrainz.foo/artist/${id}> <http://musicbrainz.foo/edits-pending> "${edits_pending}"^^<http://www.w3.org/2001/XMLSchema#int> .
 
<http://musicbrainz.foo/artist/${id}> <http://musicbrainz.foo/last-updated> "${last_updated}"^^<http://www.w3.org/2001/XMLSchema#dateTime> .
 
<http://musicbrainz.foo/artist/${id}> <http://musicbrainz.foo/ended> "${ended}"^^<http://www.w3.org/2001/XMLSchema#boolean> .


Массовая загрузка


Предлагаемый способ загрузки больших объемов данных в Neptune — это процесс массовой загрузки через S3. После загрузки ваших файлов троек на S3 вы начинаете загрузку с помощью POST-запроса. В нашем случае это заняло около 24 часов для 500 миллионов троек. Мы ожидали, что будет быстрее.

curl -X POST -H 'Content-Type: application/json' http://your-neptune-cluster:8182/loader -d '{
 
 
 "source" : "s3://your-s3-bucket",
 
 "format" : "ntriples",
 
 "iamRoleArn" : "arn:aws:iam::your-iam-user:role/NeptuneLoadFromS3",
 
 "region" : "eu-west-1",
 
 "failOnError" : "FALSE"
 
}'

Чтобы избежать этого длительного процесса каждый раз, когда мы запускаем Neptune, мы решили восстанавливать инстанс из снапшота, в котором эти тройки уже загружены. Запуск со снапшота происходит значительно быстрее, но все еще занимает около часа, пока Neptune не станет доступен для запросов.

При первоначальной загрузке троек в Neptune, мы столкнулись с различными ошибками.

{
 
 
 "errorCode" : "PARSING_ERROR",
 
 "errorMessage" : "Content after '.' is not allowed",
 
 "fileName" : [...],
 
 "recordNum" : 25
 
}

Некоторые из них были ошибками парсинга, как показано выше. На сегодняшний день мы до сих пор не выяснили, что именно пошло не так в этот момент. Немного больше деталей определенно помогли бы здесь. Эта ошибка произошла примерно для 1% вставленных троек. Но, что касается тестирования Neptune, мы приняли тот факт, что мы работаем только с 99% информации из MusicBrainz.

Даже если это не составляет труда для людей, знакомых с SPARQL, имейте в виду, что RDF-тройки должны быть аннотированы с явными типами данных, что опять-таки может вызывать ошибки.

Потоковая загрузка


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

Neptune поддерживает операторы ввода через SPARQL-запросы, как с необработанными данными, так и на основе выборок. Ниже мы обсудим оба подхода.

Одной из наших целей было вводить данные в потоковом режиме. Рассмотрим выпуск альбома в новой стране. С точки зрения MusicBrainz это означает, что для выпуска, который включает в себя альбомы, синглы, EP и т. д., новая запись добавляется в таблицу release-country. В RDF мы сопоставляем эту информацию с двумя новыми тройками.

INSERT DATA { <http://musicbrainz.foo/release-country/737041> <http://musicbrainz.foo/release> <http://musicbrainz.foo/release/435759> };INSERT DATA { <http://musicbrainz.foo/release-country/737041> <http://musicbrainz.foo/date-year> "2018"^^<http://www.w3.org/2001/XMLSchema#int> };

Другой целью было получить новые знания из графа. Предположим, мы хотим получить количество релизов, которые каждый артист опубликовал в своей карьере. Такой запрос довольно сложен и занимает более 20 минут в Neptune, поэтому нам нужно материализовать результат, чтобы повторно использовать эти новые знания в каком-то другом запросе. Поэтому мы добавляем тройки с этой информацией обратно в граф, вводя результат подзапроса.

INSERT {
 
 
  ?artist_credit <http://musicbrainz.foo/number-of-releases> ?number_of_releases
 
} WHERE {
 
  SELECT ?artist_credit (COUNT(*) as ?number_of_releases)
 
  WHERE {
 
     ?artist_credit <http://musicbrainz.foo/rdftype> <http://musicbrainz.foo/artist-credit> .
 
     ?release_group <http://musicbrainz.foo/artist-credit> ?artist_credit .
 
     ?release_group <http://musicbrainz.foo/rdftype> <http://musicbrainz.foo/release-group> .
 
     ?release_group <http://musicbrainz.foo/name> ?release_group_name .
 
  }
 
  GROUP BY ?artist_credit
 
}

Добавление одиночных троек в граф занимает несколько миллисекунд, в то время как время выполнения для вставки результата подзапроса зависит от времени выполнения самого подзапроса.

Несмотря на то, что мы не пользовались этим часто, Neptune также позволяет удалять тройки на основе выборок или явных данных, которые можно использовать для обновления информации.

SPARQL-запросы


Представляя предыдущую подвыборку, которая возвращает количество релизов для каждого исполнителя, мы уже ввели первый тип запроса, на который мы хотим ответить, используя Neptune. Построить запрос в Neptune несложно — отправьте POST-запрос в SPARQL-эндпоинт, как показано ниже:

curl -X POST --data-binary 'query=SELECT ?artist ?p ?o where {?artist <http://musicbrainz.foo/name> "Elton John" . ?artist ?p ?o . }' http://your-neptune-cluster:8182/sparql

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

SELECT
 
 
 ?artist_name ?year
 
 ?releases_in_year ?releases_up_year
 
 ?artist_type_name ?releases
 
 ?artist_gender ?artist_country_name
 
 ?artist_begin_date ?bands
 
 ?bands_in_year
 
WHERE {
 
 # Bands for each artist
 
 {
 
   SELECT
 
     ?year
 
     ?first_artist
 
     (group_concat(DISTINCT ?second_artist_name;separator=",") as ?bands)
 
     (COUNT(DISTINCT ?second_artist_name) AS ?bands_in_year)     
 
   WHERE {
 
     VALUES ?year {
 
       1960 1961 1962 1963 1964 1965 1966 1967 1968 1969
 
       1970 1971 1972 1973 1974 1975 1976 1977 1978 1979
 
       1980 1981 1982 1983 1984 1985 1986 1987 1988 1989
 
       1990 1991 1992 1993 1994 1995 1996 1997 1998 1999
 
       2000 2001 2002 2003 2004 2005 2006 2007 2008 2009
 
       2010 2011 2012 2013 2014 2015 2016 2017 2018
 
     }   
 
     ?first_artist <http://musicbrainz.foo/name> "Elton John" .
 
     ?first_artist <http://musicbrainz.foo/rdftype> <http://musicbrainz.foo/artist> .
 
     ?first_artist <http://musicbrainz.foo/type> ?first_artist_type .
 
     ?first_artist <http://musicbrainz.foo/name> ?first_artist_name .
 

 
 
     ?second_artist <http://musicbrainz.foo/rdftype> <http://musicbrainz.foo/artist> .
 
     ?second_artist <http://musicbrainz.foo/type> ?second_artist_type .
 
     ?second_artist <http://musicbrainz.foo/name> ?second_artist_name .
 
     optional { ?second_artist <http://musicbrainz.foo/begin-date-year> ?second_artist_begin_date_year . }
 
     optional { ?second_artist <http://musicbrainz.foo/end-date-year> ?second_artist_end_date_year . }
 

 
 
     ?l_artist_artist <http://musicbrainz.foo/entity0> ?first_artist .
 
     ?l_artist_artist <http://musicbrainz.foo/entity1> ?second_artist .
 
     ?l_artist_artist <http://musicbrainz.foo/link> ?link .
 

 
 
     optional { ?link <http://musicbrainz.foo/begin-date-year> ?link_begin_date_year . }
 
     optional { ?link <http://musicbrainz.foo/end-date-year> ?link_end_date_year . }
 

 
 
     FILTER (!bound(?link_begin_date_year) || ?link_begin_date_year <= ?year)
 
     FILTER (!bound(?link_end_date_year) || ?link_end_date_year >= ?year)
 
     FILTER (!bound(?second_artist_begin_date_year) || ?second_artist_begin_date_year <= ?year)
 
     FILTER (!bound(?second_artist_end_date_year) || ?second_artist_end_date_year >= ?year)
 
     FILTER (?first_artist_type NOT IN (<http://musicbrainz.foo/artist-type/2>, <http://musicbrainz.foo/artist-type/5>, <http://musicbrainz.foo/artist-type/6>))
 
     FILTER (?second_artist_type IN (<http://musicbrainz.foo/artist-type/2>, <http://musicbrainz.foo/artist-type/5>, <http://musicbrainz.foo/artist-type/6>))
 
   }
 
   GROUP BY ?first_artist ?year
 
 }
 
 # Releases up to a year
 
 {
 
   SELECT
 
     ?artist
 
     ?year
 
     (group_concat(DISTINCT ?release_name;separator=",") as ?releases)
 
     (COUNT(*) as ?releases_up_year)
 
   WHERE {
 
     VALUES ?year {
 
       1960 1961 1962 1963 1964 1965 1966 1967 1968 1969
 
       1970 1971 1972 1973 1974 1975 1976 1977 1978 1979
 
       1980 1981 1982 1983 1984 1985 1986 1987 1988 1989
 
       1990 1991 1992 1993 1994 1995 1996 1997 1998 1999
 
       2000 2001 2002 2003 2004 2005 2006 2007 2008 2009
 
       2010 2011 2012 2013 2014 2015 2016 2017 2018 
 
     }
 

 
 
     ?artist <http://musicbrainz.foo/name> "Elton John" .
 

 
 
     ?artist_credit_name <http://musicbrainz.foo/artist-credit> ?artist_credit .
 
     ?artist_credit_name <http://musicbrainz.foo/rdftype> <http://musicbrainz.foo/artist-credit-name> .
 
     ?artist_credit_name <http://musicbrainz.foo/artist> ?artist .
 
     ?artist_credit <http://musicbrainz.foo/rdftype> <http://musicbrainz.foo/artist-credit> .
 

 
 
     ?release_group <http://musicbrainz.foo/artist-credit> ?artist_credit .
 
     ?release_group <http://musicbrainz.foo/rdftype> <http://musicbrainz.foo/release-group> .
 
     ?release_group <http://musicbrainz.foo/name> ?release_group_name .
 
     ?release <http://musicbrainz.foo/release-group> ?release_group .
 
     ?release <http://musicbrainz.foo/name> ?release_name .
 
     ?release_country <http://musicbrainz.foo/release> ?release .
 
     ?release_country <http://musicbrainz.foo/date-year> ?release_country_year .
 

 
 
     FILTER (?release_country_year <= ?year)
 
   }
 
   GROUP BY ?artist ?year
 
 }
 
 # Releases in a year
 
 {
 
   SELECT ?artist ?year (COUNT(*) as ?releases_in_year)
 
   WHERE {
 
     VALUES ?year {
 
       1960 1961 1962 1963 1964 1965 1966 1967 1968 1969
 
       1970 1971 1972 1973 1974 1975 1976 1977 1978 1979
 
       1980 1981 1982 1983 1984 1985 1986 1987 1988 1989
 
       1990 1991 1992 1993 1994 1995 1996 1997 1998 1999
 
       2000 2001 2002 2003 2004 2005 2006 2007 2008 2009
 
       2010 2011 2012 2013 2014 2015 2016 2017 2018 
 
     }
 

 
 
     ?artist <http://musicbrainz.foo/name> "Elton John" .
 

 
 
     ?artist_credit_name <http://musicbrainz.foo/artist-credit> ?artist_credit .
 
     ?artist_credit_name <http://musicbrainz.foo/rdftype> <http://musicbrainz.foo/artist-credit-name> .
 
     ?artist_credit_name <http://musicbrainz.foo/artist> ?artist .
 
     ?artist_credit <http://musicbrainz.foo/rdftype> <http://musicbrainz.foo/artist-credit> .
 

 
 
     ?release_group <http://musicbrainz.foo/artist-credit> ?artist_credit .
 
     ?release_group <http://musicbrainz.foo/rdftype> <http://musicbrainz.foo/release-group> .
 
     ?release_group <http://musicbrainz.foo/name> ?release_group_name .
 
     ?release <http://musicbrainz.foo/release-group> ?release_group .
 
     ?release_country <http://musicbrainz.foo/release> ?release .
 
     ?release_country <http://musicbrainz.foo/date-year> ?release_country_year .
 

 
 
     FILTER (?release_country_year = ?year)
 
   }
 
   GROUP BY ?artist ?year
 
 }
 
 # Master data
 
 {
 
   SELECT DISTINCT ?artist ?artist_name ?artist_gender ?artist_begin_date ?artist_country_name
 
   WHERE {
 
     ?artist <http://musicbrainz.foo/name> ?artist_name .
 
     ?artist <http://musicbrainz.foo/name> "Elton John" .
 
     ?artist <http://musicbrainz.foo/gender> ?artist_gender_id .
 
     ?artist_gender_id <http://musicbrainz.foo/name> ?artist_gender .
 
     ?artist <http://musicbrainz.foo/area> ?birth_area .
 
     ?artist <http://musicbrainz.foo/begin-date-year> ?artist_begin_date.
 
     ?birth_area <http://musicbrainz.foo/name> ?artist_country_name .
 

 
 
     FILTER(datatype(?artist_begin_date) = xsd:int)
 
   }

Из-за сложности такого запроса мы могли выполнять только точечные запросы для конкретного исполнителя, например Элтона Джона, но не для всех артистов. Neptune, похоже, не оптимизирует такой запрос, опуская фильтры в подвыборки. Поэтому каждую выборку приходится фильтровать вручную по имени исполнителя.

Neptune имеет как почасовую, так и оплату за каждую операцию ввода-вывода. Для нашего тестирования мы использовали самый минимальный инстанс Neptune, который стоит $0,384/час. В случае запроса выше, который вычисляет профиль для одного исполнителя, Amazon взимает с нас несколько десятков тысяч операций ввода-вывода, что подразумевает затраты в размере $0.02.

Вывод


Во-первых, Amazon Neptune держит большинство своих обещаний. Будучи управляемым сервисом, это графовая база данных, которая чрезвычайно проста в установке и может быть запущена без большого количества настроек. Вот наши пять ключевых выводов:

  • Массовая загрузка простая, но медленная. Но она может усложниться из-за сообщений об ошибках, которые не очень помогают
  • Потоковая загрузка поддерживает все, что мы ожидали, и была достаточно быстрой
  • Запросы просты, но недостаточно интерактивны для выполнения аналитических запросов
  • SPARQL-запросы должны быть оптимизированы вручную
  • Платежи Amazon сложно оценить, так как сложно оценить объем данных, отсканированных по SPARQL-запросу.

На этом все. Записывайтесь на бесплатный вебинар по теме «Балансировка нагрузки».