➟ FLUXX BACKEND | backbone pour la réalisation de backend web

Fluxx Backend

Fluxx est une ossature pour toute application web moderne construite sur le paradigme de backend monolithique modulaire.

Il existe aussi une version frontend de l’ossature : Fluxx Frontend.

Qu’est-ce qu’un backend monolithique modulaire ?

Un backend monolithique modulaire est une architecture de développement de logiciel où toutes les fonctionnalités d’une application sont regroupées dans une seule application, ou “monolithe”, mais sont organisées de manière modulaire. Cela signifie que bien que toutes les parties de l’application résident dans un seul codebase et dépendent d’un seul déploiement, elles sont divisées en modules distincts qui encapsulent différentes fonctionnalités ou responsabilités.

Caractéristiques principales d’un backend monolithique modulaire :

Monolithe :

  • Codebase unique : Toute l’application est construite et déployée comme une seule unité.
  • Déploiement unique : Une seule version de l’application est déployée à la fois, ce qui simplifie le processus de déploiement.

Modularité :

  • Modules séparés : Le code est organisé en modules distincts, chacun avec une responsabilité ou une fonctionnalité spécifique.
  • Encapsulation : Chaque module encapsule ses données et sa logique métier, réduisant les dépendances directes entre les modules.
  • Interface définie : Les modules interagissent via des interfaces bien définies, facilitant la maintenance et l’évolution du code.

Avantages :

  • Maintenance Facilitée : La séparation en modules rend le code plus facile à comprendre et à maintenir. Les développeurs peuvent travailler sur un module sans affecter les autres.
  • Réutilisabilité : Les modules peuvent être réutilisés dans différentes parties de l’application ou même dans d’autres projets.
  • Testabilité : Les modules bien définis peuvent être testés de manière indépendante, améliorant ainsi la qualité du logiciel.
  • Evolutivité du Code : Il est plus facile d’ajouter de nouvelles fonctionnalités ou de modifier les existantes sans toucher à l’ensemble du codebase.
  • Coûts d’Hébergement Extrêmement Réduits : Comparé aux backends dans le cloud de type serverless, un backend monolithique modulaire peut offrir des coûts d’hébergement extrêmement réduits. Cela est dû au fait que vous n’avez pas besoin de payer pour chaque fonction ou service individuellement, mais plutôt pour un seul serveur qui héberge l’ensemble de l’application.
  • Légèreté et Rapidité de Fonctionnement : Un backend monolithique modulaire est souvent plus léger et plus rapide que les solutions serverless. Cela est dû au fait qu’il n’y a pas de latence de réseau entre les services, et que toutes les parties de l’application sont exécutées dans le même processus.
  • Simplicité de Déploiement : Avec un backend monolithique modulaire, le déploiement est généralement plus simple car il n’y a qu’une seule application à gérer.
  • Cohérence : Toutes les parties de l’application sont développées et gérées ensemble, ce qui peut conduire à une plus grande cohérence dans le code et l’architecture.
  • Scalabilité Horizontale et Verticale : Un backend monolithique modulaire offre une grande flexibilité en termes de scalabilité. Vous pouvez facilement augmenter la capacité de traitement en multipliant les processus au sein d’un même serveur (scalabilité verticale) ou en distribuant l’application sur plusieurs serveurs (scalabilité horizontale). En utilisant un reverse proxy en front, vous pouvez équilibrer la charge entre les différents serveurs et processus, ce qui permet à l’application de gérer efficacement une grande quantité de trafic.

Paradigme RCM (Route-Controller-Model) :

Le paradigme RCM est une approche héritée du paradigme MVC (Model-View-Controller). Il définit la structure et l’organisation du code en trois composants principaux : Route, Controller et Model.

Route :

  • Définition des Chemins : Les routes définissent les chemins d’accès (URL) aux différentes parties de l’application web.
  • Gestion des Requêtes : Les routes gèrent les requêtes entrantes et déterminent quel contrôleur doit être utilisé en fonction de l’URL demandée.

