Пример последовательного построения SPARQL-запроса

Материал из Российская школа по Semantic Web
Перейти к: навигация, поиск
Пример последовательного построения SPARQL-запроса
Номер 5
Лектор Катков Ю.В.
Prerequisites RDF, SQL, регулярные выражения, HTTP, HTTP-заголовки, RESTful API, вики, шаблоны MediaWiki
Замечание дописать распространенный пример
Аннотация В начале лекции рассказывается о DBpedia и её SPARQL-интерфейсе. Затем мы переходим на написанию запросов нарастающей сложности.


Для того, чтобы как следует понять специфику работы со SPARQL, полезно решить несколько примеров. Здесь мы возьмем пример одного запроса и последовательно покажем, как правильно построить его SPARQL-представление. Вот наша задача:

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


Это довольно простая задачка, поскольку RDF-данные о компаниях встречаются в нескольких источниках. Можно было бы поискать списки компаний в архивах открытых государственных данных США и Британии, но мы поступим проще - будем использовать данные, взятые из Википедии.

Содержание

DBpedia

Логотип DBpedia

Каждый SPARQL-запрос посылается не в неопределённую неведомую даль Всемирной паутины, а напротив, задается по отношению к строго определённому множеству RDF-данных. Мы будем запрашивать данные из DBpedia — одного из наиболее крупных центров информации в Linked Data. DBpedia — это проект создания структурированной и постоянно обновляющейся копии Википедии, представленной в языке RDF. Для того, чтобы создать DBpedia, разработчики написали множество программ-червей, которые день и ночь вытягивают из википедии всю структурированную информацию, которую она может предложить: Категории, Перенаправления и самое главное, Информационные боксы. Большая часть данных DBPedia — это переведенные в RDF информационные боксы, которые располагаются на многих страницах Википедии.

Автоматические скрипты регулярно сканируют Википедию и стараются грамотно преобразовать текстовое содержимое Информационных боксов в структурированные RDF-данные - даты, числа, строки и уникальные идентификаторы (URI).

Взгляните, например, на страницу, посвященную Леонардо да Винчи. Справа от текста находится тот самый информационный бокс, который будет преобразован в RDF-тройки.

Dbpedia leonardo infobox rdf.png

Графически эти данные могут быть отображены следующим образом:

Граф, полученный из Википедии. Кружками обозначены идентификаторы ресурсов, прямоугольниками — литералы

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

Вопросы, которые стоит себе задать

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

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


У любознательного читателя сразу должно возникнуть несколько вопросов.

Прямо сейчас мы дедим ответ лишь на последний вопрос. Молодыми у нас будут называться все деятели искусства, которые родились после 1939 года. [1]

Построение запроса

Направляемся в SPARQL-точку доступа DBPedia. Для ввода запросов можно воспользоваться несколькими сервисами. Поначалу лучше работать с официальной точкой доступа. Все, что в ней есть - форма для ввода запросов, переключатель формата вывода и кнопка ввода. По умолчанию результаты официальная точка выдает в виде простой html-таблицы с простым текстом — вас ничто не будет отвлекать.

После можно попробовать SNORQL-конструктор запросов, имеющий более дружественный интерфейс и встроенный RDF-браузер или iSPARQL, в котором есть даже графический конструктор (QBE) [2].

Приступим же.

Самый простой запрос

Как мы уже знаем, в RDF-хранилищах все данные представлены в виде троек субъект — свойство — значение (также говорят "субъект — предикат — объект", "подлежащее — сказуемое — дополнение", "откуда — свойство — куда"). Если нам хочется получить субъекты, у которых есть заданные свойства с заданными значениями, все, что нам нужно сделать — это поставить на место субъекта переменную. Точка доступа заполнит переменную всеми субъектами, от которых идут указанные нами свойства к указанным нами значениям.

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

SELECT * WHERE
 { 
   ?a <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://dbpedia.org/ontology/Company> .
 }
 LIMIT 100

Звездочка после SELECT означает, что мы хотим увидеть значения всех переменных, упомянутых в запросе. В данном запросе у нас всего лишь одна переменная ?a. В SPARQL все переменные начинаются с вопросительного знака. Как видите, запрос заключен в фигурные скобки, а ограничение на вывод результата (модификатор LIMIT) стоит после него.

