Introduction
Dans la deuxième partie (GraphQL + OWL : Espaces de noms), nous avons commencé à coder le module logiciel permettant de réaliser cela. Plus précisément, nous avons discuté et illustré comment gérer les espaces de noms.
Cette troisième partie traite des types et des propriétés, qui sont les éléments fondamentaux d’une ontologie et d’un schéma GraphQL.
À la fin de l’implémentation, il devra être possible de :
- accéder à un endpoint GraphQL
- utiliser l’API d’introspection
- naviguer dans le schéma discuté ici dans la première partie (GraphQL + OWL : Une interface GraphQL “ontologisée”). Plus précisément, nous devrions trouver les types et champs correspondants correctement définis dans chaque espace de noms géré.
Des types, mais aussi des champs
-->
-->
Il semble évident que le système doit avoir un type correspondant dans le schéma GraphQL pour chaque classe définie dans l’ontologie.
Vous avez peut-être remarqué que, dans la déclaration ci-dessus, aucune propriété ou attribut n’est attaché à cette classe, uniquement des métadonnées (par exemple, sous-classes, classes équivalentes, provenance, disjonctions). Le validateur de schéma signalerait une erreur si nous créions et enregistrions un type GraphQL lorsque l’analyseur d’ontologie rencontre cette définition : dans GraphQL, un type enregistré doit avoir au moins un champ.
Toutes les entités de notre domaine doivent être accessibles via leur identifiant, qui est une URI. Par conséquent, tous les types GraphQL dans le schéma possèdent au moins une propriété “uri” qui renvoie l’URI de l’instance correspondante.
type Person {
uri: ID!
}
Minimaliste, mais cela reste une définition de type valide en GraphQL !
Terminé ? Non, pas du tout : nous devons encore répondre aux questions suivantes :
- Comment peut-on récupérer une instance d’entité par URI ?
- Comment peut-on rechercher et obtenir un ensemble d’instances d’entité ?
- Que dire des champs ?
- Comment gère-t-on les conflits de noms ? C’est-à-dire, des classes portant le même nom mais définies dans des ontologies différentes ? Le fragment ci-dessus correspond à la déclaration de la classe
(foaf) Person; notez qu’il mentionne également deux autres classes nomméesPerson.
Récupérer par URI
Le type de requête de haut niveau (Query) doit définir un champ permettant de récupérer une entité appartenant à un type donné à partir de son URI.
Cependant, nous ne pouvons pas placer ce champ directement dans le type Query, car, comme vous vous en souvenez de la première partie (GraphQL + OWL : Une interface GraphQL “ontologisée”), les champs de haut niveau sous le type Query correspondent aux espaces de noms.
type Query {
dc: dc
foaf: foaf
...
}
Et cela correspond parfaitement au design : le type Person mentionné ci-dessus appartient à l’espace de noms foaf. En conséquence, le champ de récupération de la personne sera défini à ce niveau, comme suit :
type Query {
dc: dc
foaf: foaf
...
}
type foaf {
# Retrieves a Person instance by its Uniform Resource Locator (URI)
person(uri:String!): Person
}
Au moment de la requête, une demande ressemblerait à ceci :
query {
foaf {
person(uri: "https://mydomain.org/people/91283") {
uri
}
}
}
Recherche, Types de Requête et de Réponse
Dans le paragraphe précédent, nous avons récupéré une instance d’entité à partir de son URI ; ici, l’accent est mis sur la définition d’un champ permettant de rechercher dans un type d’entité donné.
Étant donné que ce champ fournit un service de recherche, sa signature doit inclure des capacités de tri, de pagination et de filtrage. Veuillez noter qu’à ce stade, nous n’envisageons pas d’intégrer une fonctionnalité de recherche en texte intégral.
En suivant la même logique d’espace de noms décrite ci-dessus, voici une première tentative :
type Query {
dc: dc
foaf: foaf
...
}
type foaf {
# Retrieves a Person instance by its Uniform Resource Locator (URI)
person(uri:String!): Person
# Retrieves a list of people according to the input sort, pagination and
# filtering criteria
people(sort:String, offset:Int, rows:Int, filters: [String]): [Person]
}
Retourner uniquement un tableau d’instances de Person est réducteur, car la réponse ne contiendrait pas de métadonnées comme le décalage de départ utilisé ou le nombre total d’occurrences trouvées.
De plus, étant un service de recherche, une option possible (future) pourrait être d’ajouter des fonctionnalités de recherche intéressantes telles que le facettage, la correction orthographique, les suggestions de contenu similaire, etc.
Autrement dit, un tableau de Person comme résultat ne suffit pas ; nous devons définir un type de réponse dans notre schéma qui englobe la liste des entités ainsi que les métadonnées supplémentaires que nous souhaitons inclure dans le résultat. Voici le type que nous recherchons :
type PersonCollection {
# The list of resources in this response.
resources: [Person]
# The start offset of results in this response.
offset: Int
# The total number of matches.
totalMatches: Int
# other metadata in the future can be defined below as additional fields
}
En conséquence, la signature du champ ci-dessus devient la suivante :
...
type foaf {
# Retrieves a Person instance by its Uniform Resource Locator (URI)
person(uri:String!): Person
# Retrieves a list of people according to the input sort, pagination and
# filtering criteria
people(sort:String, offset:Int, rows:Int, filters: [String]): [PersonCollection]
}
...
Au moment de la requête :
...
query {
foaf {
people(offset: 10, rows: 10, sort: "name desc") {
resources {
uri
}
totalMatches
offset
}
}
}
...
Champs, Propriétés, (Encore) Espaces de Noms
Jusqu’à présent, nous avons défini les entités et leurs méthodes de récupération. Cependant, nos entités sont un peu pauvres ; elles ne contiennent qu’un seul champ, l’URI. Nous devons enrichir le type enregistré avec les propriétés qui pourraient leur être associées.
Et c’est là que réside le défi : oui, parce que
toute propriété RDF peut être appliquée à n'importe quelle ressource RDF.
Cela entre en conflit avec le fait d’avoir un schéma où les types et les champs sont statiquement définis. La phrase ci-dessus est claire : vous pouvez ajouter n’importe quelle propriété à n’importe quel type. GraphQL étant basé sur un schéma, il n’y a pas d’échappatoire : nous devons ajouter cet ensemble infini de propriétés à tous les types. Comment gérons-nous cela ? Voici notre proposition.
Commençons par restreindre le domaine : nous avons dit que le système devait être initialisé avec un nombre prédéfini (et donc fini) d’ontologies. Par conséquent, nous connaissons à l’avance le nombre de types et de propriétés avec lesquels nous travaillons.
Par exemple, foaf:Person a une propriété foaf:name (qui, comme son nom l’indique, représente le nom d’une personne) et pourrait également avoir un dc:title (ce qui est un peu étrange, mais d’un point de vue RDF, tout à fait correct). À première vue, une option pourrait être la suivante :
type Person {
name: String
title: String
}
Cependant, cette option n’est pas évolutive. Nous ne parlons pas de deux propriétés : dans l’exemple, les deux ontologies contiennent de nombreuses propriétés et, de plus, nous pourrions potentiellement initialiser le système avec un nombre arbitraire d’ontologies ; dix, par exemple.
Si nous suivons la même sémantique des espaces de noms appliquée ci-dessus, les propriétés assignées à un type donné pourraient être organisées et regroupées par espace de noms. Cela nécessite de créer des sous-types dans notre schéma, un pour chaque paire espace de noms/type :
type FoafProperties {
name: String
}
type DcProperties {
title: String
}
Avec ces types supplémentaires, la définition du type Person devient la suivante :
type Person {
foaf: FoadProperties
dc: DcProperties
}
La forme de la requête devient plus concrète :
query {
foaf {
person(uri: "https://mydomain.org/people/91283") {
dc {
title
... other Dublin Core properties
}
foaf {
name
... other FOAF properties
}
... other "namespaced" properties
}
}
}
Nous pouvons encore aller plus loin pour améliorer la lisibilité et l’utilisabilité : parmi toutes les propriétés, il existe un groupe qui appartient au même espace de noms que le type auquel il est associé. Dans ce cas, le niveau de l’espace de noms est redondant et peut être supprimé.
Dans l’exemple, le type Person appartient à l’espace de noms foaf, et la propriété name appartient également à foaf. Nous devrions pouvoir demander ces champs directement, sans entrer dans l’espace de noms propriétaire. Quelque chose comme ceci :
query {
foaf {
person(uri: "https://mydomain.org/people/91283") {
// name and other foad properties don't need the "namespace" level
name
... other FOAF properties
dc {
title
... other Dublin Core properties
}
... other "namespaced" properties
}
}
}
Dans l’implémentation de référence, nous avons laissé les deux options afin que l’utilisateur puisse demander arbitrairement les propriétés appartenant au même espace de noms soit directement, soit en les imbriquant sous le niveau de l’espace de noms :
query {
foaf {
person(uri: "https://mydomain.org/people/91283") {
// implicit namespace option
name
... other FOAF properties
// explicit namespace option
foaf {
name
... other FOAF properties
}
... other "namespaced" properties
}
}
}
Prochains Défis
La gestion des types et des propriétés est un sujet sans fin ; ce qui est décrit ci-dessus n’est qu’une première étape, car dans une ontologie, il peut y avoir (voir l’exemple au début de cet article) de nombreuses métadonnées qui influencent la forme et le comportement des types et des propriétés.
Voici une liste non exhaustive des éléments que nous allons aborder dans les prochains articles :
- l’héritage
- le polymorphisme
- les relations sémantiques (par exemple, symétriques, inverses)
Essayons !
L’article n’est pas uniquement théorique ; une implémentation de référence sur GitHub accompagne le contenu. Le dépôt n’est pas public ; si vous souhaitez y jeter un œil, faites-le-nous savoir, et nous vous donnerons volontiers accès.
Cela dit, après avoir cloné le dépôt, vous pouvez démarrer GraphOWL de deux manières.
Option #1 : Docker
Le projet inclut tout ce dont vous avez besoin pour créer une image Docker fonctionnelle. Vous devez, bien sûr, avoir Docker installé sur votre machine. Voici les étapes :
> cd $PRJ_HOME
> mvn clean install
...
[INFO]
[INFO] --- exec:3.1.0:exec (docker-build) @ graphowl-engine-api ---
#0 building with "desktop-linux" instance using docker driver
#1 [internal] load .dockerignore
#1 transferring context: 2B done
...
#8 naming to com.spaziocodice/graphowl-engine-api:1.0.0-SNAPSHOT done
#8 DONE 0.1s
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 01:02 min
> cd src/test/resources
> docker-compose up
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v2.4.13)
2023-10-15 06:27:39.466 Starting App v1.0.0-SNAPSHOT using Java 17.0.2 ...
...
2023-10-15 06:28:11.542 : Type dcterms:SpatialScheme registered.
2023-10-15 06:28:11.542 : Type dcterms:TypeScheme registered.
2023-10-15 06:28:11.828 Exposing 2 endpoint(s) beneath base path '/actuator'
2023-10-15 06:28:11.843 Tomcat started on port(s): 8080 (http) with context path ''
2023-10-15 06:28:11.848 Started App in 32.564 seconds (JVM running for 32.763)
Option #2: IDE
Si vous chargez le dépôt dans un IDE comme IntelliJ, démarrer le module est simplement une question d’exécuter la classe principale :
Nous serions ravis de recevoir vos questions, doutes et retours concernant cet article de blog !
N’hésitez pas à nous contacter ou à laisser un message dans la section des commentaires ci-dessous.