Uso de Redis para implementar la búsqueda de similitud vectorial: resolver el problema de coincidencia de similitud entre texto, imágenes y audio

En el campo del procesamiento del lenguaje natural, una tarea común e importante es la búsqueda de similitudes de texto. La búsqueda de similitud de texto se refiere a encontrar el párrafo o párrafos de texto más similares o más relevantes de la base de datos en función de un fragmento de texto ingresado por el usuario. Se puede aplicar en muchos escenarios, como sistemas de preguntas y respuestas, sistemas de recomendación, motores de búsqueda, etc.

Por ejemplo, cuando un usuario hace una pregunta en Zhihu, el sistema puede encontrar la respuesta que mejor coincida o sea la más valiosa para la pregunta entre las respuestas existentes en Zhihu y mostrársela al usuario.

Para lograr una búsqueda igualmente eficiente, necesitamos utilizar algunas estructuras de datos y algoritmos especiales. Entre ellos, la búsqueda por similitud de vectores es un algoritmo que funciona bien en la búsqueda de datos a gran escala. Como base de datos de valores clave de alto rendimiento, Redis también puede ayudarnos a implementar la búsqueda de similitud de vectores.

Antes de comenzar a aprender cómo usar Redis para implementar la búsqueda por similitud de vectores, debe comprender los conocimientos y principios básicos de los vectores y la búsqueda por similitud de vectores para comprender mejor el siguiente contenido.

¿Qué es un vector?

Vector es un concepto básico en muchas ciencias naturales como matemáticas, física y ciencias de la ingeniería, es una cantidad con dirección y longitud y se utiliza para describir problemas como geometría espacial, mecánica, procesamiento de señales, etc. En informática, los vectores se utilizan para representar datos como texto, imágenes o audio. Además, los vectores también representan la impresión del modelo de IA de datos no estructurados como texto, imágenes, audio y video.

Principios básicos de la búsqueda de similitud de vectores.

El principio básico de la búsqueda de similitud de vectores es asignar cada elemento del conjunto de datos a un vector y utilizar un algoritmo de cálculo de similitud específico, como algoritmos de similitud basados ​​en coseno, basados ​​en similitud euclidiana o basados ​​en Jaccard para encontrar los vectores de consulta que son más similares entre sí.

Redis implementa la búsqueda de similitud vectorial

Después de comprender el principio, comenzamos a implementar cómo usar Redis para implementar la búsqueda por similitud de vectores. Redis nos permite utilizar consultas de similitud de vectores en el comando FT.SEARCH. Nos permite cargar, indexar y consultar vectores almacenados como campos en hashes de Redis o documentos JSON.

//Dirección del documento relacionado

Similitud vectorial | Redis

1. Instalación de búsqueda de Redis

Con respecto a la instalación y uso de Redis Search, no entraré en detalles aquí, si no está familiarizado con esto, puede consultar el artículo anterior:

C#+Redis Search: cómo utilizar Redis para implementar una búsqueda de texto completo de alto rendimiento

2. Cree una biblioteca de índices vectoriales

Aquí usamos NRedisStack y StackExchange.Redis dos bibliotecas para interactuar con Redis.

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

Antes de realizar una búsqueda vectorial, primero debe definir y crear un índice y especificar un algoritmo de similitud.

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"
                        }));
}

Lo que este código significa es:

  • Se utiliza un método asincrónico ft.CreateAsync para crear el índice. Acepta tres parámetros: el nombre del índice indexName, un objeto FTCreateParams y un objeto Schema;
  • La clase FTCreateParams proporciona algunas opciones de parámetros para especificar los parámetros del índice. Aquí, el método .On(IndexDataType.HASH) se usa para especificar el tipo de datos del índice como hash, y el método .Prefix(prefix) se usa para especificar el prefijo de los datos del índice;
  • La clase de esquema se utiliza para definir los campos y los tipos de campos en el índice. Aquí se define un campo de etiqueta para distinguir los datos filtrados. Se define un campo de texto para almacenar los datos originales y un campo de vector se utiliza para almacenar los datos vectoriales convertidos a partir de los datos originales;
  • VectorField.VectorAlgo.HNSW se utiliza para especificar el algoritmo vectorial como HNSW (Hierarchical Navigable Small World). También se pasa un objeto de diccionario para configurar los parámetros del campo vectorial. Entre ellos, la clave es de tipo cadena y el valor es de tipo objeto.

Actualmente Redis admite dos algoritmos de similitud:

El algoritmo de mundo pequeño de navegación jerárquica HNSW utiliza una red de mundo pequeño para crear índices. Tiene una velocidad de consulta rápida y una huella de memoria pequeña. La complejidad del tiempo es O (logn) y es adecuada para indexación a gran escala.

