Sommaire

 

Pomme de Gallica et pomme d'Api

Vendredi 16 février 2018 18:00:02 +0200

Compte rendu d'une expérimentation de l'API d'interrogation de la base Gallica

Les équipes de Gallica ont récemment rendu publique la première version d'une API d'interrogation de leur base. C'est une grande et belle nouveauté. Cette API donne à tout un chacun un accès programmatique à l'intégralité des index de Gallica et offre ainsi la possibilité d'élaborer des applications alimentées interactivement par les documents de toutes natures qui en proviennent.

L'exploitation se fait par une simple requête HTTP ; les résultats sont restitués sous la forme d'un flux XML. Le modèle d'interrogation et d'encodage des résultat est celui de la norme SRU (version 1.2).

La documentation de l'API est accessible ici.

J'ai commencé de mon côté un démonstrateur d'interrogation de Gallica exploitant cette API qui m'a permis de prendre connaissance du modèle de données de la base Gallica et d'explorer quelques pistes d'interrogation de ces données. Ce faisant, j'ai également pu prendre la mesure de certaines limitations imposées par l'API (ou par le moteur de recherche sous-jacent). J'en fais état en fin de post ci-dessous.

Mon démonstrateur, que j'ai baptisé Kaliga, est une interface d'interrogation simplifiée de Gallica et s'inspire largement de celle disponible sur le site officiel. Elle est écrite en PHP et est accessible depuis cette page.

On trouvera ci-dessous un premier compte rendu des observations faites à l'occasion du développement de Kaliga, ainsi qu'un certain nombre de questions encore ouvertes, qui viendront compléter ou éclaircir les indications de la documentation officielle.

Reproduction d'une affiche de Georges Redon (1927).
Source Gallica (ark:/12148/btv1b9016581m)

L'API Gallica

Modèle de données

Je ne vais pas présenter ce modèle ici ; j'en donnerai des aperçus à travers les exemples d'utilisation de l'API.

Pour se faire une idée des données interrogeables et de leur modélisation, on peut, de façon empirique, exploiter plusieurs sources.

La première est d'observer l'ensemble des critères exposées dans la recherche avancée sur le site Gallica. La seconde est d'analyser les notices associées aux documents dans Gallica. La troisième est de recenser ce qui est restitué dans le flux XML de l'API. La documentation de l'API, enfin, donne également un aperçu du modèle, en fournissant notamment des listes de valeurs pour certains attributs documentaires.

Exploration du flux XML

En réponse à une requête, les données sont restituées dans un flux XML. Le modèle documentaire est reflété par un ensemble de balises spécifiques. D'autres balises donnent accès à des informations de gestion de la transaction (p. ex. la requête exécutée ou le nombre total estimé de résultats).

La macro-structure du flux XML est la suivante :

<srw:searchRetrieveResponse ... > <srw:version>1.2</srw:version> <srw:echoedSearchRetrieveRequest> <srw:query> [paramètre query] </srw:query> <srw:version>1.2</srw:version> </srw:echoedSearchRetrieveRequest> <srw:numberOfRecords> [entier] </srw:numberOfRecords> <srw:records> [résultats unitaires] </srw:records> <srw:nextRecordPosition> [entier] </srw:nextRecordPosition> </srw:searchRetrieveResponse>

L'élément records est l'enveloppe regroupant l'ensemble des résultats unitaires (ou documents) restitués pour la requête. Chaque résultat unitaire est identifié par la balise record (voir ci-dessous).

