Écrit par

Sales Engineer at InterSystems
Article Guillaume Rongier · Oct 16, 2023 8m read

Support des vecteurs, enfin presque

Aujourd'hui, il y a beaucoup de bruit autour du LLM, de l'IA, etc. Les bases de données vectorielles en font partie, et il existe déjà de nombreuses réalisations différentes pour le support en dehors d'IRIS. 

Pourquoi Vector?

  • Recherche de similarité : Les vecteurs assurent une recherche de similarité efficace, par exemple en trouvant les éléments ou les documents les plus similaires dans un ensemble de données. Les bases de données relationnelles classiques sont conçues pour des recherches de correspondances exactes, qui ne sont pas adaptées à des tâches telles que la recherche de similitudes d'images ou de textes.
  • Flexibilité : Les représentations vectorielles sont polyvalentes et peuvent être obtenues à partir de différents types de données, tels que du texte (via des embeddings comme Word2Vec, BERT), des images (via des modèles d'apprentissage profond), et autres.
    • Recherches multimodales** : Les vecteurs permettent d'effectuer des recherches dans différentes modalités de données. Par exemple, avec une représentation vectorielle d'une image, on peut rechercher des images similaires ou des textes connexes dans une base de données multimodale.

Et pour bien d'autres raisons encore.

Donc, pour ce concours python, j'ai décidé de mettre en place ce support. Et malheureusement, je n'ai pas réussi à le terminer à temps, je vais vous expliquer pourquoi.

Il y a un certain nombre de choses importantes à faire pour qu'il soit complet

  • Accepter et stocker des données vectorisées, avec SQL, exemple simple, (3 dans cet exemple est le nombre de dimensions, il est fixé par champ, et tous les vecteurs dans le champ doivent avoir des dimensions exactes)
createtable items(embedding vector(3));
insertinto items (embedding) values ('[1,2,3]');
insertinto items (embedding) values ('[4,5,6]');
  • Fonctions de similarité, il existe des algorithmes de similarité différents, adaptés à une recherche simple sur une petite quantité de données, sans utiliser d'index
    -- Euclidean distanceselect embedding, vector.l2_distance(embedding, '[9,8,7]') distance from items orderby distance;
    -- Cosine similarityselect embedding, vector.cosine_distance(embedding, '[9,8,7]') distance from items orderby distance;
    -- Inner productselect embedding, -vector.inner_product(embedding, '[9,8,7]') distance from items orderby distance;
  • * Index personnalisé, pour accélérer la recherche sur une grande quantité de données. Les index peuvent utiliser un algorithme différent et des fonctions de distance différentes de celles mentionnées ci-dessus, ainsi que d'autres options. * HNSW![](https://miro.medium.com/v2/resize:fit:1400/1*ziU6_KIDqfmaDXKA1cMa8w.png) * Index de fichier inversé ![](/sites/default/files/inline/images/images/image(7029).png) * La recherche n'utilisera que l'index créé et son algorithme trouvera l'information demandée.

    Insertion de vecteurs

    On suppose que le vecteur est un tableau de valeurs numériques, lesquelles peuvent être des entiers ou des flottants, ainsi que signées ou non. Dans IRIS, nous pouvons le stocker comme $listbuild, il a une bonne représentation, il est déjà supporté, il faut seulement implémenter la conversion d'ODBC en logique.

    Les valeurs peuvent ensuite être insérées sous forme de texte brut à l'aide de pilotes externes tels que ODBC/JDBC ou à partir d'IRIS avec ObjectScript.

  • SQL
    insertinto items (embedding) values ('[1,2,3]');
  • à partir ObjectScript
    set rs = ##class(%SQL.Statement).%ExecDirect(, "insert into test.items (embedding) values ('[1,2,3]')")
    

    set rs = ##class(%SQL.Statement).%ExecDirect(, "insert into test.items (embedding) values (?)", $listbuild(2,3,4))

  • Ou Embedded SQL
    &sql(insertinto test.items (embedding) values ('[1,2,3]'))
    

    set val = $listbuild(2,3,4) &sql(insertinto test.items (embedding) values (:val))

  • Il sera toujours stocké sous la forme de $lb(), et renvoyé au format textuel dans ODBC.

     
    Comportement imprévu
    Au cours des tests effectués avec DBeaver, j'ai constaté que la première ligne après la connexion était insérée correctement, mais que toutes les autres étaient insérées telles quelles, sans aucune validation ou conversion. 
    <p>
      J'ai ensuite découvert que JDBC utilise les insertions rapide <a href="https://docs.intersystems.com/iris20211/csp/docbook/DocBook.UI.Page.cls?KEY=RSQL_insert#RSQL_insert_fast">Fast Inserts</a> par défaut, dans ce cas, les données insérées sont stockées directement dans les globales, et j'ai donc dû les désactiver manuellement
    </p>
    
    <p>
      Dans DBeaver, sélectionnez optfastSelect dans le champ FeatureOption.
    </p>
    
    <p>
      <img src="/sites/default/files/inline/images/images/image(7036).png" />
    </p>
    

    Calculations

    Les vecteurs sont principalement nécessaires pour calculer les distances entre deux vecteurs.

    Pour le concours, j'ai eu besoin d'utiliser l'outil Embedded Python, et c'est là que se pose le problème de savoir comment utiliser $lb dans Embedded Python. Il y a une méthode ToList dans %SYS.Class, mais le paquetage d'iris de Python ne l'a pas intégrée, et il faut l'appeler comme ObjectScript.

    ClassMethod l2DistancePy(v1 As dc.vector.type, v2 As dc.vector.type) As%Decimal(SCALE=10) [ Language = python, SqlName = l2_distance_py, SqlProc ]
    {
        import iris 
        import math
        
        vector_type = iris.cls('dc.vector.type')
        v1 = iris.cls('%SYS.Python').ToList(vector_type.Normalize(v1))
        v2 = iris.cls('%SYS.Python').ToList(vector_type.Normalize(v2))
    
        return math.sqrt(sum([(val1 - val2) ** 2for val1, val2 in zip(v1, v2)]))
    }
    

    Cela ne semble pas correct du tout. Je préférerais que $lb puisse être interprété directement comme une liste en python, ou comme les fonctions intégrées to_list et from_list.

    Un autre problème s'est posé lorsque j'ai essayé de tester cette fonction de différentes manières. En utilisant SQL à partir d'Embedded Python qui emploie la fonction SQL écrite dans Embedded Python, elle se plante. J'ai donc dû ajouter les fonctions ObjectScript.

    ModuleNotFoundError: No module named 'dc'
    SQL Function VECTOR.NORM_PY failed with error:  SQLCODE=-400,%msg=ERROR #5002: ObjectScript error:
    
    
      %0AmBm3l0tudf^%sqlcq.USER.cls37.1 *python object not found

    Les fonctions sont actuellement implémentées pour calculer la distance, à la fois en Python et en ObjectScript

    • Distance euclidienne
      [SQL]_system@localhost:USER> select embedding, vector.l2_distance_py(embedding, '[9,8,7]') distance from items orderby distance;
      +-----------+----------------------+
      | embedding | distance             |
      +-----------+----------------------+
      | [4,5,6]   | 5.91607978309961613  |
      | [1,2,3]   | 10.77032961426900748 |
      +-----------+----------------------+
      2 rows in setTime: 0.011s
      [SQL]_system@localhost:USER> select embedding, vector.l2_distance(embedding, '[9,8,7]') distance from items orderby distance;
      +-----------+----------------------+
      | embedding | distance             |
      +-----------+----------------------+
      | [4,5,6]   | 5.916079783099616045 |
      | [1,2,3]   | 10.77032961426900807 |
      +-----------+----------------------+
      2 rows in setTime: 0.012s
    • Similarité cosinus
      [SQL]_system@localhost:USER> select embedding, vector.cosine_distance(embedding, '[9,8,7]') distance from items orderby distance;
      +-----------+---------------------+
      | embedding | distance            |
      +-----------+---------------------+
      | [4,5,6]   | .034536677566264152 |
      | [1,2,3]   | .11734101007866331  |
      +-----------+---------------------+
      2 rows in setTime: 0.034s
      [SQL]_system@localhost:USER> select embedding, vector.cosine_distance_py(embedding, '[9,8,7]') distance from items orderby distance;
      +-----------+-----------------------+
      | embedding | distance              |
      +-----------+-----------------------+
      | [4,5,6]   | .03453667756626421781 |
      | [1,2,3]   | .1173410100786632659  |
      +-----------+-----------------------+
      2 rows in setTime: 0.025s
    <li>
      Produit scalaire
      <pre class="codeblock-container" idlang="1" lang="SQL" tabsize="4"><code class="language-sql hljs">[SQL]_system@localhost:USER> <span class="hljs-keyword">select</span> embedding, vector.inner_product_py(embedding, <span class="hljs-string">'[9,8,7]'</span>) distance <span class="hljs-keyword">from</span> items <span class="hljs-keyword">order</span> <span class="hljs-keyword">by</span> distance;
    

    +-----------+----------+ | embedding | distance | +-----------+----------+ | [1,2,3] | 46 | | [4,5,6] | 118 | +-----------+----------+ 2 rows in setTime: 0.035s [SQL]_system@localhost:USER> select embedding, vector.inner_product(embedding, '[9,8,7]') distance from items orderby distance; +-----------+----------+ | embedding | distance | +-----------+----------+ | [1,2,3] | 46 | | [4,5,6] | 118 | +-----------+----------+ 2 rows in setTime: 0.032s

    En outre, les fonctions mathématiques suivantes ont été intégrées : add (ajouter), sub (sous), div (diviser), mul (multiplier). InterSystems permet de créer ses propres fonctions d'agrégation. Ainsi, il serait possible de faire la somme totale de tous les vecteurs ou de trouver la moyenne. Malheureusement, InterSystems ne permet pas d'utiliser le même nom et nécessite l'utilisation d'un nom (et d'un schéma) propre pour la fonction. Mais il ne prend pas en charge les résultats non numériques pour les fonctions d'agrégation.

    La fonction vector_add simple, qui renvoie la somme de deux vecteurs

    Lorsqu'elle est utilisée en tant qu'agrégat, la valeur est de 0, alors que le vecteur attendu est également de 0.

    Création d'un index

    Malheureusement, je n'ai pas réussi à terminer cette partie, en raison de certains obstacles que j'ai rencontrés lors de la réalisation. 

    • L'absence de $lb intégré à des conversions de listes python et inversement lorsque le vecteur dans IRIS est stocké dans $lb, et que toute la logique de construction de l'index est censée être en Python, il est important également de récupérer les données de $lb et de les réintégrer dans les globales.
    <li>
      absence de support pour les globales
      <ul>
        <li>
          Dans IRIS, $Order supporte la direction, cette fonction peut donc être utilisée en sens inverse, alors que la réalisation de l'ordre dans Python Embedded ne la supporte pas, il faudra donc lire toutes les clés et les inverser ou stocker la fin quelque part.
        </li>
    
      </ul>
    </li>
    
    
    <li>
      J'ai des doutes suite à une mauvaise expérience avec les fonctions SQL de Python, appelées à partir de Python mentionné ci-dessus.
    </li>
    
    
    <li>
      Au cours de la construction de l'index, il était prévu de stocker les distances dans le graphe entre les vecteurs, mais un bogue s'est produit lors du stockage de nombres flottants dans les données globales de l'index.
    </li>
    

    J'ai abordé 11 problèmes avec Embedded Python que j'ai trouvés pendant le travail, donc la plupart du temps il faut trouver des solutions de contournement pour résoudre les problèmes. Avec l'aide du projet de @Guillaume Rongier nommé iris-dollar-list j'ai réussi à résoudre certains problèmes.

    Installation

    Quoi qu'il en soit, ce logiciel est toujours disponible et peut être installé avec IPM, et utilisé même avec des fonctionnalités limitées. 

    zpm "install vector"

    Ou en mode de développement avec docker-compose

    git clone https://github.com/caretdev/iris-vector.git
    cd iris-vector
    docker-compose up -d