Controller :

  • Logique de l’Application : Les contrôleurs contiennent la logique de l’application. Ils prennent les données du modèle, les transforment si nécessaire, et les passent à la vue.
  • Intermédiaire : Les contrôleurs agissent comme un intermédiaire entre les modèles et les vues. Ils reçoivent les requêtes de l’utilisateur, interagissent avec le modèle pour obtenir ou modifier les données, et renvoient une réponse à l’utilisateur.

Model :

  • Gestion des Données : Les modèles gèrent les données de l’application. Ils interagissent avec la base de données, effectuent des opérations CRUD (Create, Read, Update, Delete) et renvoient les résultats au contrôleur.
  • Indépendance : Les modèles sont indépendants de la logique de l’application et de l’interface utilisateur. Cela signifie qu’ils peuvent être réutilisés et testés indépendamment du reste de l’application.

Avantages du Paradigme RCM :

  • Organisation du Code : Le paradigme RCM aide à organiser le code de manière logique et cohérente, ce qui facilite la maintenance et l’évolution de l’application.
  • Séparation des Préoccupations : Chaque composant du paradigme RCM a une responsabilité spécifique, ce qui permet une séparation claire des préoccupations.
  • Réutilisabilité et Testabilité : Les modèles peuvent être réutilisés dans différentes parties de l’application et testés indépendamment, ce qui améliore la qualité du code.
  • Flexibilité : Le paradigme RCM offre une grande flexibilité, car il permet de modifier ou d’ajouter des fonctionnalités à une partie de l’application sans affecter les autres parties.

Stack technologique

Fastify

Fastify est un framework web rapide et performant pour Node.js. Conçu pour offrir une efficacité maximale, Fastify promet des performances optimales grâce à une gestion efficace des requêtes HTTP. Il se distingue par sa faible surcharge (overhead) et sa capacité à gérer un grand nombre de requêtes par seconde. Fastify est également modulaire et extensible, ce qui permet aux développeurs de charger uniquement les fonctionnalités nécessaires à leur application, réduisant ainsi la complexité et améliorant les performances. De plus, Fastify offre une gestion des erreurs robuste et un support intégré pour la validation des schémas et les plugins.

Redis

Redis est une base de données en mémoire, clé-valeur, reconnue pour sa rapidité exceptionnelle et sa flexibilité. Utilisé principalement comme cache, queue et store de sessions, Redis permet de stocker et récupérer des données avec une latence extrêmement faible, ce qui est crucial pour les applications nécessitant des réponses rapides et une haute performance. Dans cette stack, Redis est utilisé pour la gestion des sessions, le stockage des tokens et les opérations nécessitant un accès rapide aux données temporaires. Sa simplicité d’utilisation et sa compatibilité avec de nombreux types de données en font un choix privilégié pour optimiser les performances de l’application.

Intégration de la Stack

Ensemble, Node.js, Fastify et Redis constituent une stack technologique puissante et efficace pour construire des applications web modernes. Node.js fournit la base de l’exécution côté serveur, Fastify ajoute une couche de serveur HTTP rapide et modulaire, et Redis offre une solution de stockage rapide et fiable. Cette combinaison permet de développer des applications robustes, performantes et capables de gérer de grandes charges de travail avec une latence minimale.

