ADR 0097 – Plongements locaux : « Ressemble à », projet par le sens, doublons
Accepté (2026-10-17, fil B du jalon 4, cadre : ADR 0086 « étage 2 »). Livré en deux temps le même jour : le circuit complet et son interface, testé avec un faux modèle ; puis l'adaptateur ONNX Runtime + `multilingual-e5-small`, vérifié de bout en bout avec le vrai modèle (téléchargement accepté par Aurélien).
ADR 0097 – Plongements locaux : « Ressemble à », projet par le sens, doublons
Statut : Accepté (2026-10-17, fil B du jalon 4, cadre : ADR 0086 « étage 2 »). Livré en deux temps le même jour : le circuit complet et son interface, testé avec un faux modèle ; puis l’adaptateur ONNX Runtime + multilingual-e5-small, vérifié de bout en bout avec le vrai modèle (téléchargement accepté par Aurélien).
Numéro hors de la plage 0090 à 0093 réservée au fil B par l’ADR 0086 (les quatre premières lignes ont pris les quatre numéros) et de la plage 0094 à 0096 du fil C (0094 et 0095 y sont pris) : le premier numéro libre après les deux plages.
Décision
Inactif par défaut. app.assistant.embeddings.enabled=false (AssistantProperties). Sans modèle l’application se comporte comme avant : aucun calcul, GET /api/nodes/{id}/similar rend [], CapturePreview.similar est vide, GET /api/assist/capabilities dit embeddings: false. Réglages : model (dossier du modèle, /data/models/multilingual-e5-small), similar (0,87, texte cherché contre un nœud), similar-node (0,91, deux nœuds), project (0,86, projet par le sens), duplicate (0,95, doublon probable), max-similar (5). Un modèle absent ou illisible n’empêche pas de démarrer : l’erreur est journalisée et les plongements restent inactifs.
Seuils calibrés sur le vrai modèle. Les similarités de e5 sont comprimées : des textes sans rapport restent à 0,83 à 0,85 (requête contre passage) et 0,86 à 0,90 (deux passages) ; des textes de même sujet dépassent 0,87 et 0,91 (« Acheter de la peinture pour la chambre » contre « Rénovation chambre » : 0,869 ; « Réviser l’auto » contre « Entretien des véhicules » : 0,903) ; un quasi-doublon (« Révision de la voiture » contre « Révision voiture ») atteint 0,955 et 0,988. Les seuils par défaut en découlent ; un autre modèle impose de les recalibrer.
Stockage. Migration V18__node_embedding.sql : node_embedding(node_id, model, text_hash, vector real[], updated_at), clé (node_id, model), suppression en cascade. Pas de pgvector (image PostgreSQL, sauvegarde et restauration inchangées) ; la similarité cosinus se calcule en mémoire sur des vecteurs normalisés (produit scalaire), suffisant au volume personnel. Donnée dérivée : ni événement, ni export (recalculable).
Calcul. Texte embarqué = titre + 1 000 premiers caractères du corps. NodeService publie NodeTextChanged à la création et à la modification ; EmbeddingService calcule après validation de la transaction (jamais en échec de l’écriture : l’erreur est journalisée) et seulement si l’empreinte SHA-256 du texte (avec l’identifiant du modèle) a changé. Rattrapage au démarrage dans un fil virtuel, après l’ouverture du port (contrairement à BodyIndexReconciler, car un modèle de cette taille calcule en quelques dizaines de millisecondes par nœud) ; les vecteurs d’un autre modèle sont supprimés.
Usages.
GET /api/nodes/{id}/similar: nœuds proches avecscoreetduplicate(score ≥duplicate).- Aperçu de capture :
similar(nœuds proches du titre) pour signaler un doublon ou un sujet voisin avant de créer. - Projet par le sens : quand aucun mot ne désigne de projet (ADR 0090) et qu’aucun champ n’est saisi, le projet le plus proche parmi les candidats du champ référence devient une devinette d’origine
similar; un ex æquo n’en désigne aucun.
Abstraction. TextEmbedder (modelId, embedPassage, embedQuery) : un bean suffit à brancher un modèle. Les tests utilisent un faux modèle (sac de mots hachés avec synonymes).
Modèle et adaptateur
- Modèle :
multilingual-e5-small(384 dimensions, 12 couches), version ONNX quantifiée de Xenova (model_quantized.onnx, environ 118 Mo) ettokenizer.json, posés parscripts/fetch-embedding-model.sh(à lancer à la main ; empreintes SHA-256 vérifiées ; jamais de téléchargement par l’application). Dans l’image, le dossier est/data/models/multilingual-e5-small(volume/data) ; en développement,data/models/…(ignoré par git). OnnxTextEmbedder: ONNX Runtime 1.22 dans la JVM (la dépendance ajoute environ 140 Mo au jar, bibliothèques natives de toutes les plateformes ; l’imageeclipse-temurinest en glibc, ce qui convient), préfixesquery:etpassage:, moyenne des vecteurs de jetons puis norme 1 (le masque est plein : un texte par appel).UnigramTokenizer: le tokenizer SentencePiece de XLM-RoBERTa en Java pur, lu danstokenizer.json(Viterbi sur les scores,▁devant chaque mot,<s>et</s>, 512 jetons au plus). Une bibliothèque de tokenizers (DJL) aurait téléchargé du code natif à l’exécution. Normalisation : NFKC de Java à la place de la tablenmt_nfkcde SentencePiece. Vérifié : les identifiants de jetons de 9 phrases (accents, ligatures,ß,東京, apostrophes, espaces multiples, tabulation) sont identiques à ceux de la bibliothèque de référence de HuggingFace (OnnxTextEmbedderTest, empreintes danssrc/test/resources/assist/e5-reference-ids.json).- Tests :
OnnxTextEmbedderTest(jetons, norme 1, sens) etEmbeddingOnnxIT(démarrage, calcul à l’écriture, projet par le sens, proches, doublon) s’ignorent tant que le modèle n’est pas installé : la CI sans modèle reste verte.
Reste à faire
- Interface : section « Ressemble à » de la fiche (hors des fichiers du fil B).
- Mesurer sur un vrai jeu de notes (quelques milliers) le temps de rattrapage au démarrage et la mémoire du cache de vecteurs.
Alternatives écartées
- pgvector : change l’image et la procédure de sauvegarde pour un gain nul à ce volume.
- Recalcul synchrone dans la transaction d’écriture : un modèle lent ou en panne bloquerait la saisie.
- Rattrapage bloquant au démarrage : un premier lancement avec des milliers de nœuds retarderait l’ouverture du port.
Limites connues
- Le cache des vecteurs est chargé au premier usage et mis à jour à chaque calcul ; un nœud mis à la corbeille y reste jusqu’au redémarrage mais n’est jamais rendu (son titre n’est plus lisible).
- Comparaison exhaustive : O(nombre de nœuds) par requête, à revoir (index approché) au-delà de quelques dizaines de milliers de nœuds.