利用Redis实现向量相似度搜索:解决文本、图像和音频之间的相似度匹配问题

在自然语言处理领域,有一个常见且重要的任务就是文本相似度搜索。文本相似度搜索是指根据用户输入的一段文本,从数据库中找出与之最相似或最相关的一段或多段文本。它可以应用在很多场景中,例如问答系统、推荐系统、搜索引擎等。

比如,当用户在知乎上提出一个问题时,系统就可以从知乎上已有的回答中找出与该问题最匹配或最有价值的回答,并展示给用户。

要实现类似高效的搜索,我们需要使用一些特殊的数据结构和算法。其中,向量相似度搜索是一种在大规模数据搜索中表现优秀的算法。而Redis作为一种高性能的键值数据库,也可以帮助我们实现向量相似度搜索。

在开始学习如何使用Redis实现向量相似度搜索之前,需要了解向量及向量相似度搜索的基本知识和原理,以便更好地理解后面的内容。

什么是向量?

向量是数学、物理学和工程科学等多个自然科学中的基本概念,它是一个具有方向和长度的量,用于描述问题,如空间几何、力学、信号处理等。在计算机科学中,向量被用于表示数据,如文本、图像或音频。此外,向量还代表AI模型对文本、图像、音频、视频等非结构化数据的印象。

向量相似度搜索的基本原理

向量相似度搜索的基本原理是通过将数据集中的每个元素映射为向量,并使用特定相似度计算算法,如基于余弦相似度的、基于欧氏相似度或基于Jaccard相似度等算法,找到与查询向量最相似的向量。

Redis实现向量相似度搜索

了解原理后,我们开始来实现如何使用Redis实现向量相似度搜索。Redis允许我们在FT.SEARCH命令中使用向量相似度查询。使我们可以加载、索引和查询作为Redis哈希或JSON文档中字段存储的向量。

//相关文档地址

Vector similarity | Redis

1、Redis Search安装

关于Redis Search的安装和使用,此处不再赘述,如果您对此不熟悉,可以参考上一篇文章:

C#+Redis Search:如何用Redis实现高性能全文搜索

2、创建向量索引库

这里我们使用NRedisStack和StackExchange.Redis两个库来与Redis进行交互操作。

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

在进行向量搜索之前,首先需要定义并创建索引,并指定相似性算法。

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

这段代码的意思是:

  • 使用了一个异步方法 ft.CreateAsync 来创建索引。它接受三个参数:索引名称 indexName,一个 FTCreateParams 对象和一个 Schema 对象;
  • FTCreateParams 类提供了一些参数选项,用于指定索引的参数。这里使用 .On(IndexDataType.HASH)  方法来指定索引数据类型为哈希,并使用 .Prefix(prefix)  方法来指定索引数据的前缀;
  • Schema 类用于定义索引中的字段和字段类型。这里定义了一个标签字段(tag field)用于区分过虑数据。定义了一个文本字段(text field)用于存储原始数据,以及一个向量字段(vector field)用于存储经原始数据转化后的向量数据;
  • 使用了 VectorField.VectorAlgo.HNSW 来指定向量算法为 HNSW(Hierarchical Navigable Small World)。还传递了一个字典对象,用于设置向量字段的参数。其中,键为字符串类型,值为对象类型。

目前Redis支持两种相似度算法:

HNSW分层导航小世界算法,使用小世界网络构建索引,具有快速查询速度和小内存占用,时间复杂度为O(logn),适用于大规模索引。

FLAT暴力算法,它对所有的键值对进行扫描,然后根据键值对的距离计算出最短路径,时间复杂度为O(n),其中n是键值对的数量。这种算法时间复杂度非常高,只适用于小规模的索引。

3、添加向量到索引库

索引创建后,我们将数据添加到索引中。

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

SetAsync方法用于将一个具有指定文档ID、前缀、标签、内容及内容的向量存储到索引库中。并使用SelectMany()方法和BitConverter.GetBytes()方法将向量转换为一个字节数组。

4、向量搜索

Redis 支持两种类型的向量查询:KNN查询和Range查询,也可以将两种查询混合使用。