Structure du projet

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
├── backend/                         # Racine du projet backend
│ ├── .env # Fichier de configuration des variables d'environnement
│ ├── .eslintrc.js # Configuration ESLint pour le linting du code
│ ├── .gitignore # Fichiers et dossiers à ignorer par Git
│ ├── .nvmrc # Version de Node.js spécifiée pour NVM
│ ├── ecosystem.config.js # Configuration PM2 pour le déploiement
│ ├── eslint.config.js # Configuration ESLint
│ ├── jest.config.js # Configuration Jest pour les tests unitaires
│ ├── jsdoc.json # Configuration JSDoc pour la documentation du code
│ ├── package.json # Dépendances et scripts du projet
│ ├── README.md # Documentation du projet
│ ├── remoteWorker.sh # Script pour démarrer un worker à distance
│ ├── web.js # Point d'entrée principal de l'application web
│ ├── worker.js # Script pour lancer les workers
│ ├── src/ # Contient le code source de l'application
│ │ ├── app.js # Fichier principal de configuration de l'application
│ │ ├── config/ # Configuration de l'application
│ │ │ ├── assets.js # Configuration des assets
│ │ │ ├── backend.js # Configuration backend
│ │ │ ├── db.js # Configuration de la base de données
│ │ │ ├── frontend.js # Configuration frontend
│ │ │ ├── index.js # Point d'entrée pour les configurations
│ │ │ ├── mailer.js # Configuration du service de mail
│ │ │ └── token.js # Configuration des tokens JWT
│ │ ├── controllers/ # Contrôleurs gérant la logique des routes
│ │ │ ├── index.js # Point d'entrée pour les contrôleurs
│ │ │ └── user.js # Contrôleur pour les opérations utilisateur
│ │ ├── errors/ # Gestion des erreurs
│ │ │ ├── index.js # Point d'entrée pour les erreurs
│ │ │ └── user.js # Gestion des erreurs spécifiques aux utilisateurs
│ │ ├── helpers/ # Fonctions d'aide et utilitaires
│ │ │ ├── index.js # Point d'entrée pour les helpers
│ │ │ ├── log.js # Fonctions de logging
│ │ │ ├── secure.js # Fonctions de sécurité (e.g., hash de mot de passe)
│ │ │ ├── user.js # Fonctions utilitaires pour les utilisateurs
│ │ │ └── utils.js # Autres fonctions utilitaires
│ │ ├── middlewares/ # Middlewares de l'application
│ │ │ ├── index.js # Point d'entrée pour les middlewares
│ │ │ └── secure.js # Middleware de sécurité (e.g., vérification de token)
│ │ ├── models/ # Modèles de données
│ │ │ ├── index.js # Point d'entrée pour les modèles
│ │ │ ├── log.js # Modèle pour les logs
│ │ │ ├── mailer.js # Modèle pour le mailer
│ │ │ └── user.js # Modèle pour les utilisateurs
│ │ ├── routes/ # Définitions des routes de l'application
│ │ │ ├── index.js # Point d'entrée pour les routes
│ │ │ └── user.js # Routes pour les opérations utilisateur
│ │ ├── seed/ # Scripts et données de seed
│ │ │ ├── index.js # Point d'entrée pour les scripts de seed
│ │ │ └── user.js # Script de seed pour les utilisateurs
│ │ ├── assets/ # Assets utilisés pour les seeds
│ │ │ ├── fonts/ # Polices de caractères
│ │ │ │ └── Montserrat-VariableFont_wght.ttf # Police Montserrat
│ │ │ └── images/ # Images (utilisées dans les mail, etc.)
│ ├── test/ # Tests unitaires et d'intégration
│ │ ├── .env.test # Variables d'environnement pour les tests
│ │ ├── app.test.js # Tests pour l'application
│ │ └── options.js # Options de configuration pour les tests

Fonctionnalités intégrées

  • Gestion des logs : Enregistre les événements importants et les erreurs pour le suivi et le débogage.
  • Gestion des utilisateurs : Comprend l’authentification, l’inscription, la mise à jour, et la suppression des utilisateurs.
  • Authentification : Utilisation de JSON Web Tokens (JWT) pour authentifier les utilisateurs.
  • Rafraîchissement des Tokens : Gestion des tokens de rafraîchissement pour maintenir les sessions actives.
  • Rôles et Permissions : Contrôle d’accès basé sur les rôles (par exemple, admin, utilisateur) pour sécuriser les actions critiques.

Diagramme séquentiel du flux d’authentification de l’utilisateur

Diagramme séquentiel du flux d’authentification de l’utilisateur

Installation

