Utiliser Redis pour implémenter la recherche de similarité vectorielle : résoudre le problème de correspondance de similarité entre le texte, les images et l'audio

Dans le domaine du traitement du langage naturel, la recherche de similarité de texte est une tâche courante et importante. La recherche de similarité de texte consiste à trouver le ou les paragraphes de texte les plus similaires ou les plus pertinents de la base de données sur la base d'un morceau de texte saisi par l'utilisateur. Il peut être appliqué dans de nombreux scénarios, tels que les systèmes de questions et réponses, les systèmes de recommandation, les moteurs de recherche, etc.

Par exemple, lorsqu'un utilisateur pose une question sur Zhihu, le système peut trouver la réponse qui correspond le mieux ou qui est la plus utile à la question parmi les réponses existantes sur Zhihu, et l'afficher à l'utilisateur.

Pour obtenir une recherche tout aussi efficace, nous devons utiliser des structures de données et des algorithmes spéciaux. Parmi eux, la recherche de similarité vectorielle est un algorithme qui fonctionne bien dans la recherche de données à grande échelle. En tant que base de données clé-valeur hautes performances, Redis peut également nous aider à mettre en œuvre une recherche de similarité vectorielle.

Avant de commencer à apprendre à utiliser Redis pour mettre en œuvre la recherche de similarité vectorielle, vous devez comprendre les connaissances et les principes de base des vecteurs et de la recherche de similarité vectorielle afin de mieux comprendre le contenu suivant.

Qu'est-ce qu'un vecteur ?

Le vecteur est un concept de base dans de nombreuses sciences naturelles telles que les mathématiques, la physique et les sciences de l'ingénieur. Il s'agit d'une quantité avec une direction et une longueur et est utilisée pour décrire des problèmes tels que la géométrie spatiale, la mécanique, le traitement du signal, etc. En informatique, les vecteurs sont utilisés pour représenter des données telles que du texte, des images ou de l'audio. De plus, les vecteurs représentent également l’impression du modèle d’IA sur les données non structurées telles que le texte, les images, l’audio et la vidéo.

Principes de base de la recherche de similarité vectorielle

Le principe de base de la recherche de similarité vectorielle consiste à mapper chaque élément de l'ensemble de données sur un vecteur et à utiliser un algorithme de calcul de similarité spécifique, tel que les algorithmes de similarité cosinusoïdale, euclidienne ou Jaccard, pour trouver les vecteurs de requête qui sont les plus semblables les uns aux autres.

Redis implémente la recherche de similarité vectorielle

Après avoir compris le principe, nous commençons à implémenter comment utiliser Redis pour implémenter la recherche de similarité vectorielle. Redis nous permet d'utiliser des requêtes de similarité vectorielle dans la commande FT.SEARCH. Nous permet de charger, d'indexer et d'interroger des vecteurs stockés sous forme de champs dans des hachages Redis ou des documents JSON.

//Adresse du document associé

Similitude des vecteurs | Rédis

1. Installation de la recherche Redis

Concernant l’installation et l’utilisation de Redis Search, je n’entrerai pas dans les détails ici. Si cela ne vous est pas familier, vous pouvez vous référer à l’article précédent :

Recherche C#+Redis : comment utiliser Redis pour implémenter une recherche en texte intégral hautes performances

2. Créez une bibliothèque d'index vectoriels

Ici, nous utilisons deux bibliothèques NRedisStack et StackExchange.Redis pour interagir avec Redis.

//创建一个Redis连接
static ConnectionMultiplexer mux = ConnectionMultiplexer.Connect("localhost");
//获取一个Redis数据库
static IDatabase db = mux.GetDatabase();
//创建一个RediSearch客户端
static SearchCommands ft = new SearchCommands(db, null);

Avant d'effectuer une recherche vectorielle, vous devez d'abord définir et créer un index, et spécifier un algorithme de similarité.

public static async Task CreateIndexAsync()
{
    await ft.CreateAsync(indexName,
        new FTCreateParams()
                    .On(IndexDataType.HASH)
                    .Prefix(prefix),
        new Schema()
                    .AddTagField("tag")
                    .AddTextField("content")
                    .AddVectorField("vector",
                        VectorField.VectorAlgo.HNSW,
                        new Dictionary<string, object>()
                        {
                            ["TYPE"] = "FLOAT32",
                            ["DIM"] = 2,
                            ["DISTANCE_METRIC"] = "COSINE"
                        }));
}

