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