Pour installer et exécuter le projet en local, suivez les étapes suivantes :

  1. Cloner le dépôt

    1
    2
    $ git clone git@deployment:jeremydierx/fluxx-backend.git
    $ cd fluxx-backend
  2. Installer les dépendances

    1
    $ npm install
  3. ** Adapter les fichiers de configuration à vos besoins**

    1
    2
    $ cp .env.example .env
    $ cp ecosystem.config.example.js ecosystem.config.js

En local

Initialisation de la base de données

1
$ npm run seed # le mot de passe admin généré se trouve dans seedUsers.txt

Démarrage du serveur web en local (dev)

1
$ npm run dev

L’application sera accessible à l’adresse https://localhost:[port][port] est le port configuré dans le fichier .env.

Accès complet à l’application backend depuis la console nodejs

1
$ node

puis, dans la console nodejs :

1
const app = require('./src/app')({appMode: 'console'})

Execution des tests unitaires et d’intégration

1
$ npm run test

Génération de la documentation intégrée (JSDoc)

1
$ npm run docs

Linting

1
$ npm run lint

Démarrage des workers locaux

Pour démarrer un worker local, utilisez la commande suivante en fonction du nombre de workers que vous souhaitez démarrer :

1
2
3
$ npm run worker numWorker=1 # Démarrage d'un worker
$ npm run worker numWorker=2 # Démarrage de deux workers
$ npm run worker numWorker=3 # Démarrage de trois workers

etc.

En distant (serveur de production)

Commandes de Déploiement pour l’Environnement de Production

1
2
3
4
5
$ npm run remoteSetupProd # Configure l'environnement de production.
$ npm run remoteDeployProd # Met à jour l'application en production.
$ npm run remoteSeedProd # Exécute le seeding en production.
$ npm run remoteListProd # Affiche la liste des applications pm2 en production.
$ npm run remoteSaveProd # Sauvegarde l'état actuel des processus pm2 en production.

Commande pour la gestion du serveur applicatif (backend web)

1
2
3
4
$ npm run remoteWebStartProd # Démarre l'application web en production.
$ npm run remoteWebStopProd # Arrête l'application web en production.
$ npm run remoteWebRestartProd # Redémarre l'application web en production.
$ npm run remoteWebDeleteProd # Supprime l'application web en production.

Commandes pour la gestion des workers en production

1
2
3
4
$ npm run remoteWorkerStartProd # Démarre les workers distants en production.
$ npm run remoteWorkerStopProd # Arrête les workers distants en production.
$ npm run remoteWorkerRestartProd # Redémarre les workers distants en production.
$ npm run remoteWorkerDeleteProd # Supprime les workers distants en production.

Pourquoi je n’utilise pas de framework «tout-en-un» ?

Apprentissage en Profondeur:

  • Masquage de la Complexité : Les frameworks tout-en-un ont tendance à abstraire beaucoup de complexités, ce qui peut empêcher les développeurs d’apprendre et de comprendre les mécanismes sous-jacents. Cette abstraction peut limiter la capacité des développeurs à résoudre des problèmes complexes ou à optimiser les performances de manière efficace.
  • Dépendance au Framework : Une dépendance excessive à un framework spécifique peut restreindre la flexibilité des développeurs et les rendre moins adaptables à d’autres technologies ou paradigmes.

Surcharge Fonctionnelle:

  • Overkill pour les Projets Simples : Ces frameworks viennent souvent avec une multitude de fonctionnalités intégrées qui peuvent être superflues pour de nombreux projets, rendant la configuration initiale et la maintenance plus lourdes et complexes.
  • Performance Impactée : L’inclusion de fonctionnalités non nécessaires peut alourdir l’application et impacter ses performances, surtout si ces fonctionnalités ne sont pas utilisées mais continuent de consommer des ressources.