Ce que signifie ce code est :

  • Une méthode asynchrone ft.CreateAsync est utilisée pour créer l'index. Il accepte trois paramètres : le nom d'index indexName, un objet FTCreateParams et un objet Schema ;
  • La classe FTCreateParams fournit quelques options de paramètres pour spécifier les paramètres de l'index. Ici, la méthode .On(IndexDataType.HASH) est utilisée pour spécifier le type de données d'index comme hachage, et la méthode .Prefix(prefix) est utilisée pour spécifier le préfixe des données d'index ;
  • La classe Schema est utilisée pour définir les champs et les types de champs dans l'index. Un champ de balise est défini ici pour distinguer les données filtrées. Un champ de texte est défini pour stocker les données originales, et un champ vectoriel est utilisé pour stocker les données vectorielles converties à partir des données originales ;
  • VectorField.VectorAlgo.HNSW est utilisé pour spécifier l'algorithme vectoriel comme HNSW (Hierarchical Navigable Small World). Un objet dictionnaire est également transmis pour définir les paramètres du champ vectoriel. Parmi eux, la clé est de type chaîne et la valeur est de type objet.

Actuellement, Redis prend en charge deux algorithmes de similarité :

L'algorithme de navigation hiérarchique HNSW pour petits mondes utilise un réseau de petits mondes pour créer des index. Il a une vitesse de requête rapide et une faible empreinte mémoire. La complexité temporelle est O(logn) et convient à l'indexation à grande échelle.

L'algorithme de force brute FLAT analyse toutes les paires clé-valeur, puis calcule le chemin le plus court en fonction de la distance entre les paires clé-valeur. La complexité temporelle est O(n), où n est le nombre de paires clé-valeur. Cet algorithme a une complexité temporelle très élevée et ne convient que pour les index à petite échelle.

3. Ajouter des vecteurs à la bibliothèque d'index

Une fois l'index créé, nous ajoutons des données à l'index.

public async Task SetAsync(string docId, string prefix, string tag, string content, float[] vector)
{
    await db.HashSetAsync($"{prefix}{docId}", new HashEntry[] {
        new HashEntry ("tag", tag),
        new HashEntry ("content", content),
        new HashEntry ("vector", vector.SelectMany(BitConverter.GetBytes).ToArray())
    });
}

La méthode SetAsync est utilisée pour stocker un vecteur avec l'ID de document, le préfixe, la balise, le contenu et le contenu spécifiés dans la bibliothèque d'index. Et utilisez la méthode SelectMany() et la méthode BitConverter.GetBytes() pour convertir le vecteur en un tableau d'octets.

4. Recherche de vecteurs

Redis prend en charge deux types de requêtes vectorielles : la requête KNN et la requête Range, et les deux types de requêtes peuvent également être mélangés.

Requête KNN

La requête KNN est utilisée pour trouver les N vecteurs les plus similaires en fonction d'un vecteur de requête.

public async IAsyncEnumerable<(string Content, double Score)> SearchAsync(float[] vector, int limit)
{
    var query = new Query($"*=>[KNN {limit} @vector $vector AS score]")
                .AddParam("vector", vector.SelectMany(BitConverter.GetBytes).ToArray())
                .SetSortBy("score")
                .ReturnFields("content", "score")
                .Limit(0, limit)
                .Dialect(2);

    var result = await ft.SearchAsync(indexName, query).ConfigureAwait(false);
    foreach (var document in result.Documents)
    {
        yield return (document["content"],Convert.ToDouble(document["score"]));
    }
}

Ce que signifie ce code est :

  • Créez une requête d'objet de requête et définissez les conditions de requête. Les conditions de requête incluent :
    1. "*=>[KNN {limit} @vector $vector AS score]" : utilisez l'algorithme KNN pour effectuer une recherche de similarité vectorielle, limiter le nombre de résultats à limiter, utiliser le vecteur vectoriel donné comme vecteur de requête et noter les résultats de la requête selon la similarité Trier ;
    2. AddParam("vector", vector.SelectMany(BitConverter.GetBytes).ToArray()) : convertit le tableau à virgule flottante en un tableau d'octets et le transmet à la requête en tant que paramètre de requête ;
    3. SetSortBy("score") : Trie les résultats selon le score de similarité ;
    4. ReturnFields("content", "score") : renvoie les deux champs content et score de l'ensemble de résultats ;
    5. Limit(0, limit) : Limite la position de départ de l'ensemble de résultats à 0 et le nombre de résultats à limiter ;
    6. Dialecte(2) : définissez le dialecte de requête sur 2, qui est le langage de requête par défaut de Redis, Redis Protocol ;
  • Appelez la méthode de recherche asynchrone ft.SearchAsync(indexName, query) et attendez les résultats de la recherche ;
  • Parcourez l'ensemble de résultats de recherche result.Documents, convertissez chaque document en un tuple (contenu de chaîne, double score) et parcourez l'instruction de rendement.

