ruby > 2.0
# Mac OSX
brew install postgresql
# Ubuntu
sudo apt-get install postgresql-9.3
sudo su postgres -c psql
CREATE ROLE <username> SUPERUSER LOGIN;
\q
Récupérer le code :
git clone [email protected]:nemile/test-teamrise.git
Installer les dépendances :
bundle install
Enrichir la base de donée (voir seeds.rb):
rake db:create
rake db:migrate
rake db:seed
Naviguer vers l'index d'objectifs :
rails s
et naviguer vers http://localhost:3000/objectives
Pour générer la détection de type d'objectifs à partir des exemples fournis
rake objectives:detect_types
Un fichier results.csv
sera généré à la racine du projet.
La difficulté du modèle réside dans l’association récursive sur les objectifs. Nous avons fait le choix d’une base de donnée relationnelle, car une base de données NoSQL avec dénormalisation du modèle entraînerait des documents très lourds et une répétition trop importante des données, dangereuse pour la consistance et longue à mettre à jour.
Puisque le but de l’exercice est d’afficher un organigramme, une simple clé étrangère parent_id qui pointe vers le parent d’un objectif entraînerait N+1 requêtes en base à l’affichage. Le dataset fourni ne contient que 3 niveaux de profondeur et ne nécessiterait que peu de requêtes; mais pour une grosse entreprise (avec beaucoup d’objectifs) l’affichage de l’organigramme serait lent.
C’est pourquoi nous stockons nos objectifs en base sous forme de nested set. Le nested set utilise deux colonnes “droite” et “gauche” qui permettent de déterminer pour un objectif un intervalle d’ids à requêter pour avoir ses descendants, et de les récupérer en une seule requête. Nous réduisons ainsi le nombre de requêtes au nombre d’objectifs de plus bas niveau.
Pour cette exercice, nous n’avons pas ajouté de clé étrangère team_id sur la table objectives. Pour filtrer par équipe, nous récupérons d’abord l’équipe et effectuons une jointure sur la table objectives. Si nous voulions afficher tous les objectifs avec leur équipe, nous pourrions la rajouter, car la requête pour l’obtenir serait moins complexe que celle qui recherche le plus vieux parent avant et qui récupère son équipe. Dans cet exercice nous pouvons directement filtrer par équipe, donc nous l’avons omise.
Pour le filtrage par niveau, nous avons utilisé une colonne level
qui est remplie à l’ajout d’un objectif. Nous avons également rajouté un index sur cette colonne pour booster le filtrage.
Nous avons également réalisé un filtrage simple par état d’avancement. Pour ce faire nous comparons les colonnes progress_value et progress_target, et nous avons ajouté un index pour ces deux colonnes.
Afin de diminuer encore plus les requêtes en base, nous avons ajouté du cache sur le fichier app/views/objectives/_objective.html.erb
. Si un objectif enfant est mis à jour, la clé de cache pour son parent expire, car le paramètre touch: true
passé ici met à jour la colonne updated_at
de son parent, qui est utilisée dans la clé de cache.
l'appel à render
effectué dans index.html.erb contient une option cached: true
qui permet de fetcher depuis le cache avec un seul appel, ce qui réduit encore le chargement.
L'utilisation de rails/arel nous permet de bénéficier du protection contre l'injection SQL.
Le nested set est rapide en lecture, mais lent en écriture (il faut déterminer les valeurs à insérer dans les colonnes “droite” et “gauche’). Pour cette exercice ça n’est pas un problème, mais si l’application proposait l’ajout d’objectif n’importe où dans l’arbre suivi d’un ré-affichage, cela poserait une baisse de performance. Pour contrer ce problème, on pourrait utiliser une API qui effectue l’insertion de manière asynchrone pendant que l’arbre est mis à jour de façon optimiste côté client, avec une librairie JS comme ReactJS qui effectuerait le rendu rapidement grâce à son DOM virtuel.
Un petit module permettant d’automatiser la détection des types d’objectif à été ajouté au dossier lib/ du projet. Des résultats sont disponibles ici : https://docs.google.com/spreadsheets/d/1qozyFCqRES6bydRdB2rK9gZlLrIWCOjCaKUdw-WliPw/edit?usp=sharing
Son fonctionnement est basé sur des expressions régulières qui tentent de remplir les critères suivant :
- détecter des nombres correspondant à des quantités (ne pas matcher les potentielles dates)
- détecter des unités
- détecter des fréquences
A partir des chaines de caractères récupérées, nous pouvons déterminer le type d’un objectif.
Certains types d’objectifs on mal été détectés, pour plusieurs raisons :
- aucun système de détection de langue n’a été mis en place.
- les fréquences à matcher ne sont pas exhaustives.
Il faudrait améliorer ces points :
- rajouter des unités (unités SI par exemple), des fréquences.
- détecter si l’objectif concerne l’augmentation ou la diminution de sa valeur pour savoir si progress_start est 0 ou n’est pas déductible, i.e. rajouter la détection de termes comme “increase”, “below”...
De manière générale, il faut enrichir les différents lexiques utilisés pour maximiser les résultats.