Flexibilité Limitée:

  • Personnalisation Difficile : La personnalisation ou l’extension des fonctionnalités d’un framework tout-en-un peut être difficile ou impossible sans recourir à des hacks ou des contournements, ce qui peut nuire à la maintenabilité du code.
  • Contraintes architecturales : Ces frameworks imposent souvent une architecture et une structure spécifiques, limitant la capacité des développeurs à adapter l’application à des besoins uniques ou à adopter des meilleures pratiques qui sortent du cadre défini par le framework.

Le principe KIS (Keep It Simple)

Le principe KIS (Keep It Simple) prône la simplicité et l’absence de complexité inutile dans le développement et la conception. En privilégiant des solutions directes et faciles à comprendre, ce principe facilite la maintenance, réduit les erreurs et améliore l’efficacité. En se concentrant sur l’essentiel et en éliminant les éléments superflus, KIS permet de créer des systèmes plus robustes, plus accessibles et plus rapides à mettre en œuvre. Adopter KIS aide les équipes à rester agiles, à réduire les coûts et à livrer des produits de qualité supérieure en évitant les complications inutiles.

Éviter la programmation orientée objet (POO)

Préférez les fonctions et les structures de données simples aux classes et objets complexes pour réduire la complexité du code.

Utiliser JSDOC à la place de TypeScript

Documentez votre code JavaScript avec JSDoc pour bénéficier de l’auto-complétion et de la vérification des types, sans la complexité supplémentaire de TypeScript.

Privilégier les solutions simples

Choisissez toujours la solution la plus simple et directe pour résoudre un problème, même si elle semble moins élégante ou moins sophistiquée.

Éviter les abstractions inutiles

Limitez l’utilisation des abstractions (comme les interfaces, les frameworks complexes) qui peuvent rendre le code plus difficile à comprendre et à maintenir.

Utiliser des noms de variables et de fonctions explicites

Choisissez des noms clairs et significatifs pour vos variables et fonctions afin de rendre le code auto-documenté.

Diviser le code en petites fonctions

Écrivez des fonctions courtes et spécifiques qui effectuent une seule tâche, ce qui facilite la compréhension et la maintenance.

Minimiser les dépendances

Réduisez le nombre de bibliothèques et de frameworks externes pour limiter les points de défaillance et simplifier la gestion des mises à jour.

Favoriser la composition plutôt que l’héritage

Utilisez la composition de fonctions et de modules au lieu de l’héritage pour structurer votre code, ce qui permet de réutiliser et de tester plus facilement les composants.

Écrire des tests simples et clairs

Rédigez des tests unitaires et d’intégration qui sont faciles à comprendre et à maintenir, couvrant les cas d’utilisation principaux sans surcharger le projet.

Limiter les commentaires

Évitez de commenter chaque ligne de code. Utilisez des commentaires uniquement lorsque cela est nécessaire pour expliquer des choix non évidents.

Utiliser des outils de linters

Employez des outils de linting comme ESLint pour automatiser la vérification de la qualité et la cohérence du code.

Suivre les conventions de code

Adoptez et respectez des conventions de codage claires et bien définies pour maintenir un code cohérent et lisible par tous les membres de l’équipe.

Éviter les optimisations prématurées

Ne vous concentrez pas sur l’optimisation du code avant de vérifier qu’il y a effectivement un problème de performance. Priorisez la simplicité et la clarté.

Favoriser l’utilisation de l’outillage standard

Utilisez les fonctionnalités natives du langage et des environnements de développement avant de recourir à des solutions externes ou sur-mesure.

Conclusion

En suivant ces principes KIS, vous pourrez créer des systèmes plus simples, plus robustes et plus faciles à maintenir, tout en réduisant les coûts et le temps de développement.

Fluxx Backend est disponible sur GitHub sous licence MIT.

Jérémy @ Code Alchimie


Une idée de business en ligne ? Un saas à développer ? Une boutique en ligne à créer ?
Essayer mon-plan-action.fr pour vous aider à démarrer votre projet en ligne.

Mon Plan Action

➟ Créer une RAG app en local !