В этом запросе мы используем полный URL класса Компания: http://dbpedia.org/ontology/Company и полный URL свойства Тип из языка RDF: http://www.w3.org/1999/02/22-rdf-syntax-ns#type. Каждый раз, когда вы употребляете в запросе URL, необходимо брать его в треугольные скобки. Таким образом, наш запрос можно интерпретировать следующим образом: "вывести 100 субъектов, обладающих свойством Тип со значением Компания." Обратите внимание, что порядок следования важен: нельзя просто переставить местами переменные и URL'ы.

Добавляем префиксы

В RDF все объекты, свойства и классы обозначаются своими идентификаторами URL. Однако, если мы всегда будем использовать полную URL-запись в наших SPARQL-запросах, их будет сложнее читать и понимать. Количество пересылаемых на SPARQL-точку данных также увеличится, что может быть неприятным.

К счастью в SPARQL, как и в RDF есть механизм сокращения URL'ов. В начале запроса можно объявить используемые аббревиатуры для ресурсов. Такие аббревиатуры называются префиксами и объявляются с помощью конструкции PREFIX имя_префикса : <сокращаемый URL>:

PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
PREFIX dbpedia-owl: <http://dbpedia.org/ontology/>

SELECT * WHERE
 { 
   ?a rdf:type dbpedia-owl:Company .
 }
 LIMIT 100

Обратите внимание, что префикс RDF оканчивается символом решетки, а dbpedia-owl — прямым слэшем. Если вы потеряете последний символ, то полученные URL'ы будут неправильными, а SPARQL-точка даже не выдаст ошибки. Механизм префиксов работает так, что значение до двоеточия просто приклеивается к значению после двоеточия. Если забыть прямой слэш в конце, точка доступа сделает нам URL http://dbpedia.org/ontologyCompany, не увидит никого, связанного с этим идентификатором и вернёт нулевой ответ.

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

Больше одного условия запроса

Проверьте, что запрос работает как надо. Он выдает идентификаторы (URL) компаний. Нам же надо не идентификаторы, а названия, поэтому давайте добавим к нашему запросу дополнительное условие:

PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
PREFIX dbpedia-owl: <http://dbpedia.org/ontology/>
SELECT * WHERE
{ 
  ?a rdf:type            dbpedia-owl:Company .
  ?a dbpprop:companyName ?corporation        .
}
LIMIT 100

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

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

Точка с запятой

PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
PREFIX dbpedia-owl: <http://dbpedia.org/ontology/>
SELECT * WHERE
{
  ?a rdf:type            dbpedia-owl:Company ;
     dbpprop:companyName ?corporation        .
}
LIMIT 100

Смысл запроса остается прежним.

Добавление основателей

Основатель IBM товарищ Ватсон

Если вы помните, нас интересуют лишь компании, основанные представителями культуры и искусства, вывести имена этих основателей их фотографии. Сначала стоит взглянуть на то, как в объекте IBM описан основатель Уотсон. Похоже, там используется свойство dbpedia-owl:foundedBy. Значит для вывода URL основателя нам достаточно написать вот такой запрос:

PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
PREFIX dbpedia-owl: <http://dbpedia.org/ontology/>
SELECT * WHERE
{
  ?companyURL rdf:type            dbpedia-owl:Company ;
              dbpprop:companyName ?corporation        ;
              dbpedia-owl:foundedBy ?founderURL       . 
}
LIMIT 100

Теперь в переменную ?founderURL будет записан URL основателя. Еще вы должны были заметить, что мы заменили имя переменной ?a на ?companyURL. Это полезно, потому что количество переменных растет и в них скоро можно запутаться.

Объединение условий запроса

Теперь стоит указать, что основатели должны быть или деятелями культуры, или деятелями искусства. Сначала найдем в DBpedia какого-нибудь деятеля искусства и посмотрим, что его отличает от других людей. Найдем в фасетном поиске Рэя Брэдбери и еще нескольких писателей и музыкантов и обнаружим, что у всех них свойство rdf:type указывает в том числе на dbpedia-owl:Artist. Модифицируем наш запрос, отсеев всех, кто не Artist:

PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
PREFIX dbpedia-owl: <http://dbpedia.org/ontology/>
SELECT * WHERE
{
  ?companyURL rdf:type               dbpedia-owl:Company;
              dbpprop:companyName   ?corporation        ;
              dbpedia-owl:foundedBy ?founderURL         . 
  ?founderURL  rdf:type              dbpedia-owl:Artist  .
}
LIMIT 100

