Master 2, Bases de données avancées, année 2022

TD3 (Groupe ML)

L’objectif est de créer une fonction qui va extraire d’un texte les mots importants pour fournir des tags.

La spécification du problème consiste donc à prendre un texte, le décomposer en suite de mot et d’une manière ou d’une autre sélectionner un petit nombre de mot dans le texte.

Pour le texte tiré de la page wikipedia

L’asperge est une plante de la famille des Asparagaceae originaire de l’est du bassin méditerranéen. Connue des Romains, elle est cultivée comme plante potagère en France depuis le XVe siècle. Le terme désigne aussi ses pousses comestibles, qui proviennent de rhizomes d’où partent chaque année les bourgeons souterrains ou turions qui donnent naissance à des tiges s’élevant entre 1 et 1,5 mètre.

On peut avoir comme tag par exemple “asperge”, “potagère”, “Romains”, “rhizomes”.

Le but de ce TP et de déployer un modèle et de l’interconnecter à une base de donnée. La partie apprentissage est donc à améliorer dans un second temps une fois le déploiement réalisé.

Partie 1: Un CMS basique

Dans votre VM contenant PostgreSQL, créer une nouvelle base de donnée article contenant une table ou chaque ligne contient un identifiant d’article, un texte et une date de publication.

Dans votre VM middleware, créer un nouveau middleware permettant:

Réaliser votre schéma afin que ces opérations soient efficaces.

Télécharger le document json qui contient des extraits de Wikipédia et ajouter les à votre base de donnée.

Partie 1: Un algorithme qui tagues des textes

L’objectif est de tagguer automatiquement les textes qui sont dans la bases de données.

  1. Vous pouvez trouver ici un fichier json (compressé) contenant pour chaque mot sa fréquence d’apparition dans un corpus de texte.

En vous basant sur ce document, proposer une implémentation primitive et simple du problème de tags sous forme d’une fonction python tag qui prend en argument un texte retourne 5 mots du textes choisit d’une certaines manières..

  1. Ajoutez à votre schéma une table Tags qui associe des tags à chaque article. Ajoutez des indexes pour que la recherche par tags soit efficace, et pour que récupérer les tags d’un article soit efficace.

  2. Écrire un script qui exécute votre fonction tags sur chaque article de la base et qui insère le tag dans la base. Essayez autant que possible d’éviter les allés/retours entre la base de donnée et votre script. Mesurer le temps d’exécution de votre script.

  3. Comment intégrer votre fonction tag au middleware pour garantir que les tags soient insérés dynamiquement?

Partie 2: Une intégration simple de Python dans PostgreSQL

Pour éviter les A/R entre la base de donnée et le client Python, on peut intégrer directement à PostgreSQL la fonction tag.

  1. Installer le paquet postgresql-plpython3-12 qui l’installe l’extension plpython3u dans PostgreSQL. Activer l’extension à l’aide de la requête SQL CREATE EXTENSION plpython3u

  2. Lisez la documentation pour comprendre comment exécuter une fonction python dans PostgreSQL.

  3. En exécutant la fonction

CREATE OR REPLACE FUNCTION pyenv()
  RETURNS TEXT
AS $$
import os, getpass
return f"pwd: {os.getcwd()} user: {getpass.getuser()}"
$$ AS LANGUAGE plpython3u

En déduire quel utilisateur exécute ces processus et depuis quel répertoire.

  1. Activer le timing dans psql à l’aide de la commande \timing on. Comparer le temps d’exécution d’une requête SQL la plus simple possible avec le temps d’exécution pyenv. Estimez le surcout d’un appel à une fonction Python.

  2. Copiez le fichier lexique.json dans un répertoire accessible pour l’utilisateur qui exécute les fonctions Python et adapter votre code pour la fonction tag et importer là dans PostgreSQL.

  3. Estimez le coût d’un appel à cette fonction, proposez une piste d’amélioration.

  1. Combien coûte en RAM les appels à la fonction tag?

  2. Que ce passe-t-il en cas de nombreux appel concurrent qui utilisent cette fonction? Essayer avec 5 sessions ouvertes simultanément ayant chacune appelé la fonction tag au moins une fois (et maintenu ouverte). Constaté l’utilisation de la mémoire vive sur le serveur.