La RAG donne des super pouvoirs à votre model d’IA générative !

Introduction

Dans cet article, nous allons explorer la création d’un POC (Proof of Concept) pour un système de RAG (Retrieval Augmented Generation) en utilisant Node.js, une base de données vectorielle ChromaDB, et le modèle de langage Llama3 via Ollama.

Qu’est-ce qu’une RAG ?

Une RAG, ou Retrieval Augmented Generation, est une technique qui combine la recherche d’informations pertinentes dans une base de données (retrieval) avec la génération de texte (generation) pour fournir des réponses plus précises et contextuelles. En d’autres termes, un RAG peut récupérer des données spécifiques et utiliser un modèle de langage pour générer une réponse en se basant sur ces données, ce qui le rend particulièrement utile pour des applications nécessitant des réponses détaillées et informées.

Qu’est-ce que ChromaDB et une base de données vectorielle ?

ChromaDB est une base de données vectorielle, un type de base de données spécialement conçu pour stocker et rechercher des vecteurs. Les vecteurs sont des représentations numériques de données (comme des textes ou des images) qui permettent de mesurer la similarité entre ces données. Dans le cas présent, ChromaDB est utilisée pour rechercher des phrases similaires à la question posée, ce qui aide à récupérer les informations les plus pertinentes pour générer une réponse précise.

Qu’est-ce qu’Ollama ?

Ollama est un outil qui facilite l’utilisation de modèles de langage de grande taille (LLM) comme Llama3. Il permet de gérer facilement ces modèles et de les intégrer dans des applications. Dans ce POC, Ollama est utilisé pour générer des réponses basées sur les données récupérées par ChromaDB, ajoutant une couche de compréhension et de génération de texte naturel.

Qu’est-ce qu’un LLM (Llama3) ?

Un LLM (Large Language Model), comme Llama3, est un modèle d’intelligence artificielle entraîné sur de vastes quantités de données textuelles. Ces modèles peuvent comprendre et générer du texte en langage naturel de manière très sophistiquée. Dans ce POC, Llama3 est utilisé pour transformer les données récupérées en réponses claires et pertinentes, améliorant ainsi l’expérience utilisateur en fournissant des informations complètes et bien formulées.

Nous allons détailler chaque étape pour vous permettre de reproduire ce POC facilement.

Prérequis

Avant de commencer, assurez-vous d’avoir les éléments suivants installés sur votre machine :

  • Ubuntu 22.04 (mais cela devrait fonctionner sur les autres OS)
  • Node.js (version 20 ou supérieure)
  • npm (Node Package Manager)
  • Python3 (pour lancer le serveur ChromaDB)

Objectif

Notre dataset contient des informations sur des personnes et des animaux. Nous allons utiliser ChromaDB pour stocker ces informations et rechercher des données pertinentes en fonction de la question posée. Ensuite, nous utiliserons Ollama avec le modèle Llama3 pour générer une réponse basée sur les données récupérées. Voici les informations dont nous disposons :

“Alex porte un bonnet vert”
“Alex est un homme”
“Laura conduit une voiture bleue”
“Laura est une femme”
“Médore joue avec une balle blanche”
“Médore est un chien”
“Minou fait ses griffes sur le canapé”
“Minou est un chat”
“Sam a les cheveux longs”
“Sam est un enfant”

La question posée sera : “Que fait le chat ?”. Nous allons rechercher des données similaires dans notre dataset, puis utiliser Ollama avec Llama3 pour générer une réponse basée sur ces données.

Principe de fonctionnement de notre RAG

Diagramme de flux de notre RAG app

Notre RAG app en action !

Étape 1 : Installer ChromaDB

Commencez par installer ChromaDB sur votre système Linux Ubuntu :

1
$ pip install chromadb

Démarrer le serveur ChromaDB :

1
2
3
$ mkdir ~/rag-app
$ mkdir ~/rag-app/db
$ chroma run --path ~/rag-app/db

Étape 2 : Installer Ollama et Llama3