El algoritmo de fuerza bruta FLAT escanea todos los pares clave-valor y luego calcula la ruta más corta en función de la distancia entre los pares clave-valor. La complejidad del tiempo es O (n), donde n es el número de pares clave-valor. Este algoritmo tiene una complejidad temporal muy alta y solo es adecuado para índices de pequeña escala.

3. Agregue el vector a la biblioteca de índice.

Una vez creado el índice, agregamos datos al índice.

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())
    });
}

El método SetAsync se utiliza para almacenar un vector con el ID del documento, el prefijo, la etiqueta, el contenido y el contenido especificados en la biblioteca de índice. Y utilice el método SelectMany() y el método BitConverter.GetBytes() para convertir el vector en una matriz de bytes.

4. Búsqueda de vectores

Redis admite dos tipos de consultas vectoriales: consultas KNN y consultas de rango, y los dos tipos de consultas también se pueden combinar.

consulta KNN

La consulta KNN se utiliza para encontrar los N vectores más similares dado un vector de consulta.

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"]));
    }
}

Lo que este código significa es:

  • Cree una consulta de objeto de consulta y establezca condiciones de consulta. Las condiciones de consulta incluyen:
    1. "*=>[KNN {límite} @vector $vector AS puntuación]": utilice el algoritmo KNN para realizar una búsqueda de similitud de vectores, limite el número de resultados, utilice el vector dado como vector de consulta y califique los resultados de la consulta. según similitud Ordenar;
    2. AddParam("vector", vector.SelectMany(BitConverter.GetBytes).ToArray()): convierte la matriz de punto flotante en una matriz de bytes y la pasa a la consulta como parámetro de consulta;
    3. SetSortBy ("score"): ordena los resultados según la puntuación de similitud;
    4. ReturnFields("content", "score"): devuelve el contenido de los dos campos y la puntuación del conjunto de resultados;
    5. Límite (0, límite): limita la posición inicial del resultado establecido en 0 y el número de resultados a limitar;
    6. Dialecto (2): establezca el dialecto de consulta en 2, que es el idioma de consulta predeterminado de Redis, Protocolo Redis;
  • Llame al método de búsqueda asincrónica ft.SearchAsync(indexName, query) y espere los resultados de la búsqueda;
  • Itere sobre el conjunto de resultados de búsqueda result.Documents, convierta cada documento en una tupla (contenido de cadena, puntuación doble) y repita la declaración de rendimiento.

Consulta de rango:

Las consultas de rango proporcionan una forma de filtrar resultados según la distancia entre un campo vectorial en Redis y un vector de consulta según algún umbral predefinido (radio). Al igual que las cláusulas NUMERIC y GEO, pueden aparecer varias veces en la consulta, especialmente para búsquedas mixtas con 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"]));
    }
}

Este código utiliza una consulta mixta de KNN y Range. En comparación con el código anterior, se agrega un nuevo parámetro @tag, que limitará los resultados para incluir solo el contenido de la etiqueta dada. Hacerlo puede aumentar la precisión de la consulta y mejorar la eficiencia de la consulta.

5. Eliminar el vector de la biblioteca de índice.

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

Este método elimina los datos del vector especificado de la biblioteca de índice eliminando la clave de caché hash asociada con el vector especificado.

6. Eliminar la biblioteca de índices vectoriales.

tarea asíncrona pública DropIndexAsync() 
{ 
    espera ft.DropIndexAsync(indexName, true); 
}

Este método await ft.DropIndexAsync acepta dos parámetros: indexName y true. indexName indica el nombre de la biblioteca de índice y verdadero indica si se debe eliminar el archivo de índice al eliminar el índice.

7. Consultar información de la base de datos del índice.

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

A través del método await ft.InfoAsync (indexName), podemos obtener el tamaño de la biblioteca de índice especificada, la cantidad de documentos y otra información relacionada de la biblioteca de índice.

La demostración completa es la siguiente:

usando NRedisStack; 
usando NRedisStack.Search; 
usando NRedisStack.Search.DataTypes; 
usando NRedisStack.Search.Literals.Enums; 
usando StackExchange.Redis; 
usando NRedisStack.Search.Schema estático; 

