Страница, написанная для человека, часто остаётся тёмным лесом для машины. Поисковая система угадывает тематику по ключевым словам, но связать товар с категорией, производителем и серией она может только на ощупь, с оглядкой на соседние страницы. Технологии RDF и OWL меняют саму ткань контента: каждая сущность получает точный адрес, каждая связь получает имя, и сайт превращается в граф, который компьютер читает так же уверенно, как человек читает абзац. Разберём, из чего собираются такие графы, чем полноценная онтология отличается от простого словаря меток и что связность данных даёт проекту на практике.
Триплеты как минимальная единица машинного описания смысла
Resource Description Framework, коротко RDF, описывает мир короткими фразами из трёх элементов: субъект, предикат, объект. Редакцию RDF 1.1 консорциум W3C утвердил в 2014 году, и с тех пор запись факта выглядит одинаково в любой системе. Субъектом выступает IRI, то есть международный адрес ресурса, либо безымянный узел. Предикатом всегда служит IRI. Объектом бывает IRI, литерал или снова безымянный узел. Формула проста: субъект это то, о чём говорится, предикат это название связи, объект это значение. Триплет "товар 42 имеет название механическая клавиатура" не требует угадываний, у каждого звена есть точный адрес.
В базовой записи N-Triples три факта о каталоге выглядят так:
<https://example.org/product/42> <https://example.org/vocab/hasName> "Механическая клавиатура" .
<https://example.org/product/42> <https://example.org/vocab/hasCategory> <https://example.org/category/64> .
<https://example.org/category/64> <https://example.org/vocab/hasName> "Периферия" .
Три строки уже образуют маршрут: от товара к разделу каталога и дальше к названию раздела. Так рождается направленный помеченный граф, где у каждого узла и каждого ребра есть адрес, который не спутаешь ни с опечаткой, ни с перестановкой слов. Порядок триплетов не имеет значения, любую строку можно удалить или добавить без поломки остального графа, в этом коренное отличие от жёсткой табличной схемы. Литералы при необходимости получают тип из пространства имён xsd: целое число, дата, десятичная дробь, строка, и парсер тогда понимает, что 5490 это сумма, а не текст. Безымянные узлы с префиксом подчёркивания позволяют говорить о промежуточных сущностях, которым пока не нужен собственный адрес, например о конкретной партии товара внутри партии поставки. Стандарт фиксирует и медиатипы: сериализация Turtle отдаётся с типом text/turtle, JSON-LD с типом application/ld+json, базовый N-Triples с типом application/n-triples, поэтому принимающая сторона всегда знает, каким парсером читать поток, а граф можно передавать между сервисами без устных договорённостей.
Синтаксис Turtle для читаемой записи графа фактов
Полные адреса в каждой строке быстро утомляют глаз, поэтому спецификация W3C определяет компактную запись Turtle с префиксами. В шапке файла один раз объявляются сокращения, дальше они работают как переменные. Ключевое слово "a" подставляется вместо rdf:type, точка с запятой продолжает перечисление свойств у одного субъекта, запятая перечисляет объекты у одного свойства, точка закрывает утверждение.
@prefix ex: <https://example.org/vocab/> .
@prefix shop: <https://example.org/product/> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
shop:42 a ex:Product ;
ex:name "Механическая клавиатура" ;
ex:price "5490"^^xsd:decimal ;
ex:hasCategory ex:peripherals .
ex:peripherals a ex:Category ;
ex:name "Периферия" .
Файл читается сверху вниз любым текстовым редактором, построчный diff в системе контроля версий показывает, какой именно факт изменился, и ревью онтологии превращается в обычную задачу для кода. Turtle понимают все крупные инструменты: Apache Jena, RDFLib, GraphDB, редактор Protégé, поэтому онтологию можно вести в текстовом репозитории рядом с исходниками продукта и не привязываться к конкретному вендору. Когда словарь растёт, префиксы остаются читаемыми, а полный адрес всегда восстановим из шапки.
JSON-LD и словарь schema.org в разметке страниц сайта
Для страницы браузеру нужен не отдельный файл, а встроенный блок разметки, и эту роль выполняет JSON-LD, рекомендация W3C версии 1.1 от июля 2020 года. Синтаксис обычный JSON, а ключ @context играет роль шапки из Turtle: короткие имена разворачиваются в полные адреса словаря. Придумывать словарь с нуля почти никогда не требуется, потому что с 2011 года развивается общий словарь schema.org, запущенный совместно поисковыми системами Google, Bing и Yahoo, позже к нему подключился Яндекс. Сотни типов, от Product и Article до Organization и BreadcrumbList, покрывают большинство сценариев сайта.
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "Product",
"name": "Механическая клавиатура",
"category": "Периферия",
"offers": {
"@type": "Offer",
"price": "5490",
"priceCurrency": "RUB",
"availability": "https://schema.org/InStock"
}
}
</script>
Поисковый робот получает не догадку по ключевым словам, а прямое утверждение о типе сущности, цене и наличии, и за это выдача отвечает расширенным сниппетом с ценой и хлебными крошками. Внутри компании тот же JSON-LD служит интерфейсом между контент-командой и фронтендом: редактор заполняет поля в системе управления контентом, шаблон собирает из них разметку, а валидатор проверяет блок перед публикацией. Параллельно существует и встроенная разметка RDFa, где связи записываются атрибутами прямо в теги, однако блочный вариант живёт в одном месте страницы, не размазан по шаблону и проще проходит ревью. Расширение словаря сводится к добавлению поля в шаблон, старые страницы при этом остаются корректными, потому что граф принимает новые свойства без миграций.
Онтологии OWL учат машину делать логические выводы
Словарь фиксирует слова, онтология фиксирует правила их совместной жизни. Язык OWL 2, принятый консорциумом W3C в октябре 2009 года, живёт поверх RDF и добавляет конструкторы, знакомые формальной логике: классы, подклассы, эквивалентность, обратные и транзитивные свойства, количественные ограничения на значения.
@prefix ex: <https://example.org/vocab/> .
@prefix owl: <http://www.w3.org/2002/07/owl#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
ex:InputDevice a owl:Class .
ex:Keyboard a owl:Class ;
rdfs:subClassOf ex:InputDevice .
ex:WirelessKeyboard a owl:Class ;
rdfs:subClassOf ex:Keyboard .
ex:hasComponent a owl:TransitiveProperty .
ex:producedBy a owl:ObjectProperty ;
owl:inverseOf ex:produces .
Правило rdfs:subClassOf работает как каскад: раз беспроводная клавиатура это клавиатура, а клавиатура это устройство ввода, всякий экземпляр беспроводной клавиатуры автоматически получает и тип устройства ввода. Транзитивное свойство распространяет состав по цепочке деталей от корпуса до винтика, обратное свойство даёт машине второе направление без дублирования фактов: достаточно записать, что завод производит товар, и связь "товар произведён заводом" машина выводит сама. Ограничения owl:someValuesFrom и owl:cardinality заставляют онтологию держать обязательные поля, например требовать у товара хотя бы одну цену. Программа логического вывода, которую называют ризонером, прогоняет онтологию через правила и достраивает граф неявными фактами, которые ни один редактор не вписал руками.
Профили OWL 2 EL RL и QL решают разные вычислительные задачи
Полная логика OWL 2 выразительна, но алгоритмически дорога, поэтому стандарт делится на три профиля. Профиль EL рассчитан на огромные таксономии: вывод выполняется за полиномиальное время, такие качества ценят биомедицинские базы с сотнями тысяч классов. Профиль QL нацелен на переписывание запросов в обычный SQL поверх реляционной базы, когда RDF остаётся логическим слоем над существующим хранилищем. Профиль RL исполняется правилами прямой цепочки и удобен там, где факты приходят потоком и вывод должен успевать за ними на лету.
Готовые движки закрывают все три режима. Табличный вывод полной логики делает ризонер HermiT, профиль EL разгоняет движок ELK, а фреймворк Apache Jena несёт встроенный правиловый ризонер для RDFS и основной части OWL RL. Выбор профиля всегда выглядит как сделка: чем выразительнее язык, тем дороже каждый ответ машины, и каталогу сайта обычно хватает RL с парой транзитивных свойств. При этом мигрировать между профилями несложно, ведь любая онтология остаётся обычным RDF-графом, а профиль определяет лишь то, какие правила машина обязана уметь исполнять.
Запросы SPARQL извлекают смысл из связанных данных
Граф без языка запросов остался бы складом фактов. SPARQL, обновлённый W3C до версии 1.1 в марте 2013 года, умеет сопоставлять шаблоны триплетов, фильтровать значения и собирать агрегаты. Тело запроса выглядит как те же триплеты с переменными, и база возвращает всё, что подходит под шаблон.
PREFIX ex: <https://example.org/vocab/>
SELECT ?name ?price ?category
WHERE {
?product a ex:Product ;
ex:name ?name ;
ex:price ?price ;
ex:hasCategory ?cat .
?cat ex:name ?category .
FILTER (?price < 8000)
FILTER (LANG(?name) = "ru")
}
ORDER BY ASC(?price)
LIMIT 20
Здесь не нужен JOIN трёх таблиц и знание, в какой колонке лежит раздел каталога: маршрут уже записан в самих данных. Конструкция OPTIONAL добирает необязательные поля, UNION объединяет альтернативные ветки, GROUP BY вместе с SUM и AVG сворачивает продажи по месяцам. Конструкция CONSTRUCT возвращает не таблицу, а новый граф, и на этом строятся генераторы рекомендаций: один запрос собирает смысловую окрестность товара, второй формирует из неё блок похожих позиций. Сервер Fuseki из состава Apache Jena поднимает SPARQL endpoint, к которому ходят внутренние сервисы сайта, от фильтра каталога до чат-помощника, отвечающего на вопросы о характеристиках.
Агрегация закрывает и отчётные задачи. Один запрос сворачивает разделы каталога в сводку:
SELECT ?category (SUM(?price) AS ?sum) (COUNT(?product) AS ?cnt)
WHERE {
?product a ex:Product ;
ex:price ?price ;
ex:hasCategory ?cat .
?cat ex:name ?category .
}
GROUP BY ?category
Каждая строка ответа это уже готовая карточка раздела с суммарной стоимостью и числом позиций, и такому отчёту не важно, сколько новых свойств появилось в графе за квартал.
Практичная схема внедрения семантического слоя в проекте
Технологии по отдельности дают мало, эффект появляется при сборке в один контур, где публикация на страницах, внутреннее хранилище и онтология усиливают друг друга. Рабочий план умещается в пять шагов:
- составить перечень классов сущностей проекта и связей между ними;
- сопоставить классы со стандартными типами schema.org и зафиксировать собственные расширения;
- выгружать контент из основной базы в триплеты и проверять граф схемами SHACL;
- подключить SPARQL endpoint для поиска, фильтров и рекомендаций;
- выводить JSON-LD в шаблоны страниц и валидировать разметку на каждом релизе.
Для валидации W3C в июле 2017 года выпустил язык SHACL: фигуры-шаблоны описывают, какие типы и свойства обязан иметь каждый класс, и проверка графа становится такой же рутиной, как юнит-тесты. На Python работа с графом выглядит так:
import rdflib
g = rdflib.Graph()
g.parse("catalog.ttl", format="turtle")
q = """
PREFIX ex: <https://example.org/vocab/>
SELECT ?name ?price WHERE {
?p a ex:Product ;
ex:name ?name ;
ex:price ?price .
FILTER (?price < 8000)
}
"""
for row in g.query(q):
print(row.name, row.price)
Выгода собирается из мелочей. Каталог перестаёт зависеть от жёсткой схемы: новый тип свойства не требует миграций и месяца согласований, он просто появляется в графе и старые данные остаются корректными. Поиск и рекомендации строятся по смыслу, а не по совпадению слов. Цепочка из страницы товара, обзора, инструкции и отзыва читается роботом как маршрут по графу, и расширенные сниппеты в выдаче превращаются из рекламного бонуса в естественное следствие порядка в контенте. Сайт, который объясняет себя машине на языке RDF и OWL, перестаёт быть набором текстов и становится базой знаний, полезной и читателю, и любой внешней системе, которая однажды заглянет внутрь.