Dans une première partie, nous avons abordé comment interagir avec un modèle pour obtenir un dialogue en fonction d'informations sur lesquelles il n'a pas été entraîné. Pour résumer, on ajoute au contexte les informations désirées. Mais alors, supposons que vous vouliez utiliser toute une base de connaissances ? Cela ferait beaucoup trop d'informations à ajouter au contexte. Pour cela, nous devons mettre en base de données l'ensemble des informations que nous souhaitons fournir aux utilisateurs. Nous allons découper nos contenus sous forme de paragraphes et appliquer un index vectoriel. Cet index convertit le texte en vecteurs numériques représentant le sens sémantique des mots et des phrases. Ce qui est formidable, c'est que cela permet de faire des recherches basées sur le sens plutôt que sur les mots exacts.

Parmi les bases de données permettant d'effectuer ce genre de recherche, nous retrouvons :

  1. Elasticsearch : C'est un moteur de recherche et d'analyse distribué, largement utilisé pour la recherche textuelle. Elasticsearch peut être utilisé pour créer des index vectoriels grâce à ses plugins et intégrations, notamment pour le traitement du langage naturel.

  2. Faiss (Facebook AI Similarity Search) : Développé par Facebook AI, Faiss est une bibliothèque permettant l'indexation vectorielle efficace et la recherche de similarité. Elle est particulièrement adaptée pour gérer de grands ensembles de vecteurs et est souvent utilisée dans les systèmes de recommandation et de recherche sémantique.

  3. Milvus : Milvus est un système de gestion de bases de données vectorielles open-source. Il est conçu pour gérer des index vectoriels à grande échelle et est compatible avec plusieurs modèles de machine learning, y compris ceux utilisés pour le traitement du langage naturel.

  4. Pinecone : Pinecone est une base de données de vecteurs conçue pour les applications de machine learning. Elle offre une gestion et une recherche de vecteurs à grande échelle, ce qui est utile pour les applications de recherche sémantique et de NLP.

  5. Weaviate : Weaviate est une base de données de connaissances vectorielles, permettant le stockage de données sous forme vectorielle. Elle prend en charge les requêtes sémantiques et est optimisée pour les cas d'utilisation impliquant le traitement du langage naturel.

  6. Annoy (Approximate Nearest Neighbors Oh Yeah) : Annoy est une bibliothèque C++ avec des bindings Python conçue pour rechercher les voisins les plus proches dans des espaces de grande dimension. Elle est utilisée pour créer des index vectoriels et est efficace pour les requêtes de recherche rapide.

  7. HNSW (Hierarchical Navigable Small World) : HNSW est un algorithme populaire pour la recherche de voisins les plus proches dans des espaces de haute dimension. Plusieurs systèmes de gestion de bases de données intègrent HNSW pour la création d'index vectoriels.

  8. Postgresql (pgvector) : Postgresql utilise HNSW pour la création d'index vectoriels dans le cadre de l'extension pgvector.

Vous pouvez trouver une liste plus complète à cette adresse :
https://js.langchain.com/docs/integrations/vectorstores Cela offre déjà pas mal de possibilités.

Concernant la partie stockage, comment crée-t-on ces vecteurs ? On utilise ce qu'on appelle les 'embeddings'. Les embeddings transforment des données complexes, telles que des mots, en vecteurs dans un espace multidimensionnel. Par exemple, chaque mot d'un texte peut être représenté par un vecteur de 50, 100, 300 dimensions ou plus. Ces vecteurs sont conçus de telle manière que les mots ayant des significations similaires sont proches dans cet espace vectoriel. Par exemple, 'roi' et 'reine' seraient proches, reflétant leur relation sémantique. Il existe des modèles d'embeddings pré-entraînés tels que Word2Vec, GloVe ou BERT, qui ont été entraînés sur de vastes corpus de texte et peuvent être utilisés pour obtenir des embeddings de mots de haute qualité.

Vous pourrez trouver une liste des différents systèmes d'embeddings disponibles à cette adresse : https://js.langchain.com/docs/integrations/text_embedding

Il est important de comprendre que lorsque vous utilisez un modèle pour créer vos embeddings, vous devrez utiliser le même modèle pour créer vos requêtes par la suite. Il est essentiel de maintenir une cohérence afin de maximiser les chances pour le moteur de la base de données de comparer correctement la distance entre les vecteurs de votre requête et ceux stockés dans votre base de données.

En résumé, vous avez en base de données une série de vecteurs, et vous allez faire une requête avec une autre série de vecteurs. La base de données va comparer les distances ; plus elles sont faibles, plus les significations seront similaires.

Pour aller plus loin, j'ai trouvé les exemples de cet article assez parlants : https://medium.com/@aravilliatchutaram/intent-classification-using-vector-database-txtai-sqlite-and-large-language-models-821f939b87ba

Si vous souhaitez utiliser un système différent de celui proposé en standard par txtai, voici une ressource ultime : https://neuml.hashnode.dev/customize-your-own-embeddings-database Ici, vous trouverez des exemples de génération d'embeddings avec NumPy, PyTorch, Faiss, HNSW et même l'utilisation d'une API externe comme Huggingface.