Хотелось бы теперь включить в этот запрос в том числе и актёров и представителей боевых искусств — в конце концов, они тоже деятели культуры. Актеры имеют тип Actor, мастера боевых искусств имеют тип MartialArtist или свойство occupation со значение Martial_arts. Подумайте, почему нам не написать запрос следующего вида?

#Это плохой запрос. Он ничего не возвратит
PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
PREFIX dbpedia-owl: <http://dbpedia.org/ontology/>
SELECT * WHERE
{
  ?companyURL rdf:type               dbpedia-owl:Company      ;
              dbpprop:companyName   ?corporation              ;
              dbpedia-owl:foundedBy ?founderURL               . 
  ?founderURL  rdf:type                dbpedia-owl:Artist        ;
                rdf:type               dbpedia-owl:Actor         ;
                rdf:type               dbpedia-owl:MartialArtist ;
                dbpedia-owl:occupation dbpedia:Martial_arts      .
}
LIMIT 100

Такой запрос выдает нулевой результат, ведь он желает, чтобы ?founderURL имел все три типа — и Artist, и Actor, и MartialArtist и вдобавок имел свойство occupation равное Martial_arts. Увы, о таких универсалах DBpedia не осведомлена. И вообще, нам же нужна связка ИЛИ, а не И. Тут приходит на помощь конструкция UNION.

PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
PREFIX dbpedia-owl: <http://dbpedia.org/ontology/>
SELECT * WHERE
{
  ?companyURL rdf:type               dbpedia-owl:Company      ;
              dbpprop:companyName   ?corporation              ;
              dbpedia-owl:foundedBy ?founderURL               . 
  { ?founderURL rdf:type  dbpedia-owl:Artist. }
  UNION
  { ?founderURL rdf:type dbpedia-owl:Actor. }
  UNION
  { ?founderURL rdf:type dbpedia-owl:MartialArtist. }
  UNION
  { ?founderURL dbpedia-owl:occupation dbpedia:Martial_arts.}     
}
LIMIT 100

Фильтрация и представление литералов

Ура! Нам удалось вместить в один вопрос и Чарли Чаплина, и Чака Норриса. Однако если Чака мы условились считать молодым, о Чарли такого точно не скажешь. Стоит понять, как можно отфильтровать людей, родившихся после 1939 года, а остальных из результатов запроса исключить.

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

PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
PREFIX dbpedia-owl: <http://dbpedia.org/ontology/>
PREFIX xsd: <http://www.w3.org/2001/XMLSchema#> 
SELECT * WHERE
{
  ?companyURL rdf:type               dbpedia-owl:Company      ;
              dbpprop:companyName   ?corporation              ;
              dbpedia-owl:foundedBy ?founderURL               . 
  ?founderURL dbpedia-owl:birthDate ?founderBirth             .
  FILTER (?founderBirth > "1940-03-10"^^xsd:date )            .
  { ?founderURL rdf:type  dbpedia-owl:Artist. }
  UNION
  { ?founderURL rdf:type dbpedia-owl:Actor. }
  UNION
  { ?founderURL rdf:type dbpedia-owl:MartialArtist. }
  UNION
  { ?founderURL dbpedia-owl:occupation dbpedia:Martial_arts.}
}
LIMIT 100

На этом месте иногда возникают проблемы с пониманием запроса. Обратите внимание, что переменная ?founderURL является значением свойства dbpedia-owl:foundedBy для субъекта ?companyURL. И эта же переменная сама является субъектом, у которого есть дата рождения dbpedia-owl:birthDate.

Другой момент, на который стоит обратить внимание - это представление даты. Здесь используется тип данных date из XML-schema, и указывается этот тип явным образом, следуя за двумя крышечками ^^.

Добавление новых полей для вывода

Основатель компании World Combat League

Давайте запросим все данные об основателе, подцепив еще одну тройку с участием той же самой переменной ?founderURL. Откуда мы знаем, как DBpedia обозначаются имена, описания фотографии и даты рождения? Мы просто посмотрим на то, как они сделаны у любого человека, например основателя IBM или у нашего молодого Чака Норриса, выданного в прошлом запросе.


PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
PREFIX dbpedia-owl: <http://dbpedia.org/ontology/>
PREFIX xsd: <http://www.w3.org/2001/XMLSchema#> 
SELECT * WHERE
{
  ?companyURL rdf:type               dbpedia-owl:Company ;
              dbpprop:companyName   ?corporation         ;
              dbpedia-owl:foundedBy ?founderURL          . 
   ?founderURL dbpedia-owl:birthDate ?founderBirth       ;
               foaf:name             ?founderName        ;
               dbpedia-owl:abstract  ?founderDescription ;
               foaf:depiction        ?founderPicture     .
  FILTER (?founderBirth > "1940-03-10"^^xsd:date )       .
  { ?founderURL rdf:type  dbpedia-owl:Artist. }
  UNION
  { ?founderURL rdf:type dbpedia-owl:Actor. }
  UNION
  { ?founderURL rdf:type dbpedia-owl:MartialArtist. }
  UNION
  { ?founderURL dbpedia-owl:occupation dbpedia:Martial_arts.}
}
LIMIT 100

Такой запрос выдает нам все URL'ы компаний, их названия, URL-ы основателей, их имена, описания портреты и даты рождения.

Довольно много полей, которые уже нам не интересны, например URL-ы компаний и URL-адреса основателей. Давайте явно укажем список интересных нам переменных:

PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
PREFIX dbpedia-owl: <http://dbpedia.org/ontology/>
PREFIX xsd: <http://www.w3.org/2001/XMLSchema#> 
SELECT ?corporation ?founderName ?founderPicture ?founderBirth ?founderDescription WHERE
{
  ?companyURL rdf:type               dbpedia-owl:Company ;
              dbpprop:companyName   ?corporation         ;
              dbpedia-owl:foundedBy ?founderURL          . 
   ?founderURL dbpedia-owl:birthDate ?founderBirth       ;
               foaf:name             ?founderName        ;
               dbpedia-owl:abstract  ?founderDescription ;
               foaf:depiction        ?founderPicture     .
  FILTER (?founderBirth > "1940-03-10"^^xsd:date )       .
  { ?founderURL rdf:type  dbpedia-owl:Artist. }
  UNION
  { ?founderURL rdf:type dbpedia-owl:Actor. }
  UNION
  { ?founderURL rdf:type dbpedia-owl:MartialArtist. }
  UNION
  { ?founderURL dbpedia-owl:occupation dbpedia:Martial_arts.}
}
LIMIT 100

Фильтрация строковых литералов по языку

Пока что

PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
PREFIX dbpedia-owl: <http://dbpedia.org/ontology/>
PREFIX xsd: <http://www.w3.org/2001/XMLSchema#> 
SELECT ?corporation ?founderName ?founderPicture ?founderBirth ?founderDescription WHERE
{
  ?companyURL rdf:type               dbpedia-owl:Company ;
              dbpprop:companyName   ?corporation         ;
              dbpedia-owl:foundedBy ?founderURL          . 
   ?founderURL dbpedia-owl:birthDate ?founderBirth       ;
               foaf:name             ?founderName        ;
               dbpedia-owl:abstract  ?founderDescription ;
               foaf:depiction        ?founderPicture     .
  FILTER (?founderBirth > "1940-03-10"^^xsd:date )       .
  FILTER langMatches( lang(?corporation), "EN" )         .
  FILTER langMatches( lang(?founderDescription), "EN" )  .
  { ?founderURL rdf:type  dbpedia-owl:Artist. }
  UNION
  { ?founderURL rdf:type dbpedia-owl:Actor. }
  UNION
  { ?founderURL rdf:type dbpedia-owl:MartialArtist. }
  UNION
  { ?founderURL dbpedia-owl:occupation dbpedia:Martial_arts.}
}
LIMIT 100

Опциональные поля

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

SELECT ?corporation ?founderName ?founderPicture ?founderBirth ?founderDescription WHERE
{
  ?companyURL rdf:type               dbpedia-owl:Company ;
              dbpprop:companyName   ?corporation         ;
              dbpedia-owl:foundedBy ?founderURL          .
  ?founderURL dbpedia-owl:birthDate ?founderBirth       ;
              foaf:name             ?founderName        ;
              dbpedia-owl:abstract  ?founderDescription .
  OPTIONAL 
  {
     ?founderURL foaf:depiction        ?founderPicture   .
  }
  FILTER (?founderBirth > "1940-03-10"^^xsd:date )       .
  FILTER langMatches( lang(?corporation), "EN" )         .
  FILTER langMatches( lang(?founderDescription), "EN" )  .
  { ?founderURL rdf:type  dbpedia-owl:Artist. }
  UNION
  { ?founderURL rdf:type dbpedia-owl:Actor. }
  UNION
  { ?founderURL rdf:type dbpedia-owl:MartialArtist. }
  UNION
  { ?founderURL dbpedia-owl:occupation dbpedia:Martial_arts.}
}
LIMIT 100