Requête de plage :

Les requêtes de plage fournissent un moyen de filtrer les résultats en fonction de la distance entre un champ vectoriel dans Redis et un vecteur de requête basé sur un seuil prédéfini (rayon). Semblables aux clauses NUMERIC et GEO, elles peuvent apparaître plusieurs fois dans la requête, notamment pour les recherches mixtes avec KNN.

public static async IAsyncEnumerable<(string Tag, string Content, double Score)> SearchAsync(string tag, float[] vector, int limit)
{
    var query = new Query($"(@tag:{tag})=>[KNN {limit} @vector $vector AS score]")
                .AddParam("vector", vector.SelectMany(BitConverter.GetBytes).ToArray())
                .SetSortBy("score")
                .ReturnFields("tag", "content", "score")
                .Limit(0, limit)
                .Dialect(2);

    var result = await ft.SearchAsync(indexName, query).ConfigureAwait(false);
    foreach (var document in result.Documents)
    {
        yield return (document["tag"], document["content"], Convert.ToDouble(document["score"]));
    }
}

Ce code utilise une requête mixte de KNN et Range. Par rapport au code précédent, un nouveau paramètre @tag est ajouté, ce qui limitera les résultats à inclure uniquement le contenu de la balise donnée. Cela peut augmenter la précision de la requête et améliorer son efficacité.

5. Supprimer les vecteurs de la bibliothèque d'index

public async Task DeleteAsync(string docId, string prefix) 
{ 
    wait db.KeyDeleteAsync($"{prefix}{docId}"); 
}

Cette méthode supprime les données vectorielles spécifiées de la bibliothèque d'index en supprimant la clé de cache de hachage associée au vecteur spécifié.

6. Supprimez la bibliothèque d'index vectoriels

public async Task DropIndexAsync() 
{ 
    wait ft.DropIndexAsync(indexName, true); 
}

Cette méthode wait ft.DropIndexAsync accepte deux paramètres : indexName et true . indexName indique le nom de la bibliothèque d'index et true indique s'il faut supprimer le fichier d'index lors de la suppression de l'index.

7. Interroger les informations de la base de données d'index

public async Task<InfoResult> InfoAsync() 
{ 
    return wait ft.InfoAsync(indexName); 
}

Grâce à la méthode wait ft.InfoAsync(indexName), nous pouvons obtenir la taille de la bibliothèque d'index spécifiée, le nombre de documents et d'autres informations associées sur la bibliothèque d'index.

La démo complète est la suivante :

en utilisant NRedisStack ; 
en utilisant NRedisStack.Search ; 
en utilisant NRedisStack.Search.DataTypes ; 
en utilisant NRedisStack.Search.Literals.Enums ; 
en utilisant StackExchange.Redis ; 
en utilisant NRedisStack.Search.Schema statique ; 