Nous allons maintenant installer Ollama, Llama3 et démarrer le service (dans une autre console) :

1
2
$ curl -fsSL https://ollama.com/install.sh | sh
$ ollama run llama3

Étape 3 : Installer les dépendances pour Node.js

Nous allons maintenant créer un script en Node.js pour interagir avec ChromaDB et Ollama. Commençons par installer les packages nécessaires :

1
2
3
4
$ cd ~/rag-app
$ npm init
$ npm install chromadb chromadb-default-embed ollama
$ touch rag.js

Étape 3 : Initialiser ChromaDB et Ollama (rag.js)

1
2
3
4
5
6
7
8
9
const ollama = require('ollama').default

const {
ChromaClient,
DefaultEmbeddingFunction
} = require('chromadb')

const client = new ChromaClient()
const embedder = new DefaultEmbeddingFunction()

Étape 4 : Définir les Données et la Collection

Ajoutons les documents et la collection à ChromaDB (dataset) :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const collectionName = 'docs'

// le choix du modèle
const model = 'llama3'

// le dataset
const documents = [
"Alex porte un bonnet vert",
"Alex est un homme",
"Laura conduit une voiture bleue",
"Laura est une femme",
"Médore joue avec une balle blanche",
"Médore est un chien",
"Minou fait ses griffes sur le canapé",
"Minou est un chat",
"Sam a les cheveux longs",
"Sam est un enfant"
]

console.log(`
--- Documents de départ ---\n
${documents.join('\n')}`)

Étape 5 : Supprimer et Créer une Collection

Nous allons ajouter les fonctions pour supprimer et créer une nouvelle collection dans ChromaDB :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// on supprime la collection si elle existe
async function deleteCollection(name) {
try {
await client.deleteCollection({ name })
return true
} catch (error) {
console.error('Error deleting collection', error)
}
}

// on crée la collection (docs)
async function createCollection(name) {
try {
const collection = await client.createCollection({
name,
embeddingFunction: embedder
})
return collection
} catch (error) {
console.error('Error creating collection', error)
}
}

Étape 6 : Ajouter des Documents à la Collection

Ajoutons les documents dans la collection que nous venons de créer :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// on ajoute les documents à la collection
async function addToCollection(collection) {
try {
const ids = documents.map((_, i) => i.toString())
await collection.add({
ids,
documents,
embeddings: await embedder.generate(documents)
})
return collection
} catch (error) {
console.error('Error adding items to collection', error)
}
}

Étape 7 : Recherche par Similarité

Implémentons la recherche par similarité dans la collection :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// on recherche dans la collection par similarité
async function searchInCollection(collection) {
try {
const nResults = 2
const question = 'Que fait le chat ?'
console.log(`--- Question ---\n\n${question}\n`)
const results = await collection.query({
nResults,
queryEmbeddings: await embedder.generate([question])
})
const data = results.documents[0].join(', ')
console.log(`--- Données proches retrouvées en DB (${nResults} résultats max) ---\n\n${data}\n`)
return data
} catch (error) {
console.error('Error searching in collection', error)
}
}

Étape 8 : Générer la Réponse avec Ollama / LLama3

Nous allons maintenant utiliser Ollama pour générer une réponse à partir des données trouvées :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// on génère la réponse à partir des données trouvées
async function generateResponse(data) {
try {
const { response } = await ollama.generate({
model,
prompt: `En utilisant ces données : ${data}. Répond à cette question : Quel est le sexe de la personne qui porte des gants ?`
})
console.log(`--- Réponse de ${model} ---`)
console.log(response)
console.log()
return response
} catch (error) {
console.error('Error generating response:', error)
}
}

Étape 9 : Exécuter les Fonctions

Pour finir, créons une fonction principale pour exécuter toutes les étapes séquentiellement:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// on exécute les fonctions
function main(){
deleteCollection(collectionName)
.then(deleted => {
return createCollection(collectionName)
})
.then(collection => {
return addToCollection(collection)
})
.then(collection => {
return searchInCollection(collection)
})
.then(data => {
return generateResponse(data)
})
}

