六、ChatGPT Embeddings与Redis强强结合实现文本相似度分析与搜索

要实现文本相似度搜索,我们需要解决两个问题:一是如何计算文本之间的相似度,二是如何快速地从大量文本中找出最相似的文本。在本文中,我们来探索如何利用ChatGTP Embeddings功能,将文本转换为向量,并存储到Redis中,实现向量相似度搜索。还请给个小关注👇

什么是ChatGPT的Embeddings?

ChatGPT是一种基于深度学习的自然语言处理模型,它可以生成流畅、有逻辑、有情感和有创意的对话文本。ChatGPT使用了一种称为BERT的预训练模型来生成Embeddings。

Embeddings是一种将文本转换为数值向量的技术,它可以让计算机更好地理解和处理自然语言。Embeddings可以将每个单词或者每个句子映射到一个高维空间中的一个点,这个点的坐标就是该单词或句子的向量。

Embeddings可以保留文本中的语义、语法和情感信息,使得具有相似含义或相似用法的单词或句子在空间中距离较近,而具有不同含义或不同用法的单词或句子在空间中距离较远,从而生成更加丰富和准确的向量。

如何使用ChatGPT的Embeddings?

2f4c69ebe3550ffbe40f0c9caa4ee462.png要使用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中。

我们看一下效果:

e7d3e662a18e15ec0196b1b0a1c153c6.png

从上图可以看出,得分越小,相似度越高,“我爱吃苹果”与“我喜欢吃苹果”在生活中是同一个意思,这种搜索方式保留了文本中的语义信息,因此可以将它应于很多场景中,如问答系统、推荐系统等。

//源码地址
https://github.com/ynanech/ChatGPT.Demo

👇感谢阅读,点赞+分享+收藏+关注👇d841513a0dfba8080f7edee5d2148702.png

文章出自猿惑豁微信公众号

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值