Little hardware, Big data 2/5

les différents logos des produits utilisés

Voici la deuxième partie de la présentation du projet "Little hardware, Big data", qui est un moteur de recherche web fonctionnant sur Raspberry Pi 2 (Un mini-ordinateur à petit prix). A part pour l'exercice technique, y a-t-il une raison de coder un moteur de recherche en 2016?

Little Hardware, Big Data

Les moteurs de recherche du 'marché'

Dans tout moteur de recherche, le résultat le plus pertinent est affiché en premier. Le cœur d'un moteur de recherche est sa manière d'évaluer la pertinence d'un document en fonction de la recherche d'un utilisateur. L'évaluation de la pertinence est dit le 'classement'. Le classement des moteurs de recherche exploite des aspects comme la présence des termes recherchés, la sémantique, des informations sur l'utilisateur, la popularité du document, le paiement, etc.

Time : le moteur de recherche ludique

Ce projet-ci est un moteur de recherche à classement chronologique. Le classement est fait par évaluation des dates contenues dans les textes. Le moteur de recherche renvoi des phrases, extraites de leur contexte. Ces phrases sont triées chronologiquement par la date contenue dans la phrase. Les phrases ne contenant pas de dates sont ignorées. Ainsi le document le plus pertinent pour la recherche "chat" sera "Les premières découvertes paléontologiques situaient les premiers foyers de domestication du chat en Égypte, vers 2000 av. J.-C.". et en dernière page on trouvera "Cet homme s'est fait “pirater” son ordinateur par son chat en 2016".

Composants

Cet article traite de la partie '2. Analyser', celle qui va exploiter des textes bruts pour fournir des documents "phrase+date", et on va ensuite indexer la date.

  1. Le Downloader : Récupérer un maximum de textes quelque-soit leur format. (Article 1)
  2. L' Analyser : Exploiter les données fournies par le Downloader pour construire un index Lucene constitué de phrases datées. (Article 2, celui-ci)
  3. Le Back : Mettre en place un service REST avec les fonctions de recherche, de statistiques, de synonymie, etc… (Article 3 à venir)
  4. Le Front : Construire une Single Page Application qui présente les données issues du Back. (Article 4 à venir, application accessible sur histoires.xyz)
  5. La Prod : Déployer l’application sur internet d’une manière ‘maison’ (un nom de domaine + une freebox + un raspberry). (Article 5 à venir)

2 - L' Analyser

L'analyser est un composant sous la forme d'une librairie Java. Cette librairie est ensuite utilisée dans plusieurs contextes différents.

Dans le code

Pour convertir un texte brut en données indexées chronologiquement, il faut effectuer la transformation suivante: Texte => Paragraphes => Phrases => Phrases datées (les phrases sans date sont ignorées).

Obtenir des phrases d'un texte brut

Premièrement, on extrait les paragraphes d'un texte, puis les phrases d'un paragraphe, au moyen d'expressions régulières simples.

-Pour obtenir des paragraphes:

String text = "text with paragraph";
Pattern pattern = Pattern.compile("[\r\n\t]+");
for(String paragraph : pattern.split(text)){
	//[handle a paragraph]
}

-Pour obtenir des phrases:

String paragraph = "text with phrases";
Pattern pattern = Pattern.compile("(?<=(?<!( (av|mr|dr|jc|JC|J\\.-C)))(\\.|\\?|!|•|\\|)) +");
for(String phrase : pattern.split(paragraph)){
	//[handle a phrase]
}

Obtenir les dates contenues dans des phrases

Deuxièmement, on convertit une phrase en une liste de phrases datées, en implémentant cette méthode:

List<DatedPhrase> findDatesInPhrase(final String phrase);

La liste sera vide si aucune date n'est trouvée. La liste contiendra plusieurs fois la même phrase si on trouve plusieurs dates, comme par exemple dans: "On a découvert en 1990 que le Big-Bang eu lieu il y a 15 milliards d'années".

Il s'agit de trouver toutes les dates d'une phrase. On utilise là aussi des expressions régulières. Cette partie est complexe car on adresse une langue qui est presque aussi variée que les auteurs qui l'utilisent. Cette partie évolue régulièrement, cette dernière ne sera forcément jamais à l'état fini, on se contente d'atteindre une qualité de détection satisfaisante avec le moins d'erreurs possible. L'indexation comporte 18 détecteurs, chacun avec sa propre regex, d'environ 50-100 caractères chacune.

