使用Redis實(shí)現(xiàn)向量相似度搜索
在自然語(yǔ)言處理領(lǐng)域,有一個(gè)常見且重要的任務(wù)就是文本相似度搜索。文本相似度搜索是指根據(jù)用戶輸入的一段文本,從數(shù)據(jù)庫(kù)中找出與之最相似或最相關(guān)的一段或多段文本。它可以應(yīng)用在很多場(chǎng)景中,例如問答系統(tǒng)、推薦系統(tǒng)、搜索引擎等。
比如,當(dāng)用戶在知乎上提出一個(gè)問題時(shí),系統(tǒng)就可以從知乎上已有的回答中找出與該問題最匹配或最有價(jià)值的回答,并展示給用戶。
要實(shí)現(xiàn)類似高效的搜索,我們需要使用一些特殊的數(shù)據(jù)結(jié)構(gòu)和算法。其中,向量相似度搜索是一種在大規(guī)模數(shù)據(jù)搜索中表現(xiàn)優(yōu)秀的算法。而Redis作為一種高性能的鍵值數(shù)據(jù)庫(kù),也可以幫助我們實(shí)現(xiàn)向量相似度搜索。
在開始學(xué)習(xí)如何使用Redis實(shí)現(xiàn)向量相似度搜索之前,需要了解向量及向量相似度搜索的基本知識(shí)和原理,以便更好地理解后面的內(nèi)容。
什么是向量
向量是數(shù)學(xué)、物理學(xué)和工程科學(xué)等多個(gè)自然科學(xué)中的基本概念,它是一個(gè)具有方向和長(zhǎng)度的量,用于描述問題,如空間幾何、力學(xué)、信號(hào)處理等。在計(jì)算機(jī)科學(xué)中,向量被用于表示數(shù)據(jù),如文本、圖像或音頻。此外,向量還代表AI模型對(duì)文本、圖像、音頻、視頻等非結(jié)構(gòu)化數(shù)據(jù)的印象。
向量相似度搜索的基本原理
向量相似度搜索的基本原理是通過將數(shù)據(jù)集中的每個(gè)元素映射為向量,并使用特定相似度計(jì)算算法,如基于余弦相似度的、基于歐氏相似度或基于Jaccard相似度等算法,找到與查詢向量最相似的向量。
Redis實(shí)現(xiàn)向量相似度搜索
了解原理后,我們開始來(lái)實(shí)現(xiàn)如何使用Redis實(shí)現(xiàn)向量相似度搜索。Redis允許我們?cè)贔T.SEARCH命令中使用向量相似度查詢。使我們可以加載、索引和查詢作為Redis哈希或JSON文檔中字段存儲(chǔ)的向量。
相關(guān)文檔地址
https://redis.io/docs/interact/search-and-query/search/vectors
1、Redis Search安裝
關(guān)于Redis Search的安裝和使用,此處不再贅述,如果您對(duì)此不熟悉,可以參考上一篇文章:
C#+Redis Search:如何用Redis實(shí)現(xiàn)高性能全文搜索
2、創(chuàng)建向量索引庫(kù)
這里我們使用NRedisStack和StackExchange.Redis兩個(gè)庫(kù)來(lái)與Redis進(jìn)行交互操作。
//創(chuàng)建一個(gè)Redis連接 static ConnectionMultiplexer mux = ConnectionMultiplexer.Connect("localhost"); //獲取一個(gè)Redis數(shù)據(jù)庫(kù) static IDatabase db = mux.GetDatabase(); //創(chuàng)建一個(gè)RediSearch客戶端 static SearchCommands ft = new SearchCommands(db, null);
在進(jìn)行向量搜索之前,首先需要定義并創(chuàng)建索引,并指定相似性算法。
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" })); }
這段代碼的意思是:
- 使用了一個(gè)異步方法 ft.CreateAsync 來(lái)創(chuàng)建索引。它接受三個(gè)參數(shù):索引名稱 indexName,一個(gè) FTCreateParams 對(duì)象和一個(gè) Schema 對(duì)象;
- FTCreateParams 類提供了一些參數(shù)選項(xiàng),用于指定索引的參數(shù)。這里使用 .On(IndexDataType.HASH) 方法來(lái)指定索引數(shù)據(jù)類型為哈希,并使用 .Prefix(prefix) 方法來(lái)指定索引數(shù)據(jù)的前綴;
- Schema 類用于定義索引中的字段和字段類型。這里定義了一個(gè)標(biāo)簽字段(tag field)用于區(qū)分過慮數(shù)據(jù)。定義了一個(gè)文本字段(text field)用于存儲(chǔ)原始數(shù)據(jù),以及一個(gè)向量字段(vector field)用于存儲(chǔ)經(jīng)原始數(shù)據(jù)轉(zhuǎn)化后的向量數(shù)據(jù);
- 使用了 VectorField.VectorAlgo.HNSW 來(lái)指定向量算法為 HNSW(Hierarchical Navigable Small World)。還傳遞了一個(gè)字典對(duì)象,用于設(shè)置向量字段的參數(shù)。其中,鍵為字符串類型,值為對(duì)象類型。
目前Redis支持兩種相似度算法:
HNSW分層導(dǎo)航小世界算法,使用小世界網(wǎng)絡(luò)構(gòu)建索引,具有快速查詢速度和小內(nèi)存占用,時(shí)間復(fù)雜度為O(logn),適用于大規(guī)模索引。
FLAT暴力算法,它對(duì)所有的鍵值對(duì)進(jìn)行掃描,然后根據(jù)鍵值對(duì)的距離計(jì)算出最短路徑,時(shí)間復(fù)雜度為O(n),其中n是鍵值對(duì)的數(shù)量。這種算法時(shí)間復(fù)雜度非常高,只適用于小規(guī)模的索引。
3、添加向量到索引庫(kù)
索引創(chuàng)建后,我們將數(shù)據(jù)添加到索引中。
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方法用于將一個(gè)具有指定文檔ID、前綴、標(biāo)簽、內(nèi)容及內(nèi)容的向量存儲(chǔ)到索引庫(kù)中。并使用SelectMany()方法和BitConverter.GetBytes()方法將向量轉(zhuǎn)換為一個(gè)字節(jié)數(shù)組。
4、向量搜索
Redis 支持兩種類型的向量查詢:KNN查詢和Range查詢,也可以將兩種查詢混合使用。
KNN 查詢
KNN 查詢用于在給定查詢向量的情況下查找前 N 個(gè)最相似的向量。
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"])); } }
這段代碼的意思是:
創(chuàng)建一個(gè)查詢對(duì)象 query,并設(shè)置查詢條件。查詢條件包括:
- "*=>[KNN {limit} @vector $vector AS score]":使用KNN算法進(jìn)行向量相似度搜索,限制結(jié)果數(shù)量為limit,使用給定的向量vector作為查詢向量,將查詢結(jié)果按照相似度得分進(jìn)行排序;
- AddParam("vector", vector.SelectMany(BitConverter.GetBytes).ToArray()):將浮點(diǎn)數(shù)數(shù)組轉(zhuǎn)換為字節(jié)數(shù)組,并將其作為查詢參數(shù)傳遞給查詢;
- SetSortBy("score"):按照相似度得分對(duì)結(jié)果進(jìn)行排序;
- ReturnFields("content", "score"):將content和score兩個(gè)字段從結(jié)果集中返回;
- Limit(0, limit):限制結(jié)果集的起始位置為0,結(jié)果數(shù)量為limit;
- Dialect(2):設(shè)置查詢方言為2,即Redis默認(rèn)的查詢語(yǔ)言Redis Protocol;
調(diào)用異步搜索方法 ft.SearchAsync(indexName, query),并等待搜索結(jié)果;
遍歷搜索結(jié)果集 result.Documents,將每個(gè)文檔轉(zhuǎn)換為 (string Content, double Score) 元組,并通過 yield 語(yǔ)句進(jìn)行迭代返回。
Range 查詢
Range查詢提供了一種根據(jù) Redis 中的向量字段與基于某些預(yù)定義閾值(半徑)的查詢向量之間的距離來(lái)過濾結(jié)果的方法。類似于 NUMERIC 和 GEO 子句,可以在查詢中多次出現(xiàn),特別是可以和 KNN 進(jìn)行混合搜索。
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參數(shù),將限制結(jié)果僅包含給定標(biāo)簽的內(nèi)容。這樣做可以增加查詢的準(zhǔn)確性,提高查詢效率。
5、從索引庫(kù)中刪除向量
public async Task DeleteAsync(string docId, string prefix) { await db.KeyDeleteAsync($"{prefix}{docId}"); }
這個(gè)方法通過刪除與指定向量相關(guān)聯(lián)的哈希緩存鍵,來(lái)實(shí)現(xiàn)從索引庫(kù)中刪除指定向量數(shù)據(jù)。
6、刪除向量索引庫(kù)
public async Task DropIndexAsync() { await ft.DropIndexAsync(indexName, true); }
這個(gè)方法 await ft.DropIndexAsync接受兩個(gè)參數(shù): indexName 和 true 。indexName 表示索引庫(kù)的名稱, true 表示在刪除索引時(shí)是否刪除索引文件。
7、查詢索引庫(kù)信息
public async Task<InfoResult> InfoAsync() { return await ft.InfoAsync(indexName); }
通過 await ft.InfoAsync(indexName) 方法,我們可以獲取到指定索引庫(kù)的大小,文檔數(shù)量等相關(guān)索引庫(kù)信息。
完整 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 { //創(chuàng)建一個(gè)Redis連接 static ConnectionMultiplexer mux = ConnectionMultiplexer.Connect("localhost"); //獲取一個(gè)Redis數(shù)據(jù)庫(kù) static IDatabase db = mux.GetDatabase(); //創(chuàng)建一個(gè)RediSearch客戶端 static SearchCommands ft = new SearchCommands(db, null); //索引名稱 static string indexName = "test:index"; //索引前綴 static string prefix = "test:data"; static async Task Main(string[] args) { //創(chuàng)建一個(gè)向量的索引 await CreateIndexAsync(); //添加一些向量到索引中 await SetAsync("1", "A", "測(cè)試數(shù)據(jù)A1", new float[] { 0.1f, 0.2f }); await SetAsync("2", "A", "測(cè)試數(shù)據(jù)A2", new float[] { 0.3f, 0.4f }); await SetAsync("3", "B", "測(cè)試數(shù)據(jù)B1", new float[] { 0.5f, 0.6f }); await SetAsync("4", "C", "測(cè)試數(shù)據(jù)C1", new float[] { 0.7f, 0.8f }); //刪除一個(gè)向量 await DeleteAsync("4"); //KUN搜索 await foreach (var (Content, Score) in SearchAsync(new float[] { 0.1f, 0.2f }, 2)) { Console.WriteLine($"內(nèi)容:{Content},相似度得分:{Score}"); } //混合 await foreach (var (Tag, Content, Score) in SearchAsync("A", new float[] { 0.1f, 0.2f }, 2)) { Console.WriteLine($"標(biāo)簽:{Tag},內(nèi)容:{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"])); } } } }
到此這篇關(guān)于使用Redis實(shí)現(xiàn)向量相似度搜索的文章就介紹到這了,更多相關(guān)Redis向量相似度內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
基于Redis6.2.6版本部署Redis?Cluster集群的問題
這篇文章主要介紹了基于Redis6.2.6版本部署Redis?Cluster集群,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-04-04Redis?中ZSET數(shù)據(jù)類型命令使用及對(duì)應(yīng)場(chǎng)景總結(jié)(案例詳解)
這篇文章主要介紹了Redis?中ZSET數(shù)據(jù)類型命令使用及對(duì)應(yīng)場(chǎng)景總結(jié),本文通過示例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2023-01-01RabbitMQ+redis+Redisson分布式鎖+seata實(shí)現(xiàn)訂單服務(wù)的流程分析
訂單服務(wù)涉及許多方面,分布式事務(wù),分布式鎖,例如訂單超時(shí)未支付要取消訂單,訂單如何防止重復(fù)提交,如何防止超賣、這里都會(huì)使用到,這篇文章主要介紹了RabbitMQ+redis+Redisson分布式鎖+seata實(shí)現(xiàn)訂單服務(wù)的流程分析,需要的朋友可以參考下2024-07-07Redis教程(二):String數(shù)據(jù)類型
這篇文章主要介紹了Redis教程(二):String數(shù)據(jù)類型,本文講解了String數(shù)據(jù)類型概述、相關(guān)命令列表、命令使用示例三部分內(nèi)容,需要的朋友可以參考下2015-04-04Redis實(shí)現(xiàn)附近商鋪的項(xiàng)目實(shí)戰(zhàn)
本文主要介紹了Redis實(shí)現(xiàn)附近商鋪的項(xiàng)目實(shí)戰(zhàn),文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2023-01-01