Introduction
Cet article propose une interface GraphQL compatible avec OWL, comprenant des requêtes et des mutations. Plutôt que de traiter des concepts OWL abstraits, nous faisons référence à une ontologie populaire, BIBFRAME, utilisée pour structurer les descriptions bibliographiques.
Nous passerons en revue un aperçu synthétique de GraphQL et de BIBFRAME, puis nous décrirons la proposition. Notez que cet article n’aborde pas la mise en œuvre logicielle, qui fera l’objet d’un autre article.
Qu'est-ce que GraphQL ?
GraphQL est un langage de requête et un runtime pour les API, développé par Facebook en 2012 et rendu open-source en 2015. Il offre une manière plus efficace, puissante et flexible de demander et manipuler des données depuis les serveurs, comparée aux API REST (Representational State Transfer) traditionnelles.
Dans un échange GraphQL, les clients peuvent spécifier exactement les données dont ils ont besoin, et le serveur répond uniquement avec les informations demandées, éliminant ainsi le problème de surchargement ou de sous-chargement des données.
Du point de vue de cet article, les fonctionnalités clés les plus pertinentes de GraphQL sont :
- Requêtes flexibles : Les clients peuvent spécifier la forme exacte de la réponse dont ils ont besoin.
- Point de terminaison unique : Contrairement aux API REST qui nécessitent souvent plusieurs points de terminaison pour différentes ressources, les API GraphQL utilisent généralement un point de terminaison unique pour tout, incluant requêtes et mutations.
- Schéma fortement typé : Les API GraphQL sont définies par un schéma qui explicite les types de données pouvant être échangés.
- Introspection : Les API GraphQL offrent des capacités d’introspection, permettant aux clients de consulter le schéma pour comprendre les types et opérations disponibles.
Consultez cet article si vous souhaitez connaître notre point de vue sur le débat GraphQL vs REST.
Qu'est-ce que BIBFRAME ?
BIBFRAME (BIBliographic FRAMEwork) est une initiative dirigée par la Library of Congress visant à moderniser et remplacer le standard MARC (Machine-Readable Cataloging), qui a été pendant des décennies la base de la description bibliographique et du catalogage dans les bibliothèques.
Le design de BIBFRAME transforme la manière dont les informations bibliographiques sont structurées et partagées à l’ère numérique, en s’appuyant sur les principes des données liées (linked data) et du Web sémantique.
Au cœur de son objectif, BIBFRAME vise à offrir un cadre plus flexible, extensible et adapté au web pour décrire les ressources bibliographiques, telles que les livres, les revues, les supports audiovisuels et d’autres types de contenu.
Initialement conçu pour les catalogues sur fiches, il s’éloigne de la structure des enregistrements MARC, qui présente des limitations pour représenter des relations complexes et s’adapter à l’écosystème moderne de l’information.
Du point de vue de cet article, les aspects les plus pertinents de BIBFRAME sont :
- Approche basée sur les données liées : BIBFRAME adopte les principes des données liées, qui mettent l’accent sur la connexion et l’interconnexion des données à travers le web.
- Modélisation sémantique : BIBFRAME utilise un modèle sémantique pour décrire les entités bibliographiques et leurs relations.
- Simplification : BIBFRAME vise à simplifier le processus de catalogage en utilisant des structures de données plus intuitives et cohérentes.
- Extensibilité : BIBFRAME est extensible, permettant aux institutions, bibliothèques et projets d’adapter le cadre à leurs besoins spécifiques.
L’ontologie est disponible au format OWL (Ontology Web Language).
"Brundle a-t-il absorbé la mouche?"
Dans le célèbre film “The Fly”, Seth Brundle se transforme en une créature hybride homme-insecte à la suite d’une expérience de téléportation qui a mal tourné.
Bien que ce ne soit pas un film avec une fin heureuse, j’aime l’idée de quelque chose qui se transforme en quelque chose de nouveau en combinant deux éléments différents, et c’est pourquoi ce film m’est immédiatement venu à l’esprit lorsque j’ai commencé à réfléchir à la combinaison de GraphQL et BIBFRAME.
L’idée est relativement simple :
Étape 1 (conception) : définir un schéma GraphQL capable d’exprimer des requêtes et des mutations en fonction des structures de données définies dans une ontologie arbitraire (comme mentionné, nous utiliserons BIBFRAME tout au long de cet article).
Étape 2 (implémentation du schéma) : un composant logiciel qui lit un fichier OWL et crée automatiquement le schéma en fonction de la conception définie à l’étape 1.
Étape 3 (implémentation de l’interface) : un composant logiciel qui implémente les résolveurs derrière les requêtes et mutations définies à l’étape 2.
Étant donné que ce sont trois étapes distinctes et cohérentes, chacune avec un type de défi différent, nous les décrirons à l’aide d’articles dédiés. Ici, nous nous concentrerons sur les défis de la première étape (et sur la proposition correspondante).
Conception du Schéma : Défis
Espaces de noms
Au moment de la rédaction, tous les types GraphQL définis dans une instance de schéma donnée partagent un espace de noms global unique. Bien que cela fonctionne bien dans de nombreux scénarios, il existe des cas, comme l’idée que nous discutons ici, où cela représente une limite difficile à surmonter.
En effet, dans le monde RDF, les espaces de noms sont une construction centrale pour séparer les préoccupations et éviter les conflits de noms entre entités et propriétés appartenant à des espaces différents. Le nom complet d’une entité décrite dans une ontologie comprend l’espace de noms (préfixé ou non) et le nom “local”.
Par exemple, BIBFRAME définit une entité Work dont le nom complet est :
http://id.loc.gov/ontologies/bibframe/Work ou, plus simplement, bf:Work (bf étant un préfixe d’espace de noms, une forme abrégée de l’espace de noms).
Si nous travaillons avec une ontologie qui étend ou utilise BIBFRAME et fournit un type également appelé Work, cela représente une entité entièrement différente de l’entité Work de BIBFRAME mentionnée ci-dessus.
Notez que cette logique s’étend également aux propriétés dans les ontologies : le nom complet d’une propriété comprend l’espace de noms et son nom local. Cela permet d’avoir plusieurs propriétés portant le même nom. Par exemple :
- https://id.loc.gov/ontologies/bibframe/creationDate est une propriété définie dans BIBFRAME.
- https://x.y.x/z/creationDate est, en revanche, une autre propriété décrite dans une autre ontologie.
Pour compliquer les choses, il est important de se rappeler qu’une ontologie n’est pas un ensemble isolé de structures de données. Elle est généralement un mélange d’éléments définis dans l’ontologie elle-même et d’autres éléments (entités, propriétés) provenant d’ontologies différentes.
Voici, par exemple, l’en-tête de l’ontologie BIBFRAME, où l’on peut voir les préfixes des espaces de noms utilisés tout au long de la définition :
...
Nommage dans GraphQL
Comme mentionné ci-dessus, dans GraphQL, nous n’avons pas d’espaces de noms. Pour surmonter ce problème, il existe des approches comme celle-ci et cette proposition bien détaillée (mais malheureusement ancienne). L’auteur de cette dernière a également créé un issue sur GitHub, mais, comme vous pouvez le constater, cela a généré un long débat (toujours ouvert).
Malheureusement, l’approche suggérée dans la documentation d’Apollo ne convient pas à notre cas d’utilisation, et la proposition, bien que j’apprécie beaucoup son design, reste une proposition et ne fait pas partie des spécifications officielles de GraphQL.
Nous devons réfléchir à une conception de schéma qui puisse répondre aux exigences ci-dessus tout en respectant parfaitement les spécifications de GraphQL.
Les règles de nommage de GraphQL n’autorisent pas l’utilisation d’URI comme noms d’entités ou de propriétés en raison de caractères comme : ou /. Sans cette limitation, nous pourrions résoudre le problème de nommage en utilisant quelque chose comme ceci :
type https://id.loc.gov/ontologies/bibframe/Work {
...BIBFRAME Work fields follow...
}
type https://x.y.z/Work {
...XYZ Work fields follow...
}
ou même, plus succinctement
type bf:Work {
...BIBFRAME Work fields follow...
}
type xyz:Work {
...XYZ Work fields follow...
}
Bien que les exemples ci-dessus, en particulier celui qui utilise des préfixes, suggèrent une solution de contournement très simple comme “Pourquoi ne pas utiliser le souligné à la place des deux-points ?”. Je veux dire quelque chose comme ceci :
type bf_Work {
...BIBFRAME Work fields follow...
}
type xyz_Work {
...XYZ Work fields follow...
}
Le design résultant présente, selon moi, les inconvénients suivants :
- Les espaces de noms ne sont pas des entités ; ce ne sont que des préfixes.
- Les soulignés ne peuvent pas être utilisés dans les noms. Si nous utilisons une ontologie qui possède une propriété appelée “has_permission”, nous devrons différencier le premier souligné des autres qui pourraient potentiellement apparaître dans le nom (par exemple, “bf_has_permission”).
- Les noms des propriétés changent fortement d’un point de vue sémantique par rapport à l’ontologie source. Cela rend les requêtes (c’est-à-dire les
querieset lesmutations) moins lisibles et donc moins compréhensibles. - Les espaces de noms ne sont pas des conteneurs ; en conséquence, il y aura une longue liste plate d’entités et une longue liste plate de propriétés associées aux entités.
La Proposition
- Il y a un petit nombre de classes
- Les classes ont un petit nombre de propriétés
- Les types appartiennent à trois ontologies différentes : Share-VDE, BIBFRAME, SKOS
De cette manière, nous pouvons capturer et discuter de nombreux cas intéressants, par exemple ceux où les ontologies sont mélangées même au sein d’une seule classe.
Voyons les types d’exemple dans le diagramme suivant :
- Les types bleus proviennent de BIBFRAME
- Les types verts proviennent de Share-VDE
- Les types rouges proviennent de SKOS
Nous avons introduit plusieurs éléments pour nous assurer de considérer les relations d’agrégation, de composition et d’héritage :
- svde:Work étend bf:Work : cela signifie une relation où svde:Work EST un bf:Work. Cela implique, entre autres, que l’enfant hérite des propriétés du parent (uri et bf:title).
- uri est une propriété sans espace de noms ; il n’a pas de sens d’avoir bf:uri et svde:uri. Elle représente toujours l’identité de l’entité.
- svde:Work ajoute une propriété svde:language qui correspond à un type svde:Language.
- svde:Language possède deux propriétés littérales : skos:label et skos:altLabel (dans le diagramme, elles sont représentées comme des classes pour souligner qu’elles appartiennent à un autre espace de noms).
Requêtes de Haut Niveau : Uniquement des Espaces de Noms
Pour chaque espace de noms utilisé dans l’ontologie ou les ontologies de référence, nous ajoutons un champ dans le type de requête de haut niveau (Query) :
type Query {
bf: bf
svde: svde
skos: skos
}
Notez que le schéma est un peu contre-intuitif à cause de la nomenclature : le nom du champ et le type ont la même valeur (skos : skos) ; ce n’est ni excellent, ni terrible. En pratique, les clients ne se soucient pas vraiment des classes et de leurs noms à l’exécution : ce qu’ils voient, ce sont des propriétés skos, bf et svde au sommet de la hiérarchie.
Que signifie cela en pratique ? Un client qui souhaite initier une interaction de type “query” avec un tel système doit déclarer l’espace de noms de l’entité ou des entités requises en premier lieu :
query QueryOnBibFrameEntitieds (...variables...) {
bf {
...query...
}
}
Requêtes "Espacées par Noms"
Chaque espace de noms fournit deux propriétés littérales (uri et prefix) ainsi que des champs pour récupérer les entités qu’il possède par leur URI et en utilisant des paramètres de recherche :
type bf {
uri: ID!
prefix: String!
work(uri: String!): BfWork
works(title: String!, offset:Int, rows: Int, sort: String): [BfWork]
}
type svde {
uri: ID!
prefix: String!
work(uri: String!): SvdeWork
works(title: String!, offset:Int, rows: Int, sort: String): [SvdeWork]
language(uri: String!): SvdeLanguage
languages(..., offset:Int, rows: Int, sort: String): [SvdeLanguage]
}
type skos {
uri: ID!
prefix: String!
}
Remarque :
- L’exemple suppose que la seule propriété “recherchable” d’un
Workest le titre. - Les champs works et languages doivent renvoyer un objet complexe contenant des métadonnées de recherche (pagination, tri) et non simplement un tableau de
works. - L’espace de noms skos ne contient aucun type (dans l’exemple, il y a uniquement des propriétés littérales qui lui appartiennent).
Types
Qu’en est-il des types mentionnés dans l’extrait précédent ? SvdeWork, BfWork, SvdeLanguage ?
Premier point : GraphQL ne prend pas en charge les sous-classes ; il existe un mot-clé
extends, mais il est utilisé pour ajouter des fonctionnalités supplémentaires aux types existants. Cependant, la réalisation d’interfaces et l’héritage multiple sont possibles.Deuxième point : Tout type parent (c’est-à-dire toute classe que nous devons sous-classer) doit être déclaré comme une interface. C’est le cas de
BfWork(sous-classé parSvdeWork).Troisième point : Pour regrouper les propriétés d’une classe sous l’espace de noms correspondant, nous devons définir un type spécifique. Par exemple, il y aura un type
BfWorkPropertieset un typeSvdeWorkProperties.Quatrième point : Tout type feuille est déclaré tel quel ; c’est le cas de
BfTitle.
Voici un premier brouillon (il manque encore un point) :
// bf:Title
interface bfTitle {
mainTitle: String
subtitle: String
partNumber: String
partName: String
}
// the properties of a bf:Work
interface bfWorkProperties {
title: bfTitle
}
// bfWork must realize the bfWorkProperties interface
interface bfWork implements bfWorkProperties {
uri:String
bf: bfWorkProperties
title: bfTitle
}
// properties of a svde:Language
type skosLanguageProperties {
label: String
altLabel: String
}
// svde:Language must realize the bfLanguageProperties
type SvdeLanguage {
uri: ID!
skos: skosLanguageProperties
}
// properties of a svde:Language
interface svdeWorkProperties {
language: SvdeLanguage
}
// svde:Work realizes bf:Work
type svdeWork implements bfWork {
uri:String
bf: bfWorkProperties
svde: svdeWorkProperties
}
Il reste encore un point qui peut être amélioré. Si je veux demander le titre et la langue d’un svde:Work, la requête ressemble à ceci :
query WorkDetailsQuery {
svde {
work(uri: "https://svde.org/works/1234") {
svde {
language {
label
altLabel
}
}
bf {
title {
mainTitle
subTitle
}
}
}
}
}
Bien que cela fonctionne, nous pouvons éviter un peu de redondance dans la déclaration de l’espace de noms. Le Work appartient à l’espace de noms svde, tout comme la langue.
Si je suis dans un type qui appartient à un espace de noms, il devrait y avoir un moyen d’éviter de répéter l’espace de noms pour les propriétés déjà dans ce contexte. Je veux dire quelque chose comme ceci :
query WorkDetailsQuery {
svde {
work(uri: "https://svde.org/works/1234") {
language {
label
altLabel
}
bf {
title {
mainTitle
subTitle
}
}
}
}
}
Notez que le champ language n’est plus dans l’espace de noms svde. Cela peut sembler une différence triviale, mais si vous imaginez un contexte avec de nombreux espaces de noms et de nombreuses propriétés, la structure de la requête en tirera certainement profit.
Cela nécessite un léger changement dans notre schéma : SvdeWork devrait inclure un membre nommé svdeWorkProperties, et en plus, il devrait implémenter lui-même l’interface svdeWorkProperties :
type svdeWork implements bfWork, svdeWorkProperties {
uri:String
bf: bfWorkProperties
svde: svdeWorkProperties
language: SvdeLanguage
}
Jusqu’à présent, nous avons réussi à concevoir un schéma avec un bon compromis (du moins à mon avis) entre lisibilité et verbosité. Comme mentionné, si GraphQL prenait en charge les espaces de noms, les choses auraient été plus simples, mais malheureusement, ce n’est pas le cas.
Prochaines Étapes
Les trois points mentionnés au début de l’article ont déjà défini les prochaines étapes.
Nous avons un design pour le schéma ; l’objectif suivant est de créer un composant logiciel qui lit une définition d’ontologie et génère les structures de données GraphQL décrites dans la proposition ci-dessus.
Ce n’est pas une tâche facile : le composant devra prendre en compte les défis cruciaux suivants :
- Relations verticales comme les généralisations ou les réalisations. L’héritage, comme nous l’avons dit, a une signification en GraphQL qui diffère considérablement de ce qui est défini dans OWL.
- Relations implicites comme “inverseOf” et “symmetric” qui pourraient avoir un impact sur les structures de données à construire.
- Espaces de noms. Dans la proposition ci-dessus, nous avons décrit un design pour les gérer, mais l’implémentation d’un tel design est un défi.
- Flexibilité : souvenez-vous que l’exigence est que le moteur ne soit pas lié à une ontologie spécifique.