La requête elle-même (paramètre query de la requête HTTP) est restituée en tête de flux dans la balise srw:query. Le nombre total de résultats unitaires à la requête est donné dans la balise srw:numberOfRecords. La version de la norme SRU appliquée est indiquée deux fois (on ne saurait être trop prudent) mais ne restitue pas la valeur donnée dans la requête HTTP (qui aujourd'hui est purement et simplement ignorée).

Note — La balise srw:nextRecordPosition en fin de flux reprend la valeur (+ 1) du paramètre maximumRecords de la requête HTTP (15, par défaut) dans le but, j'imagine, d'indiquer la valeur que devrait prendre le paramètre startRecord pour restituer un nouveau lot de résultats si le nombre total de résultats exécède la limite définie pour le flux (cette valeur serait p. ex. de 16, si la limite est celle définie par défaut à 15). La valeur de cette balise est indiquée même si le nombre total de résultats est inférieur à la limite maximale. Dans les faits, il est donc toujours nécessaire de recompter les résultats restitués dans le flux, éventuellement en exploitant la balise srw:recordPosition (qui donne le rang du résultat dans le présent flux XML et non pas parmi l'ensemble des résultats, et qui par ailleurs commence à 0 et non pas à 1).

Espaces de noms

Dans cette instanciation XML du modèle, il importe de retenir que les balises (les attributs constitutifs d'un document) relèvent de plusieurs espaces de noms différents qu'il conviendra d'identifier lors de l'analyse du flux. Dans le cadre de ce démonstrateur, j'ai essentiellement eu affaire aux attributs suivants :

  • éléments Gallica :
    • xmlns="http://gallica.bnf.fr/namespaces/gallica/"
    • apparaissent sans préfixe (p. ex. <thumbnail>)
  • éléments Dublin Core :
    • xmlns:dc="http://purl.org/dc/elements/1.1/"
    • apparaissent avec le préfixe dc (p. ex. <dc:creator>)

Les éléments Dublin Core sont restitués dans un schéma OAI (Open Archives Initiative) qui relève, lui aussi, de son propre espace de noms : xmlns:oai_dc="http://www.openarchives.org/OAI/2.0/oai_dc/".

À quoi il faut rajouter l'espace de nom générique de la norme SRU qui gouverne le flux XML dans son ensemble (préfixe srw) : xmlns:srw="http://www.loc.gov/zing/srw/".

En pratique donc, on est amené à manipuler quatre espaces de noms différents.

Identification des attributs documentaires

Plus spécifiquement, dans le flux XML, les informations relatives à un document (ses attributs documentaires) se retrouvent soit dans l'élément srw:recordData (données au format Dublin Core), soit dans l'élément srw:extraRecordData (données Gallica). La structure de base d'un résultat (« record ») se présente comme suit :

... <srw:record> <srw:recordData> <oai_dc:dc> [données au format Dublic Core] </oai_dc:dc> </srw:recordData> ... <srw:extraRecordData> [données au format Gallica] </srw:extraRecordData> </srw:record> ...

Pour obtenir le titre d'un document (élément Dublin Core), le chemin à suivre (le XPath) sera donc le suivant (en adoptant les conventions PHP de SimpleXMLElement) :

// Enregistrement des espaces de noms $xml->registerXPathNamespace('', 'http://gallica.bnf.fr/namespaces/gallica/'); $xml->registerXPathNamespace('srw', 'http://www.loc.gov/zing/srw/'); $xml->registerXPathNamespace('oai_dc', 'http://www.openarchives.org/OAI/2.0/oai_dc/'); $xml->registerXPathNamespace('dc', 'http://purl.org/dc/elements/1.1/'); // Activation du tableau des espaces de noms $ns = $xml->getNameSpaces(true); // Identification du titre d'un doc $xml-> children($ns['srw'])-> records-> children($ns['srw'])-> record-> children($ns['srw'])-> recordData-> children($ns['oai_dc'])-> dc-> children($ns['dc'])-> title

Et pour obtenir l'URI de la vignette associée au document (élément Gallica) :

// Identification de la vignette d'un doc $xml-> children($ns['srw'])-> records-> children($ns['srw'])-> record-> children($ns['srw'])-> extraRecordData-> children($ns[''])-> thumbnail

L'ensemble des attributs d'un document qu'on souhaite manipuler peut être récupéré à l'analyse d'un résultat. Voici le code PHP que j'ai utilisé pour le démonstrateur et la liste des attributs exploités (il en existe d'autres) :

foreach ($records as $record) { $dc_set = $record->children($ns['srw'])->recordData->children($ns['oai_dc'])->dc->children($ns['dc']); $gal_set = $record->children($ns['srw'])->extraRecordData->children($ns['']); $doc_set = array( 'title' => trim((string) $dc_set->title), 'creator' => array(), 'contributor' => array(), 'date' => trim((string) $dc_set->date), 'description' => array(), 'source' => trim((string) $dc_set->source), 'identifier' => trim((string) $gal_set->link), 'thumbnail' => trim((string) $gal_set->thumbnail), 'typedoc' => trim((string) $gal_set->typedoc), 'nqamoyen' => trim((string) $gal_set->nqamoyen) ); foreach ($dc_set->creator as $creator) { $doc_set['creator'][] = trim((string) $creator); } foreach ($dc_set->contributor as $contributor) { $doc_set['contributor'][] = trim((string) $contributor); } foreach ($dc_set->description as $desc) { $doc_set['description'][] = trim((string) $desc); } }

Les variables $dc_set et $gal_set définissent des raccourcis XPath vers, respectivement, les sous-éléments Dublin Core et Gallica d'un document. Le tableau $doc_set enregistre les différents attributs du document qui seront exploités par Kaliga, qu'ils proviennent d'éléments DC ou d'éléments Gallica. Certains éléments pouvant être multivalués, ils sont enregistrés comme sous-tableaux, avec l'ensemble des valeurs attestées. C'est le cas notamment du champ Auteur (élément dc.creator).

On notera enfin qu'il existe des redondances d'informations entre les deux ensembles de données. Ainsi, l'URI du document dans la base Gallica est accessible soit via la balise DC <dc:identifier> soit via la balise Gallica <link>.

Note — Pour ma part, j'ai choisi comme identifiant (URI) du document la valeur de l'élément Gallica link plutôt que l'élément DC dc:identifier, du fait que ce dernier peut être multivalué et que les valeurs ne sont pas toutes toujours des URI. C'est par exemple le cas pour ce document qui liste trois valeurs différentes dont l'une seulement est un URI.

Requêtes HTTP

Les requêtes HTTP suivent toutes un même schéma :

http://gallica.bnf.fr/sru?[paramètres]

Parmi les paramètres, quatre sont théoriquement requis :

  1. version=1.2 qui précise la version de la norme SRU utilisée (ici, 1.2),
  2. operation=searchRetrieve qui est le verbe SRU d'interrogation de la base,
  3. query=[requête] qui est la requête de recherche à exécuter,
  4. startRecord=[entier] qui précise le rang du premier des résultats à restituer parmi tous les résultats possibles (le premier résultat a le rang 1).

Note — Selon la documentation de l'API, le paramètre de version de la norme SRU (version=1.2) est obligatoire ; en pratique, dans l'implémentation Gallica, il semble facultatif, voire même ignoré par l'API (une requête avec un numéro de version fantaisiste ne pertube rien, semble-t-il). De même, le paramètre startRecord est également réputé obligatoire mais dans les faits ce n'est pas le cas (par défaut, les résultats restitués commencent au premier résultat). Toutefois, pour anticiper d'éventuelles évolutions de l'API, il semble plus prudent de s'en tenir aux instructions de la documentation et mentionner systématiquement ces deux paramètres.

Le paramètre query

Une requête (query) se compose d'une suite d'opérations (au moins une) exécutées sur un champ (ou index) au moyen de mots-clés. Une opération prend la forme suivante :

(<index> <opérateur> "<mots-clés>")

Lorsque la requête mentionne plusieurs mots-clés (chaînes de caractères), ceux-ci doivent être entourés de guillemets. En pratique, on peut conserver ces guillemets pour les requêtes à mot-clé unique et convenir que les mots-clés d'une requête sont toujours entourés de guillemets. C'est ce que fait Kaliga.

Note 1 — Ces guillemets ont ici une valeur purement syntaxique dans la construction d'une requête HTTP valide ; ils n'ont pas la valeur sémantique d'exigence de « match exact » comme pour une interrogation via les moteurs de recherche du Web.

Note 2 — Les parenthèses ne sont pas requises pour des requêtes simples, mais elles deviennent nécessaires lorsque les requêtes combinent plusieurs requêtes simples à l'aide d'opérateurs booléens. En pratique, comme pour les guillemets autour des mots-clés, on peut convenir de toujours parenthéser les requêtes.

Par exemple :

(dc.creator all "gustave flaubert")

pour interroger l'index (ou champ) des auteurs (dc.creator) avec l'ensemble (all) des mots-clés (gustave et flaubert).

Ou encore :

(dc.creator any "flaubert maupassant")

pour interroger l'index (ou champ) des auteurs (dc.creator) avec l'un ou l'autre (any) des mots-clés (flaubert et/ou maupassant).

Une requête minimale à Gallica prend donc la forme de l'exemple suivant :

http://gallica.bnf.fr/sru? ⤶ version=1.2 ⤶ &operation=searchRetrieve ⤶ &startRecord=1 ⤶ &query=(dc.creator any "flaubert maupassant")

Encodage des caractères

Par ailleurs, pour être valablement soumises, les requêtes HTTP doivent être proprement encodées pour préserver les espaces et les caractères spéciaux ou réservés, qu'il convient de traduire en leur valeur hexadécimale. Cet encodage concerne plus particulièrement le paramètre query. En PHP, on peut utiliser la fonction urlencode() pour assurer ce traitement. Pour mémoire, les caractères à encoder sont les suivants :

Caractère :()"[espace]
Code Hexa :%28%29%22%20
(ou +)

Le paramètre query dans la requête ci-dessus sera donc encodé comme suit :

http://gallica.bnf.fr/sru? ⤶ version=1.2 ⤶ &operation=searchRetrieve ⤶ &startRecord=1 ⤶ &query=%28dc.creator%20any%20%22flaubert%20maupassant%22%29

A priori, les caractères constitutifs des mots-clés (par exemple les lettres accentuées) sont également concernés, quoiqu'il semble possible de les transmettre codés en UTF-8, ce que fait Gallica. Voici ce qu'il en serait du mot-clé « la belle Hélène » :

  • Gallica : la%20belle%20Hélène
  • Kaliga : la%20belle%20H%C3%A9l%C3%A8ne

La documentation ne précise pas les contraintes en la matière et ne détaille pas non plus les traitements de normalisation éventuellement opérés sur les mots-clés saisis par l'utilisateur (cas des majuscules, des diacritiques, des traits d'union, des signes typographiques, des valeurs numériques, etc.).

À l'observation, des assimilations doivent être opérées, mais des différences de traitement semblent subsister selon la forme des mots-clés saisis par l'utilisateur. Ainsi, des requêtes sur « francs-maçons » et sur « francs macons » affichent les mêmes résultats de tête, mais annoncent des totaux sensiblement distincts (56 879 vs. 48 171, le 12 février 2018). Toutefois, ces totaux sont identiques lorsque la requête est faite à l'aide de l'opérateur d'adjacence (7 181).

Index (ou champs documentaires)

La liste des index disponibles est assez longue et Kaliga n'en exploite que quelques-uns. Parmi les champs textuels les plus communs :

  • dc.title, le titre du document.
  • dc.creator, l'auteur du document — pour s'assurer de retrouver des auteurs dans le cadre de documents produits collectivement, il peut être utile d'opérer en parallèle une recherche sur le champ dc.contributor (contributeurs) ; voir ci-dessous le paragraphe Requêtes composées.
  • dc.subject, les sujets dont traite le document (recensement manuel) ; la liste des sujets peut être parfois très longue, cf. cet exemple de document (consulter la notice).
  • dc.description, la description faite du document (description manuelle) ; certaines descriptions peuvent se révéler très longues également (cf. le document cité ci-dessus).
  • text, le texte intégral du document, résultant de son océrisation.
  • gallica, l'index de tous les index interrogeables selon la documentation : « il s'agit d'un champ texte libre permettant la recherche par mots dans tout (métadonnés, texte, tdm etc ....) ».
    Cet index générique permet une interrogation de la base Gallica sans avoir à se soucier d'identifier le ou les champs à cibler. C'est la valeur par défaut retenue par l'application Gallica dans son formulaire de recherche simple. C'est également celle retenue dans Kaliga.
    La contrepartie est un relatif amoindrissement de la précision des recherches : on ne saura pas préciser si l'apparition d'un résultat est due à la présence du mot-clé dans un champ plutôt qu'un autre (est-ce un livre de Diderot ou sur Diderot ?

j'ai retenu dans le démontrateur Kaliga les champs Titre, Auteur, Texte et Gallica. Ils peuvent être sélectionnés lors de la formulation d'une requête.

Je renvoie à la documentation de l'API pour la liste exhaustive des champs interrogeables. On notera qu'il existe des champs factuels (p. ex. les dates) qu'il est possible d'interroger à l'aide d'opérateurs de comparaison (avant, après…). Kaliga n'en fait pas (encore) usage.

Opérateurs sur les mots-clés

Pour les recherches sur des mots-clés de type chaînes de caractères, ces opérateurs sont au nombre de trois :

  • all, équivaut à un ET logique : les résultats mentionnent tous les mots de la requête (sans contrainte d'ordre ou de distance) ; c'est la valeur par défaut ;
  • any, équivaut à un OU logique : les résultats mentionnent au moins un des mots de la requête ;
  • adj, opérateur d'adjacence : tous les mots doivent être présents (ET logique), côte à côte et dans le même ordre que celui de la requête.

De même que l'index à interroger, l'opérateur à appliquer aux mots-clés peut être sélectionné lors de la formulation d'une requête.

De manière incidente, la documentation de l'API précise que les opérateurs all et any « laissent le moteur effectuer certains élargissements pour les mots de la requête ».

Ces opérations d'« élargissement » ou d'« expansion » consistent à « doper » la recherche en introduisant automatiquement (et systématiquement) des « variantes » sur chacun des mots-clés saisis. Les variations portent essentiellement sur les finales de ces mots (lemmatisation) mais sont parfois plus obscures et font varier des lettres à l'intérieur des mots, sans qu'on puisse toujours y trouver une motivation phonologique ou lexicologique (p. ex. Charles peut matcher charges ou charmes).

Ces élargissements ne sont pas sous le contrôle de l'utilisateur et ne sont pas non plus contrôlables via l'API (voir ci-dessous la discussion sur les limitations de l'API).

La documentation de l'API précise également que l'opérateur adj entraîne de facto un match exact sur les mots saisis, sans opération d'approximation comme c'est le cas avec les opérateurs all et any. Elle précise de plus que la présence, dans une requête composée, d'un opérateur adj annule toute opération d'élargissement pour toute autre requête simple également présente.

Opposition des opérateurs all et adj :

allquery=(dc.title all "conserves alimentaires")

produit, parmi les résultats, des titres de documents comme :

  • Manuel pratique et industriel des conserves alimentaires : légumes, fruits, viandes et poissons / par Raphaël de Noter
  • Hygiène alimentaire. De la fabrication des conserves de viande, par F. Sauvain,...
  • L'Art de conserver les substances alimentaires solides ou liquides, traduit de l'allemand, de Jean Charles Leuchs,... par A. Bulos
  • La conserve alimentaire : traité pratique de fabrication / par Auguste Corthay,..

Les deux mots de la requêtes figurent toujours dans les résultats produits, mais dans un ordre quelconque et sous des formes qui peuvent varier par rapport à la forme de la requête (p. ex. conservesconserver ou conservesconserve).

adjquery=(dc.title adj "conserves alimentaires")

produit, parmi les résultats, le premier résultat dans la série précédente (match exact) mais aucun autre de cette série, du fait qu'on ne s'autorise ni permutation de l'ordre des mots ni élargissement (ou match approché) sur les mots-clés d'origine.

On l'a dit, les élargissements effectués sur les mots-clés (opérateurs all et any) peuvent se révéler abusifs, du moins si l'on en juge par les extraits des textes restitués dans les résultats. C'est particulièrement le cas avec les mots courts où les effets de bord peuvent être manifestes. Ainsi des requêtes avec le mot « île(s) » (p. ex. « îles de la mer Rouge »), qui renvoient très souvent des extraits de texte avec les pronoms « il » ou « ils », comme dans cet exemple produit en réponse à la requête « îles de la mer Rouge » :

Mais — et c'est le point le plus épineux — il est difficile de déterminer si ces variantes des mots d'origine ont effectivement contribué à la production du document en résultat ou si elles n'ont pas été produites plutôt lors de la recherche (lors d'une seconde passe) des contextes d'occurrence de ces mots dans le document en question. Voir la discussion dans la section Limitations de l'API ci-dessous.

D'autres exemples similaires que j'ai rencontrés au cours de mes tests :

  • puits identifie puis ;
  • laitle/les ;
  • lieuelieu(x) ;
  • hautau(x), eau, etc.

Certains élargissements sont plus inattendus mais trouvent parfois leur explication, comme œufsest. Ici, l'assimilation se fait via la forme eft (œufseftest), qui provient d'une erreur d'analyse du 's' long (U+017F) dans le mot est ('eſt' en graphie ancienne). Même remarque pour l'assimilation fuitesuite (sous la forme 'ſuite' mal reconnue).

Certains autres élargissements sont manifestement dus à des interventions explicites des concepteurs du moteur de recherche de Gallica, comme pour l'assimilation impératriceempereur. On peut dès lors supposer qu'il existe, sous une forme ou sous une autre, une base de « synonymes », mise à contribution lors de ces élargissements. Cette base n'est pas accessible ni contrôlable via l'API.

Requêtes composées

Les requêtes composées se justifient dès lors que la recherche porte simultanément sur plusieurs champs (ou index) ou simultanément sur plusieurs mots-clés composés différents pour un même champ. Des requêtes simples ou composées sont alors reliées entre elles par des opérateurs logiques booléens. Ces opérateurs sont :

  • and, le ET logique : les deux requêtes reliées doivent être vérifiées toutes deux
  • or, le OU logique : l'une au moins des deux requêtes reliées doit être vérifiée
  • not, opérateur de négation, toujours associé à l'un des deux opérateurs précédents (and not, or not) : la requête qui suit ne doit pas être vérifiée.

En cas d'accumulation de combinaisons logiques, des parenthèses permettent de contrôler l'ordre d'application des opérations. La documentation de l'API ne précise pas l'ordre de précédence par défaut (p. ex. pour interpréter p AND q OR r) ; ma recommandation est de systématiser l'emploi des parenthèses.

Recherche sur un auteur et un titre :

query=(dc.creator all "diderot") AND (dc.title all "jacques fataliste")

Recherche sur une description et un sujet :

query=(dc.description all "grande armee") AND (dc.subject all "constantinople")

Exclusion d'un sujet (les documents dont la description ou le sujet mentionnent "Sainte-Hélène" sauf ceux dont le sujet mentionne "Napoléon") :

query=( (dc.description all "sainte hélène") OR (dc.subject all "sainte hélène") ) AND NOT (dc.subject all "napoléon")

Note 1 — On ne garantit pas que les documents retrouvés ne parleront pas de Napoléon ; on garantit seulement que les documents retrouvés n'auront pas été indexés avec un sujet mentionnant Napoléon.

Note 2 — L'opérateur de négation opère sur des champs effectivement indexés et sera sans effet s'il est appliqué à un champ vide. C'est un point à garder en mémoire lorsqu'on entend par exemple exclure la présence d'un mot-clé de l'index text : les documents exclus seront ceux dont le champ text comprend effectivement des mots-clés, à l'exclusion du mot-clé concerné. Si ce champ est vide, l'exclusion sera inopérante et le document pourra être produit en résultat, au risque d'apparaître comme un résultat contre-intuitif.

Un exemple typique d'utilisation de l'opérateur OR se retrouve dans la recherche des auteurs de documents. Le nom des auteurs peut en effet apparaître soit dans le champ Auteur (dc.creator) soit dans le champ Contributeur (dc.contributor), lorsqu'il s'agit d'un ouvrage collectif. Kaliga, comme Gallica, génère systématiquement un OR sur ces deux champs. La requête sur Diderot ci-dessus s'écrit en fait comme suit :

query=( (dc.creator all "diderot") OR (dc.contributor all "diderot") ) AND (dc.title all "jacques fataliste")

Recherche sur deux auteurs :

query=(dc.creator all "honoré d'urfé") OR (dc.creator all "guez de balzac")

Ce genre de requêtes peut se justifier pour préserver les noms complets de chacun des auteurs recherchés et éviter les interpolations dans les résultats avec une requête simple du type :

query=(dc.creator any "honoré d'urfé guez de balzac")

qui ne manquerait sans doute pas de renvoyer des résultats pour Honoré de Balzac.

Aujourd'hui, Kaliga ne génère pas de requêtes composées autres que celles relatives aux auteurs / contributeurs.

Contextes d'occurrence des mots-clés (les snippets)

L'API de recherche (via son verbe searchRetrieve) ne permet pas de connaître les contextes d'occurrence des mots-clés de la requête initiale qui ont permis de retrouver tel ou tel résultat.

Avec cette API, un résultat est essentiellement une notice documentaire, c'est-à-dire un identifiant de document accompagné de ses attributs statiques (titre, auteur, date de publication, etc.). Pour certains documents textuels, il existe même un attribut text donnant accès à la version numérisée du texte.

Pour savoir quels fragments du texte ont permis de produire le document en résultat, il est nécessaire de procéder à une nouvelle exploration, cette fois-ci du document lui-même, via sa version océrisée.

Cette exploration est possible via une seconde API, totalement distincte de la première, l'API Document de Gallica, également rendue publique, qu'on pourra consulter ici

On s'intéressera, dans cette seconde API, à la section intitulée Service d'occurrences de recherche, qui détaille la façon très simple d'interroger un document : « Ce service renvoie une liste d’occurrences d’un mot donné au sein d’un document. ».

Il s'agit à nouveau d'exécuter une requête HTTP et de récupérer un flux XML. Cette requête use du verbe ContentSearch et prend en paramètres, d'une part, l'identifiant du document à explorer (ark) et, d'autre part, un mot-clé (query). Le schéma de la requête HTTP est le suivant :

http://gallica.bnf.fr/services/ContentSearch?ark=[identifiant]&query=[mot-clé]

• L'identifiant du document correspond à la dernière partie de l'URI de ce document, soit la partie variable apparaissant après les parties figées http://gallica.bnf.fr/ark:/12148/.

Dans un URI typique comme :

http://gallica.bnf.fr/ark:/12148/bpt6k110022v

l'identifiant du document est donc le segment 'bpt6k110022v'.

Note — Cet identifiant est légèrement différent dans le cas des fascicules de périodiques (p. ex. tel numéro d'un quotidien). Pour ces documents, l'identifiant est suivi de la marque /date, comme dans cet exemple :

http://gallica.bnf.fr/ark:/12148/cb34349428x/date

où l'identifiant est donc 'cb34349428x/date'.

• Les mots-clés de recherche sont mentionnés directement derrière le paramètre query=, sans parenthèses ni guillemets. S'il s'agit d'un mot-clé composé, les différents composants sont séparés par des espaces encodés en hexadécimal (code %20).

Exemple de requête via cette API :

http://gallica.bnf.fr/services/ContentSearch?ark=bpt6k6420517q&query=r%C3%A9gence%20de%20fiume

pour rechercher les occurrences du mot-clé « Régence de Fiume » dans le document dont l'identifiant est http://gallica.bnf.fr/ark:/12148/bpt6k6420517q.

Le flux XML récupéré est également assez simple (un seul espace de noms pour les balises). Il s'agit d'une suite de résultats (item) correspondant à autant de contextes d'apparition des mots-clés recherchés, les contextes étant stockés dans des balises content. Dans ces contextes, les occurrences de ces mots-clés sont répérables par un balisage spécifique, de la forme suivante :

<span class='highlight'>[mot-clé]</span>

Note — Le balisage en question ne porte jamais que sur des chaînes de caractères simples. Si le mot-clé était un mot-clé composé, chaque composant sera balisé indépendamment (à l'exception des mots vides — il semble donc qu'il existe, au moins pour cette API, une liste de « stop words », mais cette liste n'est pas documentée).

Voici un extrait du flux XML généré via cette API pour la requête ci-dessus :

<item> <altoid/> <p_id>PAG_7</p_id> <p_width/> <p_height/> <content> ceci le général Caviglia, commandant la Vénétie Julienne, a donné communication, sur l'ordre du Gouvernement, au commandant de la <span class='highlight'>Régence</span> de <span class='highlight'>Fiume</span> pour qu'il connaisse officiellement la volonté et les ordres de la Patrie(...)De l'Adriatique et de <span class='highlight'>Fiume</span> vendus à la Serbie, répon- dent certains hommes de la <span class='highlight'>Régence</span> </content> <events/> </item>

Et sa traduction sur la page de résultats finale :

Architecture du démonstrateur Kaliga

Sans rentrer dans les détails, je souhaitais donner un aperçu de l'architecture retenue pour le démonstrateur Kaliga. Ce n'est sûrement pas la seule envisageable, ni forcément la meilleure, mais elle m'a permis de mettre en œuvre quelques éléments intéressants.

Schéma d'ensemble

L'enchaînement général est le suivant :

  1. Un formulaire HTML permet de récupérer une saisie d'un utilisateur (mots-clés et critères de recherche).
  2. Cette saisie est traduite en une requête HTTP adressée au serveur Gallica (requête curl).
  3. La réponse XML renvoyée par le serveur est exploitée pour :
    1. déterminer le nombre total de résultats générés pour cette requête (élément srw:numberOfRecords) ;
    2. extraire les éléments constitutifs de la 1re page de résultats : ces éléments sont stockés dans une base de données temporaire (base SQLite) ;
    3. construire et afficher la 1re page de résultats (en extrayant les données de la base SQLite).
  4. Si le nombre total de résultats excède ce qui peut être affiché sur la 1re page, on prépare les pages de résultats suivantes :
    1. génération des requêtes HTTP complémentaires (elles sont ici volontairement limitées à 10) par incrémentation du paramètre SRU startRecord ;
    2. adressage parallèle au serveur Gallica de toutes ces requêtes HTTP complémentaires (requête curl_multi) ;
    3. mise en place d'une boucle reprenant le schéma de la 1re page ;
      pour chaque réponse XML renvoyée par le serveur Gallica :
      • extraction des éléments constitutifs de la page de résultats correspondante (page 2, page 3, etc.) : ces éléments sont stockés dans la base de données SQLite déjà active ;
    4. affichage au pied de la 1re page de résultat de liens de pagination permettant de passer à l'une quelconque des pages de résultats ainsi préconstruites.
  5. Si l'utilisateur demande à naviguer vers une autre page de résultats que la page courante (rappel du script courant avec un nouveau numéro de page), on exploite alors les données de la base SQLite locale pour générer et afficher la page correspondante.

Tant que l'utilisateur n'effectue pas une nouvelle recherche, le contexte (c'est-à-dire avant tout la base SQLite) est maintenu à l'aide de variables de session. Ces variables sont effacées dès qu'une nouvelle recherche est demandée, de même qu'est effacée la base temporaire SQLite.

La latence qu'on peut parfois observer à l'affichage d'une page de résultats est due au temps de chargement des vignettes des documents, qui restent hébergées sur les serveurs Gallica.

Contexte d'occurrence des mots-clés

Une particularité (qui explique le délai significatif qu'on peut observer lors du traitement de certaines requêtes) a trait à la récupération des contextes d'occurrence des mots-clés.

Lorsqu'une demande d'affichage de ces contextes est faite par l'utilisateur, un appel à l'API Document de Gallica a lieu lors de la génération de la page de résultats : pour chaque résultat à afficher, une requête HTTP est émise et un traitement d'évaluation et de mise en forme des résultats est engagé. Les extraits ne sont donc pas mis en cache (p. ex. dans la base SQLite) et ils doivent être recherchés à chaque affichage d'une nouvelle page de résultats.

Cette partie de l'application pourrait sans aucun doute être améliorée pour minorer le temps de latence observé, même s'il semble que les délais de réponse des serveurs Gallica soient incompressibles (de l'ordre d'un 1/10e de seconde en moyenne pour obtenir un XML via cette API).

Cas des périodiques regroupés

J'ai dû me résoudre à supprimer le paramètre de requête commandant le regroupement ou le dégroupement des périodiques : pour une raison que je n'ai pas encore totalement élucidée, les requêtes adressées au serveur Gallica mentionnant le paramètre collapsing=true provoquent presque systématiquement des erreurs dans l'application Kaliga (au niveau de l'analyse du XML reçu).

Je ne désespère pas de comprendre un jour ce qui se joue là, mais, en attendant, toutes les requêtes à l'API Gallica se font avec le paramètre collapsing=false : les périodiques sont donc traités au numéro, sans regroupement.

Limitations de l'API et autres questions

Au cours des tests que j'ai pu mener, j'ai rencontré plusieurs limitations aux incidences plus ou moins marquantes. Certaines sont liées à l'API, d'autres sont liées au moteur de recherche lui-même. En voici une liste informelle, sujettte bien entendu à discussion.

Volumétrie

L'API limite à 50 le nombre maximal de réponses possibles à une requête qui lui est adressée. Pour les requêtes dont le résultat comprend plus de 50 documents (ce nombre est indiqué dans la balise <srw:numberOfRecords> figurant en tête du flux XML), il est possible d'obtenir plus de 50 réponses en réitérant la même requête avec un décalage du rang du premier résultat demandé (p. ex. en commençant à 51 : startRecord=51).

L'inconvénient évidemment est d'avoir à rejouer la même requête HTTP autant de fois que nécessaire. Dans le cas précis du démonstrateur Kaliga, et plus généralement dans le cadre d'applications amateur, l'inconvénient n'est pas forcément rédhibitoire, puisqu'on peut imaginer de lancer ces requêtes successives de manière asynchrone. C'est ce que fait Kaliga en parallèlisant les requêtes à l'API Gallica via la famille des commandes curl_multi_*.

Note — Pour ne pas surcharger les serveurs de Gallica, Kaliga limite à 10 le nombre maximal de requêtes HTTP à adresser au serveur pour une même requête utilisateur ; ayant par ailleurs conservé le nombre par défaut de 15 résultats par requête unitaire, on n'affiche donc dans cette application jamais plus de 150 résultats en tout (mais on affiche un lien permettant de rejouer la requête sur l'application officielle).

Performances

Par rapport à ce que l'on observe sur le site même de Gallica, le traitement des requêtes (c'est-à-dire ici le délai d'obtention des résultats XML depuis le serveur Gallica) peut s'avérer assez long, notamment lorsque la requête n'a pas été mise en cache. Ces APIs ne semblent pas en l'état destinées à un usage intensif.

Par ailleurs, les serveurs Gallica se révèlent souvent saturés et les codes retour 500 ne sont pas rares.

Les performances d'une interrogation via l'API sont particulièrement dégradées lorsqu'on souhaite obtenir les contextes d'occurrence des mots-clés recherchés, comme c'est le cas dans Kaliga (j'ai finalement rendu l'affichage de ces contextes facultatifs : il faut les demander explicitement dans le formulaire de recherche). En effet, ces contextes ne sont pas restitués avec le flux XML de la requête initiale. Ils doivent être récupérés par le biais d'une nouvelle requête adressée à une autre API (l'API Document de Gallica).

Plus précisément et comme indiqué plus haut, il s'agit, pour chaque document retourné dans le premier flux XML, d'en récupérer l'identifiant, puis d'exécuter pour cet identifiant une recherche des contextes d'occurrence avec les mêmes mots-clés, puis de récupérer et d'analyser ce second flux XML. La pénalité en temps de traitement est conséquente (chaque document donne lieu à une nouvelle requête HTTP, d'un coût moyen d'environ 1/10e de seconde, soit donc 1 seconde ½ par lot de quinze résultats).

Il existe sûrement diverses manières d'optimiser mon code, mais, en l'état des API, on n'échappera pas à un double aller-retour entre le client et le serveur pour obtenir, en complément des résultats de base, ces données de contextes.

Normalisation des mots-clés

La documentation est muette sur ce plan et il pourrait être utile de détailler ce qui est requis ou non en entrée, à la génération de la requête HTTP au serveur, de même que les traitements de normalisation qui sont éventuellement opérés côté moteur. Ainsi, pour les saisies en alphabet latin, du rôle des signes diacritiques : ont-ils une utilité, un pouvoir de discrimination ? Leur exploitation s'effectue-t-elle en fonction de la langue des documents ciblés ? De même, identifie-t-on parmi les mots de la requête des mots « vides » ?

La question pourrait être de déterminer s'il est utile ou non d'envisager de générer des formes alternatives des mots-clés avant l'appel du moteur du recherche pour produire explicitement les variantes à rechercher. Un exemple serait celui des chaînes numériques, et notamment les variantes entre chiffres arabes et chiffres romains qui ne semblent pas être prises en charge par le moteur de recherche. Par exemple, les requêtes constitution de l'an III et constitution de l'an 3 (les deux formes sont attestées) ne sont pas assimilées l'une à l'autre et les résultats diffèrent.

Élargissements sur les mots-clés

Une autre limitation (imputable au moteur plutôt qu'à l'API) tient à la non-maîtrise des élargissements opérés (ou non) sur les mots-clés recherchés. On l'a vu plus haut, ces élargissements sont systématiquement activés pour certains opérateurs de requête et systématiquement désactivés pour d'autres. La portée de ces élargissements n'est pas documentée et ce comportement ne peut pas être contrôlé programmatiquement.

Contexte d'occurrence des mots-clés

(Et affichage d'extraits des documents dans les résultats.)

Outre les inconvénients du requêtage séparé pour récupérer ces contextes, les résultats obtenus présentent par eux-mêmes des limitations fortes dues à l'absence de critères d'ordonnancement (du moins perceptibles par un lecteur humain). L'ordre d'apparition de ces extraits ne correspond ni à la pagination du document interrogé (les pages indiquées ne suivent aucun ordre particulier, même si des régularités sont avérées) ni à un éventuel tri par pertinence (les premiers contextes listés ne sont ni plus ni moins pertinents que les derniers).

La documentation de l'API évoque bien un attribut score (« L'attribut score est le score donné par le moteur de recherche, et n'est pas exploité dans l'interface. »). Or, l'exemple fourni dans la documentation ne fait pas apparaître cet attribut et, pour ma part, je n'en ai jamais trouvé la trace au cours de mes recherches.

En revanche, la documentation de l'API évoque un « capping » du nombre de contextes renvoyés (« startResult : (paramètre optionnel) il est utiliser [sic] pour paginer l'intégralité des résultat sachant que nous bloquons à 10 le nombre d'éléments retourné par le service. »), mais il semblerait que ce ne soit pas le cas : les requêtes soumises via l'API renvoient un nombre de contextes apparemment non limité (cette requête [dérivée de celle de la documentation] renvoie 29 résultats ; cette autre en renvoie 378 – tests réalisés le 9 mai 2018).

Note — L'application Kaliga limite les affichages à au plus trois contextes d'occurrence par document. Pour ne pas dépendre de l'ordre dans le fichier XML source, le tirage de ces trois contextes se fait de manière aléatoire ; aussi, en rejouant la même requête, n'obtient-on pas nécessairement les mêmes extraits pour un même document.

On observe en particulier que les contextes ne sont pas ordonnancés par la proximité des différents mots-clés d'une requête composée. Ainsi, pour une requête composée de plusieurs mots-clés (par exemple « Prusse orientale »), la co-occurence des différents mots au sein du même contexte ne favorise nullement le rang de ce contexte parmi l'ensemble des contextes. C'est pareillement le cas sur le site de Gallica et il semble qu'il faille incriminer ici le comportement du moteur de recherche plutôt que celui de l'API.

Sur ce même sujet enfin, il n'est pas tout à fait certain que les occurrences de mots mises en relief dans les résultats soient toujours toutes des occurrences ayant permis de valider la recherche avec les mots-clés d'origine. Le soupçon naît des exemples renvoyés pour des recherches avec l'opérateur d'adjacence : en théorie cet opérateur requiert la co-occurrence côte à côte des mots-clés de la requête d'origine et neutralise tout élargissement sur ces mots ; or il s'avère que, dans les résultats, on trouve quantité d'exemples d'occurrences de mots isolés et/ou ayant subi des variations par rapport à la forme d'origine.

Ainsi une recherche sur « Prusse orientale » tel quel (opérateur d'adjacence) fera mettre en relief des occurrences isolées de l'un ou l'autre des deux mots, ou des variantes :

Cette question de l'identification des occurrences d'un mot ayant réellement concouru à la production d'un résultat est un problème récurrent dans les applications de recherche. Elle se pose dès lors que les post-traitements liés à l'affichage des résultats sont exécutés indépendamment des traitements de production des résultats, ce qui est le cas ici puisque les extraits sont fournis et restitués dans une deuxième passe, totalement indépendante de la première, une fois que les résultats initiaux ont été eux-mêmes restitués.

La garantie d'une identité de traitement lors des deux passes n'est pas toujours possible : la deuxième passe en particulier ne préserve pas l'opérateur initial appliqué aux mots-clés. Les requêtes sont en effet de la forme :

http://gallica.bnf.fr/services/ContentSearch?ark=bpt6k110022v&query=prusse%20orientale

où l'on peut supposer (au vu des résultats) que query=prusse%20orientale exécute en fait un OU logique sur chacun des mots (prusse ou orientale) avec activation des possibilités d'élargissement sur chacun des mots-clés.

C'est, je crois, l'un des points problématiques de l'application de recherche étudiée ici, en ce sens que la fonctionnalité mise en œuvre ne permet pas irréfutablement de comprendre pourquoi tel résultat a pu être produit. Si, fort heureusement, dans un grand nombre des cas, la question ne se pose pas (les résultats s'entendent d'eux-mêmes), il est vraisemblable que la question se pose dès lors qu'un résultat, disons surprenant, apparaît dans la liste : les outils mis à disposition de l'utilisateur pour se faire une idée ne lui seront pas toujours de bon secours.

Monographies vs. périodiques

Toujours dans le cadre de l'identification des contextes d'occurrence des mots-clés, l'API ne permet pas de simuler un traitement réalisé par l'application Gallica, à savoir récupérer des contextes de mots-clés lorsque les fascicules (ou numéros) d'un périodique sont regroupés sous le titre de ce périodique (paramètre de requête collapsing=true).

En effet, ou bien le regroupement des fascicules est activé (valeur par défaut) et il n'est plus possible de connaître les fascicules individuels ayant permis de produire le périodique en résultat pour y chercher des contextes d'occurrence des mots-clés ; ou bien ce regroupement n'est pas fait (collapsing=false), permettant à chaque fascicule du périodique d'être traité comme un document autonome et donc permettant de rechercher les contextes des mots-clés pour chaque fascicule. Dans ce second cas, le périodique n'est plus identifié en tant que tel dans les résultats et le nombre total de résultats peut augmenter très significativement.

À titre d'exemple, une requête sur « scandale de Panama » passe de 5 823 résultats, dans le premier cas (numéros de périodiques regroupés), à 120 237 résultats dans le second (numéros de périodiques dégroupés) — tests effectués le 12 février 2018.

Qualité de l'OCR

La documentation de l'API évoque un index ocrquality restituant un indice de qualité de la numérisation du document correspondant :

« ocrquality : il s'agit d'un champ texte libre permettant la recherche dans l'index de qualité de la numérisation. La valeur est comprise entre 0 et 100. Exemple ocrquality any "099.99" »

Je n'ai pas trouvé trace de ce champ dans le flux XML et l'exécution de la requête donnée en exemple provoque une erreur à l'exécution.

Il semblerait que l'information soit en fait celle qu'on retrouve dans le champ nqamoyen (avec une syntaxe de valeur légèrement différente), comme on peut l'inférer d'une autre information fournie plus loin dans la documentation : « Le mapping critère api Catégories vers critères cql sru […] nqamoyen -> ocr.quality ».

En pratique donc, pour évaluer la qualité de la numérisation d'un document (ce qui est nécessaire pour déterminer la disponibilité du champ plein texte d'un document), on utilisera le champ nqamoyen.

À l'usage, il semblerait que le champ ocrquality ne puisse être utilisé que comme critère de tri des résultats, ainsi qu'une autre remarque dans la documentation le laisse entendre :

« À la fin du paramètre query, une fois que la requête est construite, on peut ajouter, toujours en CQL, un critère de tri. Voici les possibilités : […] ocr.quality/sort.descending : tri par ordre décroissant de la qualité de l'ocr »

De fait, une requête comme la suivante s'exécute conformément aux attentes :

query=(dc.title all "lettres persanes") sortby ocr.quality/sort.descending

À l'heure actuelle, Kaliga ne fait pas usage des critères de tri.

Pour accéder au démonstrateur Kaliga de l'API Gallica, c'est ici :
https://gregoire.clemencin.fr/kaliga

Post mis à jour le 9 mai 2018.

Normandie, Pays d'Auge. Décembre 2014.