Partie 3: Une approche microservice

Il y a plusieurs limite à l’intégration simple de Python dans PostgreSQL.

  1. Le coût du lancement de Python et des sous-modules. Certains modules sont très long à être chargé:
    • numpy se charge en 200ms
    • scipy se charge en plus d’une seconde.
  2. Le coût de chargement en mémoire de données nécessaires (modèles par exemple, dictionnaires).

Ce coût est factorisable entre par sessions et donc il est possible de faire en sorte que le middleware maintienne ouvert la session avec la base de donnée. Cela n’est pas compatible avec CGI et nécessite d’autres type d’interface, comme FastCGI.

Pour contourner ce type de difficulté, la stratégie est de séparer le calcul réalisé de la base de donnée via une architecture de micro-service. Un micro-service est un calcul stocké dans un processus distant, voire sure une autre machine ou dans le cloud.

On peut utiliser l’intégration Python de PostgreSQL pour l’interface entre le micro-service et PostgreSQL notamment.

Structure simple d’un microservice

Un microservice peut avoir la forme d’une application web et être géré complètement via les protocoles http classique. Cela nécessite néanmoins de faire attention au type d’intégration de Python choisi. On va s’appuyer sur des mécanismes simples et faire un serveur TCP minimaliste.

Pour vous aider, voici un exemple de code utilisé pour le serveur:

class Handler(socketserver.StreamRequestHandler):
    def handle(self):
        data = json.loads(self.rfile.readline().strip().decode())
        print("log in", data)
        self.request.sendall(json.dumps(tag(data)).encode()+b"\n")

if __name__ == "__main__":
    HOST, PORT = "localhost", 9999
    print("Launching server:", HOST, PORT)
    with socketserver.TCPServer((HOST, PORT), Handler) as server:
        server.serve_forever()

Lancer le serveur sur votre machine hébergeant PostgreSQL et vérifier que tout marche bien à l’aide de telnet localhost 9999 pour interagir avec.

Vous pouvez intégrer le code client à l’aide de simple connexion socket:

HOST, PORT = "localhost", 9999
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
    sock.connect((HOST, PORT))
    f = sock.makefile("w")
    f.write(json.dumps(t)+"\n") # Attention à bien mettre le retour à ligne.
    f.close()
    f = sock.makefile("r")
    print(f.readline())
    f.close()

On obtient ainsi environ 1ms par requête sans les problèmes évoqués avant mais au détriment d’un coût d’administration plus grand.

Pour rendre le système résilient, il faut ajouter le lancement du serveur python dans un Daemon afin de le relancer automatiquement. Il est également possible d’améliorer le mécanisme de communication pour éviter la sérialisation/déserialisation.

En optimisant raisonnablement la communication entre processus, j’arrive à 30secondes pour passer au travers de tous les articles sans latence pour la première exécution.

Rechargement de module à chaud

Dans un micro-service modernes, on met en place des mécanismes pour pouvoir recharger à chaud le code exécuté. Cela passe par un micro-service capable de détecter que le module a été mis à jour et le cas échéant le recharger.

Pour mettre en place un rechargement à chaud du module faites:

  1. Mettre dans un fichier python à part nommé tags.py la fonction Python tags.

  2. Mettre en place un rechargement à chaud via:

    • la méthode stat du module os
    • le rechargement de module dynamique à l’aide de la méthode reload du module importlib

Partie 4: Apprendre un modèle plus complexe

Nous avons un modèle de tag très simple. Proposez des modèles plus complexe (et plus couteux à calculer et à déployer).


Mastodon