Взгляните на результаты запроса - их стало значительно больше, а в поле фотографии (?founderPicture) появились пустые ячейки. Если мы загружаем условия запроса в OPTIONAL-блок, эти условия становятся необязательными.


Удаление дублирования

Ключевое слово DISTINCT помогает не выводить одинаковые результаты запроса. Добавим его для того, чтобы не удалять дубликаты вручную: Идем дальше - пора добавить в результаты нашего поиска слоган и стоимость. Мы выяснили что для IBM эти свойства называются assets и slogan

SELECT DISTINCT ?corporation ?founderName ?founderPicture ?founderBirth ?founderDescription WHERE
{
  ?companyURL rdf:type               dbpedia-owl:Company ;
              dbpprop:companyName   ?corporation         ;
              dbpedia-owl:foundedBy ?founderURL          .
  ?founderURL dbpedia-owl:birthDate ?founderBirth       ;
              foaf:name             ?founderName        ;
              dbpedia-owl:abstract  ?founderDescription .
  OPTIONAL 
  {
     ?founderURL foaf:depiction        ?founderPicture   .
  }
  FILTER (?founderBirth > "1940-03-10"^^xsd:date )       .
  FILTER langMatches( lang(?corporation), "EN" )         .
  FILTER langMatches( lang(?founderDescription), "EN" )  .
  { ?founderURL rdf:type  dbpedia-owl:Artist. }
  UNION
  { ?founderURL rdf:type dbpedia-owl:Actor. }
  UNION
  { ?founderURL rdf:type dbpedia-owl:MartialArtist. }
  UNION
  { ?founderURL dbpedia-owl:occupation dbpedia:Martial_arts.}
}
LIMIT 100

Последние штрихи. Столкновение с реальной жизнью

#Данный запрос не будет работать, из-за особенностей бытия
SELECT DISTINCT ?corporation ?founderName ?founderPicture ?founderBirth ?founderDescription WHERE
{
  ?companyURL rdf:type               dbpedia-owl:Company ;
              dbpprop:companyName   ?corporation         ;
              dbpedia-owl:foundedBy ?founderURL          ;
              dbpprop:slogan        ?companySlogan       ; 
              dbpprop:assets        ?companyAssets       .
  ?founderURL dbpedia-owl:birthDate ?founderBirth       ;
              foaf:name             ?founderName        ;
              dbpedia-owl:abstract  ?founderDescription .
  OPTIONAL 
  {
     ?founderURL foaf:depiction        ?founderPicture   .
  }
  FILTER (?founderBirth > "1940-03-10"^^xsd:date )       .
  FILTER langMatches( lang(?corporation), "EN" )         .
  FILTER langMatches( lang(?founderDescription), "EN" )  .
  { ?founderURL rdf:type  dbpedia-owl:Artist. }
  UNION
  { ?founderURL rdf:type dbpedia-owl:Actor. }
  UNION
  { ?founderURL rdf:type dbpedia-owl:MartialArtist. }
  UNION
  { ?founderURL dbpedia-owl:occupation dbpedia:Martial_arts.}
}
LIMIT 100
Несмотря на конфуз в конце, надеюсь, вы остались довольны! До новых встреч

Не получилось? Давайте разберемся, в чем дело. Запрос составлен корректно, однако при поптыке иго выполнить выдается ошибка

Virtuoso 42000 Error SQ200: The memory pool size 80019456 reached the 
limit 80000000 bytes, try to increase the MaxMemPoolSize ini setting

Как видите, DBpedia не хватило памяти на обработку такого запроса. Виной тому в первую очередь UNION'ы, которые делают запрос в четыре раза больше исходного размера. Теперь нужно либо разбить запрос на подзапросы, либо скачать дампы DBpedia к себе на локальный сервер и развернуть систему с RDF-хранилищем. Всё-таки dbpedia.org — это лишь ознакомительный сервис.

Примечания

  1. Сначала мы хотели задать более строгий порог вхождения, но тогда в запрос не вошел бы актер Чак Норрис, которому в 2011 стукнул 71 год.
  2. Не слишком расчитывайте на этот конструктор при формировании запросов, однако в некоторых случаях вы можете визуализировать ваш запрос (что, скорее всего, будет сопровождаться ужасной бранью со стороны сервиса).
Личные инструменты
Пространства имён
Варианты
Действия
Навигация
Инструменты