// start!
main()

Le programme complet

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
const ollama = require('ollama').default

const {
ChromaClient,
DefaultEmbeddingFunction
} = require('chromadb')

const client = new ChromaClient()
const embedder = new DefaultEmbeddingFunction()

const collectionName = 'docs'

// le choix du modèle <--
const model = 'llama3'

// le dataset <--
documents = [
"Alex porte un bonnet vert",
"Alex est un homme",
"Laura conduit une voiture bleue",
"Laura est une femme",
"Médore joue avec une balle blanche",
"Médore est un chien",
"Minou fait ses griffes sur le canapé",
"Minou est un chat",
"Sam a les cheveux longs",
"Sam est un enfant"
]

console.log(`
--- Documents de départ ---\n
${documents.join('\n')}\n`)

// le nombre de résultats récupérés par similarité depuis la DB <--
const nResults = 2

// la question <--
const question = 'Que fait le chat ?'
console.log(`--- Question ---\n\n${question}\n`)
// on supprime la collection si elle existe
async function deleteCollection(name) {
try {
await client.deleteCollection({
name
})
return true
} catch (error) {
console.error('Error deleting collection', error)
}
}

// on crée la collection (docs)
async function createCollection(name) {
try {
const collection = await client.createCollection({
name,
embeddingFunction: embedder
})
return collection
} catch (error) {
console.error('Error creating collection', error)
}
}

// on ajoute les documents à la collection
async function addToCollection(collection) {
try {
const ids = documents.map((_, i) => i.toString())
await collection.add({
ids,
documents,
embeddings: await embedder.generate(documents)
})
return collection
return coll
} catch (error) {
console.error('Error adding items to collection', error)
}
}

// on récupère les items de la collection (uniquement pour tester)
async function getItemsFromCollection(name) {
try {
const collection = await client.getCollection({ name })
console.log(await coll.get())
return collection
} catch (error) {
console.error('Error get items from collection', error)
}
}

// on recherche dans la collection par similarité
async function searchInCollection(collection) {
try {
const results = await collection.query({
nResults,
queryEmbeddings: await embedder.generate([question])
})
const data = results.documents[0].join(', ')
console.log(`--- Données proches retrouvées en DB (${nResults} résultats max) ---\n\n${data}\n`)
return data
} catch (error) {
console.error('Error searching in collection', error)
}
}

// on génère la réponse à partir des données trouvées
async function generateResponse(data) {
try {
const { response } = await ollama.generate({
model,
prompt: `En utilisant ces données : ${data}. Répond à cette question : ${question}`
})
console.log(`--- Réponse de ${model} ---\n${response}`)
return response
} catch (error) {
console.error('Error generating response:', error)
}
}

// on exécute les fonctions
function main(){
deleteCollection(collectionName)
.then(deleted => {
return createCollection(collectionName)
})
.then(collection => {
return addToCollection(collection)
})
.then(collection => {
return searchInCollection(collection)
})
.then(data => {
return generateResponse(data)
})
}

// start!
main()

Vous pouvez retrouver le code source complet sur Github

Conclusion

En suivant ces étapes, vous pouvez créer un POC de RAG en utilisant Node.js, ChromaDB et Llama3 via Ollama. Ce processus vous permet d’explorer la puissance des bases de données vectorielles et des modèles de langage avancés pour améliorer vos applications web avec des capacités de génération de réponses enrichies par la récupération d’informations pertinentes.

N’hésitez pas à adapter ce script à vos propres besoins et à explorer d’autres fonctionnalités de ChromaDB et Ollama pour aller plus loin dans l’innovation technologique.

Jérémy @ Code Alchimie


Une idée de business en ligne ? Un saas à développer ? Une boutique en ligne à créer ?
Essayer mon-plan-action.fr pour vous aider à démarrer votre projet en ligne.

Mon Plan Action