espace de noms RedisVectorExample 
{ 
    class Program 
    { 
        //Créer une connexion Redis 
        statique ConnectionMultiplexer mux = ConnectionMultiplexer.Connect("localhost"); 
        //Obtenir une base de données Redis 
        statique IDatabase db = mux.GetDatabase(); 
        //Créer un client RediSearch 
        static SearchCommands ft = new SearchCommands(db, null); 
        //Nom de l'index 
        chaîne statique indexName = "test:index" ; 
        //Préfixe d'index
        static string prefix = "test:data"; 
        static async Task Main(string[] args) 
        {   
            //Créer un index vectoriel 
            wait CreateIndexAsync(); 

            //Ajouter des vecteurs à l'index 
            wait SetAsync("1", "A " , "Données de test A1", new float[] { 0.1f, 0.2f }); 
            wait SetAsync("2", "A", "Données de test A2", new float[] { 0.3f, 0.4f }) ; 
            wait SetAsync("3", "B", "Données de test B1", new float[] { 0.5f, 0.6f }); wait SetAsync("4", "C" , "Données de test C1", new float 
            [ ] { 0,7f, 0.8f }); 

            //Supprimer un vecteur 
            wait DeleteAsync("4"); 

            //Recherche KUN  
            en attente pour chaque (var (Content, Score) in SearchAsync(new float[] { 0.1f, 0.2f }, 2))
            { 
                Console.WriteLine($"Content : {Content}, score de similarité : {Score}");
            } 
            //Attente mixte pour chaque (var (Tag, Content, Score) in SearchAsync("A", new float[] { 0.1f, 0.2f }, 2)) { Console.WriteLine($"Tag: { 

            Tag 
            } 
                , Contenu : {Content}, score de similarité : {Score}"); 
            } 
            //Vérifier si l'index existe 
            var info = wait InfoAsync(); 
            if (info != null) 
                wait DropIndexAsync(); //Supprimer l'index s'il existe 
        } 
        tâche asynchrone statique publique CreateIndexAsync() 
        { 
            wait ft.CreateAsync(indexName, 
                new FTCreateParams()
            
 
                            .On(IndexDataType.HASH)
                            .Prefix(préfixe), 
                new Schema()
                            .AddTagField("tag") 
                            .AddTextField("content") 
                            .AddVectorField("vecteur", 
                                VectorField.VectorAlgo.HNSW, 
                                new Dictionary<string, object>() 
                                { 
                                    ["TYPE"] = "FLOAT32", 
                                    ["DIM "] = 2, 
                                    ["DISTANCE_METRIC"] = "COSINE" 
                                })); 
        } 

        Tâche asynchrone statique publique SetAsync (string docId, balise de chaîne, contenu de la chaîne, 
            wait db.HashSetAsync($"{prefix}{docId}", new HashEntry[] { 
                new HashEntry ("tag", tag), 
                new HashEntry ("contenu", contenu), 
                new HashEntry ("vecteur", vector.SelectMany(BitConverter.GetBytes).ToArray()) } 
            ); 
        } 

        public static async Task DeleteAsync(string docId) 
        { 
            wait db.KeyDeleteAsync($"{prefix}{docId}"); 
        } 

        Tâche asynchrone statique publique DropIndexAsync() 
        { 
            wait ft.DropIndexAsync(indexName, 
        }
 
        tâche asynchrone statique publique<InfoResult> InfoAsync() 
        { 
            return wait ft.InfoAsync(indexName); 
        }

        public static async IAsyncEnumerable<(string Content, double Score)> SearchAsync(float[] vector, int limit) 
        { 
            var query = new Query($"*=>[KNN {limit} @vector $vector AS score]") 
                        . AddParam("vecteur", vector.SelectMany(BitConverter.GetBytes).ToArray()) 
                        .SetSortBy("score") 
                        .ReturnFields("content", "score") 
                        .Limit(0, limite) 
                        .Dialect(2); 

            var result = wait ft.SearchAsync (indexName, query).
            foreach (var document dans result.Documents) 
            {
                rendement return (document["content"], Convert.ToDouble(document["score"])); 
            } 
        } 

        public static async IAsyncEnumerable<(string Tag, string Content, double Score)> SearchAsync(string tag, float[] vector, int limit) { var 
        query 
            = new Query($"(@tag:{tag})=> [KNN {limite} @vecteur $vecteur AS score]") 
                        .AddParam("vecteur", vecteur.SelectMany(BitConverter.GetBytes).ToArray()) 
                        .SetSortBy("score") 
                        .ReturnFields("tag", "content ", "score") 
                        .Limit(0, limite) 
                        .Dialect(2);

            var result = wait ft.SearchAsync(indexName, query).ConfigureAwait(false); 
            foreach (var document dans result.Documents) 
            { 
                rendement return (document["tag"], document["content"], Convert.ToDouble(document["score"])); 
            } 
        } 
} 
    }

Pour des raisons d'espace, nous nous arrêterons d'abord ici. Dans le prochain article, nous expliquerons comment utiliser la technologie ChatGPT Embeddings pour extraire des vecteurs de texte et implémenter une correspondance de similarité de texte basée sur Redis. Par rapport aux méthodes traditionnelles, cette méthode peut mieux conserver les informations sémantiques et émotionnelles du texte, reflétant ainsi plus précisément le contenu substantiel du texte.

Je suppose que tu aimes

Origine blog.csdn.net/weixin_46437112/article/details/131988611
conseillé
Classement