要实现文本相似度搜索,我们需要解决两个问题:一是如何计算文本之间的相似度,二是如何快速地从大量文本中找出最相似的文本。在本文中,我们来探索如何利用ChatGTP Embeddings功能,将文本转换为向量,并存储到Redis中,实现向量相似度搜索。还请给个小关注👇
什么是ChatGPT的Embeddings?
ChatGPT是一种基于深度学习的自然语言处理模型,它可以生成流畅、有逻辑、有情感和有创意的对话文本。ChatGPT使用了一种称为BERT的预训练模型来生成Embeddings。
Embeddings是一种将文本转换为数值向量的技术,它可以让计算机更好地理解和处理自然语言。Embeddings可以将每个单词或者每个句子映射到一个高维空间中的一个点,这个点的坐标就是该单词或句子的向量。
Embeddings可以保留文本中的语义、语法和情感信息,使得具有相似含义或相似用法的单词或句子在空间中距离较近,而具有不同含义或不同用法的单词或句子在空间中距离较远,从而生成更加丰富和准确的向量。
如何使用ChatGPT的Embeddings?
要使用ChatGTP Embeddings功能,需要调用 v1/embeddings 接口,它有两个重要参数:
model是一个必填参数,表示要使用的模型的ID,
input是一个必填的字符串或数组参数,表示输入要嵌入的文本,即待提取向量的原始数据。
//文档地址
https://platform.openai.com/docs/api-reference/embeddings/create
//官网示例
https://github.com/openai/openai-cookbook/
例如:
//Request:
curl https://api.openai.com/v1/embeddings \
-H "Authorization: Bearer $OPENAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"input": "The food was delicious and the waiter...",
"model": "text-embedding-ada-002"
}'
//Response
{
"object": "list",
"data": [
{
"object": "embedding",
"embedding": [
0.0023064255,
-0.009327292,
.... (1536 floats total for ada-002)
-0.0028842222,
],
"index": 0
}
],
"model": "text-embedding-ada-002",
"usage": {
"prompt_tokens": 8,
"total_tokens": 8
}
}
在理解ChatGTP Embeddings功能后,进入我们今天的主题,我将创建一个新的项目ChatGPT.Demo6,代码和ChatGPT.Demo5相同,但已经将Betalgo.OpenAI库更新到了7.1.3版本。
Redis向量存储与搜索
一、创建RedisService服务
此服务的作用就是创建全局唯一的Redis连接对象,发挥StackExchange.Redis库的优势。它使用高性能多路复用技术,允许高效使用来自多个调用线程的共享连接,从而避免了频繁的连接和断开连接的操作,提高了数据库的访问性能,同时支持异步编程,提高了数据库操作的性能和效率。
在前面文章《四、ChatGPT多KEY动态轮询,自动删除无效KEY》中,我们已使用过Redis,今天我们还将继续使用Redis,为避免重复创建连接对象,我们全局创建一个单一的Redis连接对象,并封装在RedisService服务中。
这样,我们就可以在需要时快速访问Redis,并实现多路复用连接,从而提高性能和效率。也有助于简化代码结构,提高代码的可维护性和可重用性。
1、创建IRedisService接口
在Extensions文件夹中创建一个名为IRedisService.cs的接口文件,并定义了一个获取操作缓存数据库对象的方法,代码如下:
public interface IRedisService
{
Task<IDatabase> GetDatabaseAsync();
}
2、创建RedisService服务
在Extensions文件夹中创建一个名为RedisService.cs的服务文件,并在文件中写入以下代码:
public class RedisService : IDisposable, IRedisService
{
private volatile ConnectionMultiplexer _connection;
private IDatabase _cache;
private readonly string _configuration;
private readonly SemaphoreSlim _connectionLock = new SemaphoreSlim(initialCount: 1, maxCount: 1);
public RedisService(string configuration)
{
_configuration = configuration;
}
public async Task<IDatabase> GetDatabaseAsync()
{
await ConnectAsync();
return _cache;
}
private async Task ConnectAsync(CancellationToken token = default)
{
if (_cache != null) return;
await _connectionLock.WaitAsync(token);
try
{
if (_cache == null)
{
token.ThrowIfCancellationRequested();
_connection = await ConnectionMultiplexer.ConnectAsync(_configuration);
_cache = _connection.GetDatabase();
}
}
finally
{
_connectionLock.Release();
}
}
public void Dispose() => _connection?.Close();
}
RedisService类实现了IDisposable接口和IRedisService接口,以下是该服务的功能解释:
声明了一个私有变量_connection,它是一个ConnectionMultiplexer类型的对象,用于与Redis服务器建立连接;
声明了一个私有变量_cache,它是一个IDatabase类型的对象,用于缓存数据库操作;
声明了一个私有变量_configuration,用于设置缓存的地址、密码、端口等;
在Connectasync方法内部,使用了 SemaphoreSlim(信号量)来保证线程安全,然后检查_cache是否为null,如果为null,则等待连接建立完成,然后获取数据库操作对象_cache;
Dispose方法用于释放_connection对象。
3、RedisService服务注册
在Program.cs文件中,将RedisService服务进行注册,并使用单例模式:
//注册RedisService服务
builder.Services.AddSingleton<IRedisService>(new RedisService("localhost"));
二、创建RedisVectorSearchService服务
此服务的作用就是调用Redis实现向量的存储与检索。
1、创建IRedisVectorSearchService接口
在Extensions文件夹中创建一个名为IRedisVectorSearchService.cs的接口文件,并在文件中写入以下代码:
public interface IRedisVectorSearchService
{
//创建索引
Task CreateIndexAsync(string indexName, string indexPrefix, int vectorSize);
//删除索引
Task DropIndexAsync(string indexName);
//查看索引信息
Task<InfoResult> InfoAsync(string indexName);
//向量搜索
IAsyncEnumerable<(string Content, double Score)> SearchAsync(string indexName, float[] vector, int limit);
//删除索引数据
Task DeleteAsync(string indexPrefix, string docId);
//添加或修改索引数据
Task SetAsync(string indexPrefix, string docId, string content, float[] vector);
}
2、创建RedisVectorSearchService服务
在Extensions文件夹中创建一个名为RedisVectorSearchService.cs的服务文件,并在文件中写入以下代码:
public class RedisVectorSearchService : IRedisVectorSearchService
{
private readonly IRedisService _redisService;
private SearchCommands _searchCommands;
public RedisVectorSearchService(IRedisService redisService)
{
_redisService = redisService;
}
//获取Redis向量搜索对象
private async Task<SearchCommands> GetSearchCommandsAsync()
{
if (_searchCommands != null) return _searchCommands;
var db = await _redisService.GetDatabaseAsync();
_searchCommands = new SearchCommands(db, null);
return _searchCommands;
}
public async Task CreateIndexAsync(string indexName, string indexPrefix, int vectorSize)
{
var ft = await GetSearchCommandsAsync();
await ft.CreateAsync(indexName,
new FTCreateParams()
.On(IndexDataType.HASH)
.Prefix(indexPrefix),
new Schema()
.AddTextField("content")
.AddVectorField("vector",
VectorField.VectorAlgo.HNSW,
new Dictionary<string, object>()
{
["TYPE"] = "FLOAT32",
["DIM"] = vectorSize,
["DISTANCE_METRIC"] = "COSINE"
}));
}
public async Task SetAsync(string indexPrefix, string docId, string content, float[] vector)
{
var db = await _redisService.GetDatabaseAsync();
await db.HashSetAsync($"{indexPrefix}{docId}", new HashEntry[] {
new HashEntry ("content", content),
new HashEntry ("vector", vector.SelectMany(BitConverter.GetBytes).ToArray())
});
}
public async Task DeleteAsync(string indexPrefix, string docId)
{
var db = await _redisService.GetDatabaseAsync();
await db.KeyDeleteAsync($"{indexPrefix}{docId}");
}
public async Task DropIndexAsync(string indexName)
{
var ft = await GetSearchCommandsAsync();
await ft.DropIndexAsync(indexName, true);
}
public async Task<InfoResult> InfoAsync(string indexName)
{
var ft = await GetSearchCommandsAsync();
return await ft.InfoAsync(indexName);
}
public async IAsyncEnumerable<(string Content, double Score)> SearchAsync(string indexName, float[] vector, int limit)
{
var query = new Query($"*=>[KNN {limit} @vector $vector AS score]");
query = query.AddParam("vector", vector.SelectMany(BitConverter.GetBytes).ToArray())
.SetSortBy("score")
.ReturnFields("content", "score")
.Limit(0, limit)
.Dialect(2);
var ft = await GetSearchCommandsAsync();
var result = await ft.SearchAsync(indexName, query).ConfigureAwait(false);
foreach (var document in result.Documents)
{
yield return (document["content"], Convert.ToDouble(document["score"]));
}
}
}
RedisVectorSearchService类实现了IRedisVectorSearchService接口定义的方法,它通过构造函数注入IRedisService服务来与Redis进行交互,并在内部使用它来获取Redis连接和执行向量的相关操作。
之前已有两篇文章介绍过Redis向量相似度的搜索,这里不在过多讲解,如有不懂还请进行回看:
C#+Redis Search:如何用Redis实现高性能全文搜索
利用Redis实现向量相似度搜索:解决文本、图像和音频之间的相似度匹配问题
3、RedisVectorSearchService服务注册
在Program.cs文件中,将RedisVectorSearchService服务进行注册,并使用单例模式:
//注册Redis向量搜索服务
builder.Services.AddSingleton<IRedisVectorSearchService, RedisVectorSearchService>();
ChatGTP Embeddings文本向量提取
右键Controllers文件夹新建一个SearchController.cs文件,用于调用ChatGTP Embeddings服务和RedisVectorSearchService服务,实现向量提取、存储和搜索功能。
1、引入命名空间
using OpenAI.Interfaces;
using OpenAI.ObjectModels.RequestModels;
using StackExchange.Redis;
2、注入服务
private readonly IOpenAIService _openAIService;
private readonly IRedisVectorSearchService _redisVectorSearchService;
//定义索引库名字
private readonly string _indexName = "VectorSearchIndex";
//定义索引数据前缀
private readonly string _indexPrefix = "VectorSearchItem";
public SearchController(IOpenAIService openAIService, IRedisVectorSearchService redisVectorSearchService)
{
_openAIService = openAIService;
_redisVectorSearchService = redisVectorSearchService;
}
3、添加一个IndexAsync方法
[HttpGet]
public async Task<IActionResult> IndexAsync(string message, CancellationToken cancellationToken)
{
var embeddingResult = await _openAIService.Embeddings.CreateEmbedding(new EmbeddingCreateRequest()
{
Input = message,
Model = OpenAI.ObjectModels.Models.TextSearchAdaDocV1
}, cancellationToken);
if (!embeddingResult.Successful)
{
if (embeddingResult.Error == null)
throw new Exception("Unknown Error");
return Content($"{embeddingResult.Error.Code}: {embeddingResult.Error.Message}");
}
var embeddingResponse = embeddingResult.Data.FirstOrDefault();
int size = 10;
var searchResponse = _redisVectorSearchService.SearchAsync(
_indexName, embeddingResponse.Embedding.Select(m => Convert.ToSingle(m)).ToArray(), size);
var searchResult = new List<object>(size);
await foreach ((string Content, double Score) in searchResponse)
{
searchResult.Add(new
{
Content,
Score
});
}
return Ok(searchResult);
}
IndexAsync方法用于实现向量提取及相似度搜索功能,它接受message和cancellationToken两个参数,message表示要搜索的文本内容,cancellationToken用于接收取消信号,中断后续操作,整个方法的执行步骤为:
首先,通过调用ChatGPT的v1/embeddings接口,将文本信息转换为向量表示。
CreateEmbedding
方法用于创建嵌入对象,它接受一个EmbeddingCreateRequest
对象作为参数,该对象包含输入文本Input和要使用模型Model;然后,判断嵌入对象的结果,如果结果不成功,就会抛出一个异常或者返回一个包含错误信息的内容,如果嵌入结果成功,就会提取嵌入对象的数据(Double数组);
然后,将嵌入结果转换为浮点型数组(Redis只支持Float类型数组),并传递给SearchAsync方法进行搜索。这个方法返回一个IEnumerable<(string, double)>类型的结果,其中每个元素都是一个包含内容(Content)和得分的(Score)的元组;
最后,使用await foreach遍历搜索结果,并将每个结果转换为一个包含内容和得分的匿名对象,输出到客户端。
4、添加一个InitAsync方法
为了演示,我们需要一个初始化方法,用于创建索引和添加测试数据。代码如下:
public async Task<string> InitAsync()
{
var inputAsList = new List<string> { "我喜欢吃苹果", "我讨厌吃香蕉", "我爱吃瓜", "我喜欢喝茶", "我不爱喝咖啡", "我讨厌喝饮料", "我爱喝酒" };
var embeddingResult = await _openAIService.Embeddings.CreateEmbedding(new EmbeddingCreateRequest()
{
InputAsList = inputAsList,
Model = OpenAI.ObjectModels.Models.TextSearchAdaDocV1
});
if (!embeddingResult.Successful) return $"{embeddingResult.Error.Code}: {embeddingResult.Error.Message}";
try
{
await _redisVectorSearchService.InfoAsync(_indexName).ConfigureAwait(false);
await _redisVectorSearchService.DropIndexAsync(_indexName);
}
catch (RedisServerException ex) when (ex.Message == "Unknown Index name")
{
//索引不存在
}
await _redisVectorSearchService.CreateIndexAsync(_indexName, _indexPrefix, 1024);
int i = 0;
foreach (var item in inputAsList)
{
await _redisVectorSearchService.SetAsync(_indexPrefix, i.ToString(), item, embeddingResult.Data[i].Embedding.Select(m => Convert.ToSingle(m)).ToArray());
++i;
}
return "初始化成功";
}
InitAsync方法的执行步骤为:
首先,通过调用CreateEmbedding方法批量提取文本集的向量结果;
然后,调用InfoAsync方法读取索引信息,如果读取成功,说明索引已创建,并进行删除,如果读取发生指定异常,说明索引不存在;
然后,调用CreateIndexAsync方法创建索引;
最后,调用SetAsync方法将嵌入对象的结果保存到Redis中。
我们看一下效果:
从上图可以看出,得分越小,相似度越高,“我爱吃苹果”与“我喜欢吃苹果”在生活中是同一个意思,这种搜索方式保留了文本中的语义信息,因此可以将它应于很多场景中,如问答系统、推荐系统等。
//源码地址
https://github.com/ynanech/ChatGPT.Demo
👇感谢阅读,点赞+分享+收藏+关注👇
文章出自猿惑豁微信公众号