La détection de dates contenues dans les phrases :

Il y a un enjeu à perfectionner la détection: inclure de nouvelles détections et exclure les erreurs, et pas l'inverse! Alors, une couverture par des tests unitaires de 100% des cas est essentielle pour ne pas régresser. Il existe actuellement 200 Tests Unitaires couvrant les combinaisons possibles avec les regex existantes. Il y a plus de TU que de regex, car une regex prends en compte plusieurs cas de figure. Chaque évolution apportée aux détecteurs entraîne plus de travail du côté TU que du côté code, d'où un besoin d'industrialisation des TU.

Il existe deux types d'erreurs:

  • Faire évoluer une regex permet de gérer de nouveaux cas mais risque de provoquer des effets de bords sur les cas existants (régression).
  • L'ajout d'une nouvelle regex permet de couvrir de nouveaux cas, mais certains cas entrent en collision avec les regex existantes.

Exemple de construction d'un détecteur:

private static final String START = "(^| |,|;)";
private static final String YEAR_FOUR = "[1-9][0-9]{3}";
private static final String END2 = "( ?;| ?, ?| ?\\.| ?[\\(\\[]|$)";
String regex = START + APRES_START + NUMBER_FLAG + YEAR_LONG + NEG_REF_OPT + END2; 
build(DateType.APRES, regex, new JCParser());

La méthode build va créer la détection spécifiée par les paramètres. L'enum DateType sert de clé d'identification de la détection. La regex est construite à partir de différentes constantes réutilisées. Le Parser transformera en nombre la valeur texte trouvée par la regex. La représentation des dates retenues est un long égal au nombre de jours par rapport à J.C. Ainsi on fait tenir sur un seul champ des valeurs à +-100 milliard d'années.

Exemple d'un test unitaire:

@Test
public void jc19() {
	assertOnly(DateType.APRES, yearIs(1990), "Or, après 1990, on a eu la surprise de constater le contraire.");
}

La méthode assertOnly est un helper qui va:

  • Passer la phrase "Or, après [...] contraire." sur tous les détecteurs.
  • Vérifier que le détecteur DateType.APRES trouve une date.
  • Vérifier que l'unique date retournée est égale à 1990 (yearIs(1990) s'occupe de convertir 1990 en unité standard, le nombre de jours par rapport à J.C.)
  • Vérifier que tous les autres détecteurs ne trouvent rien.

D'autres méthodes pour faire du test :

  • assertNone : Vérifier que rien n'est trouvé par aucun détecteur dans la phrase donnée
  • assertTwo: Vérifier que deux dates sont trouvées sur un détecteur donné et rien sur les autres détecteurs.
  • assertTwoDifferent: Vérifier que deux dates sont trouvées chacune par un détecteur différent, et rien sur les autres.

Avec cette construction autour du test unitaire, on peut travailler sereinement sur l'évolution des détecteurs de dates, qui est le point central de l'indexation de ce moteur de recherche.

Obtenir un index prêt à être utilisé

Troisièmement, on stocke les phrases datées dans un index Lucene. Cette partie s'occupe d'agglomérer de manière homogène les méta-données: l'auteur, la date d'écriture du texte, le titre/sous-titre, l'url de renvoi vers la source, sont récupérées différemment selon si on a affaire à un fichier epub, pdf, ou un crawl de site, puis stockées de la même manière dans l'index en vue de leur exploitation par le composant 3-Le Back, qui sera traité dans le prochain article.

Conclusion

J'ai présenté quelques points de détail d'une partie essentielle d'un moteur de recherche: son système de classement. La réponse à la question posée en introduction, est que la raison de coder un moteur de recherche en 2016 est de parcourir les informations d'une manière plus ludique. Le but de ce moteur de recherche est plutôt une navigation transverse, une flânerie curieuse, car on va plutôt mettre en corrélation les différents résultats entre eux plutôt qu'en sélectionner un seul. Les extraits issus de domaines d'études différents sont présentés à la suite, parfois sans autre sens que la chronologie, en fonction de la recherche tapée.

Le projet dans sa version actuelle est disponible ici et contient une base de données Lucene de 13 millions de documents. L'ensemble est déployée dans un Raspberry pi 2, et tout est fluide.

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Captcha *