espacio de nombres RedisVectorExample 
{ 
    clase Programa 
    { 
        //Crear una conexión Redis 
        estática ConnectionMultiplexer mux = ConnectionMultiplexer.Connect("localhost"); 
        //Obtener una base de datos Redis 
        IDatabase estática db = mux.GetDatabase(); 
        //Crear un cliente RediSearch 
        estático SearchCommands ft = new SearchCommands(db, null); 
        //Nombre del índice 
        cadena estática indexName = "prueba: índice"; 
        //Prefijo de índice
        static string prefix = "test:data"; 
        static async Task Main(string[] args) 
        {   
            //Crear un índice vectorial 
            await CreateIndexAsync(); 

            //Agregar algunos vectores al índice 
            await SetAsync("1", "A " , "Datos de prueba A1", nuevo flotante[] { 0.1f, 0.2f }); 
            await SetAsync("2", "A", "Datos de prueba A2", nuevo flotante[] { 0.3f, 0.4f }); 
            await SetAsync("3", "B", "Datos de prueba B1", nuevo float[] { 0.5f, 0.6f }); await SetAsync("4", "C", "Datos de prueba C1", nuevo float 
            [ ] { 0,7f, 0.8f }); 

            //Eliminar un vector 
            await DeleteAsync("4"); 

            //Búsqueda KUN  
            await foreach (var (Contenido, Puntuación) en SearchAsync(new float[] { 0.1f, 0.2f }, 2))
            { 
                Console.WriteLine($"Contenido: {Contenido}, puntuación de similitud: {Puntuación}");
            } 

            //Mixto 
            await foreach (var (Etiqueta, Contenido, Puntuación) en SearchAsync("A", new float[] { 0.1f, 0.2f }, 2)) { Console.WriteLine($"Etiqueta: {Etiqueta 
            } 
                , Contenido: {Contenido}, puntuación de similitud: {Puntuación}"); 
            } 
            
            //Comprueba si el índice existe 
            var info = await InfoAsync(); 
            if (info != null) 
                await DropIndexAsync(); //Elimina el índice si existe 
        } 

        tarea asíncrona estática pública CreateIndexAsync() 
        { 
            espera ft.CreateAsync(indexName, 
                nuevo FTCreateParams() 
                            .On(IndexDataType.HASH)
                            .Prefix(prefijo), 
                nuevo esquema() 
                            .AddTagField("etiqueta") 
                            .AddTextField("contenido") 
                            .AddVectorField("vector", 
                                VectorField.VectorAlgo.HNSW, 
                                nuevo Diccionario<cadena, objeto>() 
                                { 
                                    ["TIPO "] = "FLOAT32", 
                                    ["DIM"] = 2, 
                                    ["DISTANCE_METRIC"] = "COSINO" 
                                })); 
        }
 
        Tarea asíncrona estática pública SetAsync(cadena docId, etiqueta de cadena, contenido de cadena, 
            await db.HashSetAsync($"{prefix}{docId}", new HashEntry[] { 
                new HashEntry ("etiqueta", etiqueta), 
                new HashEntry ("contenido", contenido), 
                new HashEntry ("vector", vector.SelectMany(BitConverter.GetBytes).ToArray()) } 
            ); 
        } 

        Tarea asíncrona estática pública DeleteAsync(string docId) 
        { 
            await db.KeyDeleteAsync($"{prefix}{docId}"); 
        } 

        Tarea asíncrona estática pública DropIndexAsync() 
        { 
            await ft.DropIndexAsync(indexName, 
        }
 
        Tarea asíncrona estática pública<InfoResult> InfoAsync() 
        { 
            return await ft.InfoAsync(indexName); 
        }

        público estático asíncrono 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, límite) 
                        .Dialect(2); 

            resultado var = esperar ft.SearchAsync(nombreíndice, consulta).
            foreach (var documento en resultado.Documentos) 
            {
                rendimiento retorno (documento["contenido"], Convert.ToDouble(documento["puntuación"])); 
            } 
        } 

        public static async IAsyncEnumerable<(etiqueta de cadena, contenido de cadena, puntuación doble)> SearchAsync(etiqueta de cadena, vector flotante[], límite int) { var 
        consulta 
            = nueva consulta($"(@tag:{tag})=> [KNN {límite} @vector $vector AS puntuación]") 
                        .AddParam("vector", vector.SelectMany(BitConverter.GetBytes).ToArray()) 
                        .SetSortBy("puntuación") 
                        .ReturnFields("etiqueta", "contenido ", "puntuación") 
                        .Limit(0, límite) 
                        .Dialect(2);

            resultado var = esperar ft.SearchAsync(indexName, consulta).ConfigureAwait(false); 
            foreach (var documento en resultado.Documentos) 
            { 
                rendimiento retorno (documento["etiqueta"], documento["contenido"], Convert.ToDouble(documento["puntuación"])); 
            } 
        } 
} 
    }

Por razones de espacio, nos detendremos aquí primero. En el próximo artículo, analizaremos cómo utilizar la tecnología ChatGPT Embeddings para extraer vectores de texto e implementar coincidencias de similitud de texto basadas en Redis. En comparación con los métodos tradicionales, este método puede retener mejor la información semántica y emocional del texto, reflejando así con mayor precisión el contenido sustancial del texto.

Supongo que te gusta

Origin blog.csdn.net/weixin_46437112/article/details/131988611
Recomendado
Clasificación