À noter que txtai propose par défaut la création de vecteurs avec le modèle https://huggingface.co/sentence-transformers/all-MiniLM-L6-v2 en utilisant Faiss comme index Ann backend.

Pour résumer :

  • Créer un embedding
from txtai.embeddings import Embeddings

embeddings = Embeddings()
  • Creer un embeeding avec un enregistrement dans une base de données : SQLITE / all-MiniLM-L6-v2 / Faiss
from txtai.embeddings import Embeddings

embeddings = Embeddings(content=True)
  • Pour creer un embedding avec Postgresql / gte-large / Faiss
pipenv shell
pipenv install psycopg2-binary
from datasets import load_dataset
import txtai

# Load dataset
ds = load_dataset("ag_news", split="train")

embeddings = txtai.Embeddings(
    content="postgresql+psycopg2://testuser:testpwd@localhost:5432/vectordb",
    objects=True,
    backend="faiss",
    path="thenlper/gte-large"
)

# indexer
embeddings.index(ds["text"])
# sauvegarder l'index
embeddings.save("./index")
# charger l'index sauvegardé
embeddings.load("./index")
## ou dans le cloud,compressé
embeddings.save("/path/to/save/index.tar.gz", cloud={...})

Dans cet exemple, nous enregistrons les contenus dans une base de données et créons notre index avec FAISS. Cela vous obligera à sauvegarder vos index pour pouvoir les recharger ultérieurement. Vous avez également la possibilité de les enregistrer dans le cloud ou dans Elasticsearch. À noter que si vous modifiez vos contenus dans la base de données, vous devrez recréer votre index.

import txtai

embeddings = txtai.Embeddings(
    content="postgresql+psycopg2://testuser:testpwd@localhost:5432/vectordb",
    objects=True,
    backend="faiss",
    path="thenlper/gte-large"
)


embeddings.reindex(path="sentence-transformers/all-MiniLM-L6-v2", backend="hnsw")
embeddings.save("./index")

stratégie de chunks ou segmentation de vos documents

Il existe plusieurs stratégies lorsqu'il s'agit de créer vos segments de données (chunks).

  1. Chunking de Taille Fixe

C'est la méthode la plus courante et la plus simple. Elle consiste à déterminer un nombre fixe de tokens pour chaque chunk, avec éventuellement un certain chevauchement entre eux pour préserver le contexte sémantique.

  1. Chunking "Content-aware" (Conscient du Contenu)

Cette approche utilise la nature du contenu pour une segmentation plus sophistiquée. Elle comprend :

  • Fractionnement de Phrases : Utilisation d'outils comme NLTK ou spaCy pour diviser le texte en phrases, offrant une meilleure préservation du contexte.

  • Chunking Récursif : Divise le texte en chunks plus petits de manière hiérarchique et itérative, en utilisant différents séparateurs ou critères jusqu'à obtenir la taille ou la structure de chunk souhaitée.

Ces techniques varient en fonction du contenu et de l'application visée, et le choix de la méthode appropriée dépend de plusieurs facteurs, y compris la nature du contenu, le modèle d'encodage utilisé, et l'objectif de l'application.

Setup simple de la base de donnée PostgreSQL :

Pas de mystère ici, nous faisons simple. Nous allons utiliser l'image Docker fournie par le développeur du module pgvector.

## docker-compose.yaml
services:
  db:
    hostname: db
    image: ankane/pgvector
    ports:
     - 5432:5432
    restart: always
    environment:
      - POSTGRES_DB=vectordb
      - POSTGRES_USER=testuser
      - POSTGRES_PASSWORD=testpwd
      - POSTGRES_HOST_AUTH_METHOD=trust
    volumes:
     - ./init.sql:/docker-entrypoint-initdb.d/init.sql
## init.sql
CREATE EXTENSION IF NOT EXISTS vector;

CREATE TABLE IF NOT EXISTS embeddings (
  id SERIAL PRIMARY KEY,
  embedding vector,
  text text,
  created_at timestamptz DEFAULT now()
);
docker compose up -d

C'est donc fini pour cette seconde partie, quelque peu théorique. J'ai seulement survolé les sujets, et vous vous doutez que chacun des composants impliqués regorge de paramètres de fine tuning. La beauté de TxtAi réside dans le fait que tout est simple et déjà préconfiguré. Ces exemples ne sont pas vraiment utiles si nous souhaitons mettre en place un système simple. Toutefois, dans la réalité d'un projet, on utilise rarement les choses telles quelles. C'est pourquoi j'ai tenu à montrer pour chaque élément les possibilités offertes par ce framework. Tout est configurable, rien n'est laissé au hasard. Ce qui est formidable avec ce framework, surtout si vous découvrez le domaine, est que l'on peut découvrir à travers la documentation les briques nécessaires à un tel système. Les choses peuvent devenir très complexes dès lors que nous souhaitons sortir du cadre établi.

Dans le troisième volet de notre série sur le système RAG, nous allons mettre un peu plus les mains dans le cambouis. Le côté rébarbatif de la théorie ayant déjà été abordé, nous pourrons nous concentrer sur le code.