KNN 查询

KNN 查询用于在给定查询向量的情况下查找前 N 个最相似的向量。

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

这段代码的意思是:

  • 创建一个查询对象 query,并设置查询条件。查询条件包括:
    1. "*=>[KNN {limit} @vector $vector AS score]":使用KNN算法进行向量相似度搜索,限制结果数量为limit,使用给定的向量vector作为查询向量,将查询结果按照相似度得分进行排序;
    2. AddParam("vector", vector.SelectMany(BitConverter.GetBytes).ToArray()):将浮点数数组转换为字节数组,并将其作为查询参数传递给查询;
    3. SetSortBy("score"):按照相似度得分对结果进行排序;
    4. ReturnFields("content", "score"):将content和score两个字段从结果集中返回;
    5. Limit(0, limit):限制结果集的起始位置为0,结果数量为limit;
    6. Dialect(2):设置查询方言为2,即Redis默认的查询语言Redis Protocol;
  • 调用异步搜索方法 ft.SearchAsync(indexName, query),并等待搜索结果;
  • 遍历搜索结果集 result.Documents,将每个文档转换为 (string Content, double Score) 元组,并通过 yield 语句进行迭代返回。

Range 查询:

Range查询提供了一种根据 Redis 中的向量字段与基于某些预定义阈值(半径)的查询向量之间的距离来过滤结果的方法。类似于 NUMERIC 和 GEO 子句,可以在查询中多次出现,特别是可以和 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"]));}
}

这段代码使用了KNN和Range混合查询,与上一段代码相比,新增了@tag参数,将限制结果仅包含给定标签的内容。这样做可以增加查询的准确性,提高查询效率。

5、从索引库中删除向量

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

这个方法通过删除与指定向量相关联的哈希缓存键,来实现从索引库中删除指定向量数据。

6、删除向量索引库

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

这个方法 await ft.DropIndexAsync接受两个参数: indexName 和 true 。indexName 表示索引库的名称, true 表示在删除索引时是否删除索引文件。

7、查询索引库信息

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

通过 await ft.InfoAsync(indexName) 方法,我们可以获取到指定索引库的大小,文档数量等相关索引库信息。

完整 Demo 如下:

using NRedisStack;
using NRedisStack.Search;
using NRedisStack.Search.DataTypes;
using NRedisStack.Search.Literals.Enums;
using StackExchange.Redis;
using static NRedisStack.Search.Schema;namespace RedisVectorExample
{class Program{//创建一个Redis连接static ConnectionMultiplexer mux = ConnectionMultiplexer.Connect("localhost");//获取一个Redis数据库static IDatabase db = mux.GetDatabase();//创建一个RediSearch客户端static SearchCommands ft = new SearchCommands(db, null);//索引名称static string indexName = "test:index";//索引前缀static string prefix = "test:data";static async Task Main(string[] args){  //创建一个向量的索引await CreateIndexAsync();//添加一些向量到索引中await SetAsync("1", "A", "测试数据A1", new float[] { 0.1f, 0.2f });await SetAsync("2", "A", "测试数据A2", new float[] { 0.3f, 0.4f });await SetAsync("3", "B", "测试数据B1", new float[] { 0.5f, 0.6f });await SetAsync("4", "C", "测试数据C1", new float[] { 0.7f, 0.8f });//删除一个向量await DeleteAsync("4");//KUN搜索 await foreach (var (Content, Score) in SearchAsync(new float[] { 0.1f, 0.2f }, 2)){Console.WriteLine($"内容:{Content},相似度得分:{Score}");}//混合await foreach (var (Tag, Content, Score) in SearchAsync("A", new float[] { 0.1f, 0.2f }, 2)){Console.WriteLine($"标签:{Tag},内容:{Content},相似度得分:{Score}");}//检查索引是否存在var info = await InfoAsync();if (info != null)await DropIndexAsync(); //存在则删除索引}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"}));}public static async Task SetAsync(string docId, 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())});}public static async Task DeleteAsync(string docId){await db.KeyDeleteAsync($"{prefix}{docId}");}public static async Task DropIndexAsync(){await ft.DropIndexAsync(indexName, true);}public static async Task<InfoResult> InfoAsync(){return await 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("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"]));}}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"]));}}}
}

篇幅原因先到这里,下一篇我们接着探讨如何利用ChatGPT Embeddings技术提取文本向量,并基于Redis实现文本相似度匹配。相比传统方法,这种方式能够更好地保留文本的语义和情感信息,从而更准确地反映文本的实质性内容。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/11635.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

数据库CAST()函数,格式(CAST AS decimal)

语法&#xff1a; CAST (expression AS data_type) 参数说明&#xff1a; expression&#xff1a;任何有效的SQServer表达式。 AS&#xff1a;用于分隔两个参数&#xff0c;在AS之前的是要处理的数据&#xff0c;在AS之后是要转换的数据类型。 data_type&#xff1a;目标系统…

【个人笔记】Linux命令之watch命令

1.命令简介 watch 以周期性方式执行给定的命令&#xff0c;并全屏显示执行结果&#xff0c;可以帮助监测一个命令的运行结果。 2.命令格式及参数选项说明 命令格式&#xff1a; watch [OPTIONS] COMMAND选项说明&#xff1a; -d, --differences [PERMANENT]高亮显示最近两…

redis中List<String>缓存处理

放入redis List<String> strList ["1","2"]; // 把list转化成String放入缓存中 redisUtil.set(key, JSONObject.toJSONString(strList),300);从redis取出 Object object redisUtil.get(key); List<String> strList1 null; if (Objects.no…

c语言的数据类型 -- 与GPT对话

1 c语言的数据类型 在C语言中,数据类型用于定义变量的类型和存储数据的方式。C语言支持多种数据类型,包括基本数据类型和派生数据类型。以下是C语言中常见的数据类型: 基本数据类型(Primary Data Types): int: 整数类型,通常表示带符号的整数。char: 字符类型,用于存储…

Java 生成随机数据

文章目录 1. Java-faker依赖demo 2. common-random依赖demo 1. Java-faker 依赖 <dependency><groupId>com.github.javafaker</groupId><artifactId>javafaker</artifactId><version>1.0.2</version> </dependency>https://…

ES6基础知识二:ES6中数组新增了哪些扩展?

一、扩展运算符的应用 ES6通过扩展元素符…&#xff0c;好比 rest 参数的逆运算&#xff0c;将一个数组转为用逗号分隔的参数序列 console.log(...[1, 2, 3]) // 1 2 3console.log(1, ...[2, 3, 4], 5) // 1 2 3 4 5[...document.querySelectorAll(div)] // [<div>, &l…

exoplayer3 ffmpeg 扩展库编译 aar,导入集成

exoplayer3 ffmpeg 扩展库编译 aar&#xff0c;导入集成。 已经编译完成的aar&#xff1a;https://download.csdn.net/download/mhhyoucom/88086822 编译项目方法&#xff1a; github下载项目&#xff1a;https://github.com/google/ExoPlayer FFmpeg 模块提供 &#xff0c;…

【机器学习】基础知识点的汇总与总结!更新中

文章目录 一、监督学习1.1、单模型1.1.1、线性回归1.1.2、逻辑回归&#xff08;Logistic Regression&#xff09;1.1.3、K近邻算法&#xff08;KNN&#xff09;1.1.4、决策树1.1.5、支持向量机&#xff08;SVM&#xff09;1.1.6、朴素贝叶斯 1.2、集成学习1.2.1、Boosting1&…

IFIX5.8安装教程

管理员身份运行&#xff1a; 安装&#xff1a; 下次安装的时候选择SCADA服务器&#xff0c;独立。然后下图就不会出现了。 重启电脑&#xff1a;

安装python需要多大内存,python下载安装包多大

大家好&#xff0c;小编来为大家解答以下问题&#xff0c;安装python需要多大内存&#xff0c;python安装占多大空间&#xff0c;现在让我们一起来看看吧&#xff01; 1、pytorch包有多大 938.79MB。pytorch包有938.79MB&#xff0c;pytorch离线安装包是一个不错的学习资源&am…

8款常见的自动化测试开源框架

在如今开源的时代&#xff0c;我们就不要再闭门造车了&#xff0c;热烈的拥抱开源吧&#xff01;本文针对性能测试、Web UI 测试、API 测试、数据库测试、接口测试、单元测试等方面&#xff0c;为大家整理了github或码云上优秀的自动化测试开源项目&#xff0c;希望能给大家带来…

OSPF路由协议(红茶三杯CCNA)

链路状态路由协议 OSPF&#xff08;开放式最短路径优先&#xff09;Open Shortest Path First 是一种链路状态路由协议&#xff0c;无路由循环&#xff08;全局拓扑&#xff09;&#xff0c;RFC2328 “开放”意味着非私有的 管理型距离&#xff1a;110 OSPF采用SPF算法计算到达…

Mnist分类与气温预测任务

目录 传统机器学习与深度学习的特征工程特征向量pytorch实现minist代码解析归一化损失函数计算图Mnist分类获取Mnist数据集&#xff0c;预处理&#xff0c;输出一张图像面向工具包编程使用TensorDataset和DataLoader来简化数据预处理计算验证集准确率 气温预测回归构建神经网络…

java商城系统和php商城系统对比

java商城系统和php商城系统是两种常见的电子商务平台&#xff0c;它们都具有一定的优势和劣势。那么&#xff0c;java商城系统和php商城系统又有哪些差异呢&#xff1f; 一、开发难度 Java商城系统和PHP商城系统在开发难度方面存在一定的差异。Java商城系统需要使用Java语言进…

数据结构 | 基本数据结构——栈

目录 一、线性数据结构 二、栈 2.1 何谓栈 2.2 栈抽象数据类型 2.3 用Python实现栈 2.4 匹配括号 2.5 普通情况&#xff1a;匹配符号 2.6 将十进制数转换成二进制数 3.7 前序、中序和后序表达式 3.7.1 从中序到后序的通用转换法 3.7.2 计算后序表达式 一、线性数据结…

[内网渗透]SUID提权

文章目录 [内网渗透]SUID提权0x01.什么是SUID&#xff1f;0x02.如何设置SUID&#xff1f;0x03.查找属主为root的SUID文件0x04.进行SUID提权1.find提权2.vim/vi/vim.tiny 以root权限修改文件3.bash提权4.less/more执行系统命令5.nano以root权限修改文件6.awk执行系统命令7.cp以r…

开源计算机视觉库OpenCV详解

目录 1、概述 2、OpenCV详细介绍 2.1、OpenCV的起源 2.2、OpenCV开发语言 2.3、OpenCV的应用领域 3、OpenCV模块划分 4、OpenCV源码文件结构 4.1、根目录介绍 4.2、常用模块介绍 4.3、CUDA加速模块 5、OpenCV配置以及Visual Studio使用OpenCV 6、关于Lena图片 7、…

LLM-Blender:大语言模型也可以进行集成学习

最近在看arxiv的时候发现了一个有意思的框架&#xff1a;LLM-Blender&#xff0c;它可以使用Ensemble 的方法来对大语言模型进行集成。 官方介绍如下&#xff1a;LLM-Blender是一个集成框架&#xff0c;可以通过利用多个开源大型语言模型(llm)的不同优势来获得始终如一的卓越性…

TCP如何保证服务的可靠性

TCP如何保证服务的可靠性 确认应答超时重传流量控制滑动窗口机制概述发送窗口和接收窗口的工作原理几种滑动窗口协议1比特滑动窗口协议&#xff08;停等协议&#xff09;后退n协议选择重传协议 采用滑动窗口的问题&#xff08;死锁可能&#xff0c;糊涂窗口综合征&#xff09;死…

linux 下va_start,va_end,va_arg,va_list这些宏到底是什么?

/* author: hjjdebug * date: 2023年 07月 27日 星期四 10:50:21 CST * descriptor: linux 下va_start,va_end,va_arg,va_list这些宏到底是什么? */ #include <stdarg.h> #include <stdio.h> void test_va(int num,...) { va_list args; …