基于Embedding实现超越ES的高级搜索

目前,很多企业会利用大模型来做企业内部知识库系统,比如智能客服系统,做法是把企业内部的整理的产品手册、常见问题手册,做成智能知识库系统,客户可以直接向知识库系统用自然语言提问,知识库系统能理解客户的问题并基于内部知识给出客户想要的答案,此时就会用到文本向量化。

什么是向量

一个二维向量可以理解为平面坐标轴中的一个坐标点(x,y),在编程领域,一个二维向量就是一个大小为二的float类型的数组。

文本向量化

所谓文本向量化是指,利用大模型可以把一个字、一个词或一段话映射为一个多维向量,比如我们可以直接在LangChain4j中来调用向量模型来对一句话进行向量化体验:

java 复制代码
package com.timi;

import dev.langchain4j.data.embedding.Embedding;
import dev.langchain4j.data.message.AiMessage;
import dev.langchain4j.data.message.UserMessage;
import dev.langchain4j.model.chat.ChatLanguageModel;
import dev.langchain4j.model.openai.OpenAiChatModel;
import dev.langchain4j.model.openai.OpenAiEmbeddingModel;
import dev.langchain4j.model.output.Response;

public class _05_Vector {

    public static void main(String[] args) {

        OpenAiEmbeddingModel embeddingModel = OpenAiEmbeddingModel.builder()
                .baseUrl("http://langchain4j.dev/demo/openai/v1")
                .apiKey("demo")
                .build();

        Response<Embedding> embed = embeddingModel.embed("你好,我叫司马懿");
        System.out.println(embed.content().toString());
		System.out.println(embed.content().vector().length);
        
    }
}

代码执行结果为:

java 复制代码
Embedding { vector = [-0.00991016, -0.009099855, ...] }
1536

从结果可以知道"你好,我叫司马懿"这句话经过OpenAiEmbeddingModel向量化之后得到的一个长度为1536的float数组。注意,1536是固定的,不会随着句子长度而变化。

那么,我们通过这种向量模型得到一句话对应的向量有什么作用呢?非常有用,因为我们可以基于向量来判断两句话之间的相似度。

向量相似度

前面提到,向量相当于就是坐标点,如果两个坐标点靠得近,那么就表示这两个向量相似,所以,如果两句话对应的向量相似,那么就表示这两句话语义比较相似,当然这中间最关键的就是向量模型,因为向量是它生成的,向量模型也是经过大量机器学习训练之后产生的,向量模型效果越好,就表示它对于自然语言理解的程度越好,同时也就表示它生成出来的向量越准备,越能反映出语义的相似度。

比如可以通过计算两个坐标的余弦相似度(代码是ChatGPT生成的):

java 复制代码
import java.util.*;

public class CosineSimilarity {
    
    // 计算两个向量的点积
    public static double dotProduct(double[] vectorA, double[] vectorB) {
        double dotProduct = 0;
        for (int i = 0; i < vectorA.length; i++) {
            dotProduct += vectorA[i] * vectorB[i];
        }
        return dotProduct;
    }
    
    // 计算向量的模
    public static double vectorMagnitude(double[] vector) {
        double magnitude = 0;
        for (double component : vector) {
            magnitude += Math.pow(component, 2);
        }
        return Math.sqrt(magnitude);
    }
    
    // 计算余弦相似度
    public static double cosineSimilarity(double[] vectorA, double[] vectorB) {
        double dotProduct = dotProduct(vectorA, vectorB);
        double magnitudeA = vectorMagnitude(vectorA);
        double magnitudeB = vectorMagnitude(vectorB);
        
        if (magnitudeA == 0 || magnitudeB == 0) {
            return 0; // 避免除以零
        } else {
            return dotProduct / (magnitudeA * magnitudeB);
        }
    }
    
    public static void main(String[] args) {
        // 示例向量
        double[] vectorA = {1, 2, 3};
        double[] vectorB = {3, 2, 1};
        
        // 计算余弦相似度
        double similarity = cosineSimilarity(vectorA, vectorB);
        System.out.println("Cosine Similarity: " + similarity);
    }
}

向量数据库

对于向量模型生成出来的向量,我们可以持久化到向量数据库,并且能利用向量数据库来计算两个向量之间的相似度,或者根据一个向量查找跟这个向量最相似的向量。

在LangChain4j中,EmbeddingStore表示向量数据库,它有20个实现类:

  1. AstraDbEmbeddingStore
  2. AzureAiSearchEmbeddingStore
  3. CassandraEmbeddingStore
  4. ChromaEmbeddingStore
  5. ElasticsearchEmbeddingStore
  6. InMemoryEmbeddingStore
  7. InfinispanEmbeddingStore
  8. MemoryIdEmbeddingStore
  9. MilvusEmbeddingStore
  10. MinimalEmbeddingStore
  11. MongoDbEmbeddingStore
  12. Neo4jEmbeddingStore
  13. OpenSearchEmbeddingStore
  14. PgVectorEmbeddingStore
  15. PineconeEmbeddingStore
  16. QdrantEmbeddingStore
  17. RedisEmbeddingStore
  18. VearchEmbeddingStore
  19. VespaEmbeddingStore
  20. WeaviateEmbeddingStore

其中有我们熟悉的几个数据库都可以用来存储向量,比如Elasticsearch、MongoDb、Neo4j、Pg、Redis。

那么我们使用Redis来演示对于向量的增删查改,首先添加依赖:

xml 复制代码
<dependency>
	<groupId>dev.langchain4j</groupId>
	<artifactId>langchain4j-redis</artifactId>
	<version>${langchain4j.version}</version>
</dependency>

然后需要注意的是,普通的Redis是不支持向量存储和查询的,需要额外的redisearch模块,我这边是直接使用docker来运行一个带有redisearch模块的redis容器的,命令为:

java 复制代码
docker run -p 6379:6379 redis/redis-stack-server:latest

注意端口6379不要和你现有的Redis冲突了。

然后就可以使用以下代码把向量存到redis中了:

java 复制代码
RedisEmbeddingStore embeddingStore = RedisEmbeddingStore.builder()
    .host("127.0.0.1")
    .port(6379)
    .dimension(1536)
    .build();

// 生成向量
Response<Embedding> embed = embeddingModel.embed("我是司马懿");

// 存储向量
embeddingStore.add(embed.content());

dimension表示要存储的向量的维度,所以为1536,如果你不是使用OpenAiEmbeddingModel得到的向量,那么维度可能会不一样。

可以使用以下命令来清空:

java 复制代码
redis-cli FT.DROPINDEX embedding-index DD

匹配向量

在上面我们存储了"我是司马懿"的向量到Redis,接下来我们来使用"我的名字叫司马懿"来查找,看能不能查找到"我是司马懿",最好新建一个类,新增和查询分为两个类比较方便:

java 复制代码
package com.timi;

import dev.langchain4j.data.embedding.Embedding;
import dev.langchain4j.data.segment.TextSegment;
import dev.langchain4j.model.openai.OpenAiEmbeddingModel;
import dev.langchain4j.model.output.Response;
import dev.langchain4j.store.embedding.EmbeddingMatch;
import dev.langchain4j.store.embedding.redis.RedisEmbeddingStore;

import java.util.List;

public class _05_Vector_Search {

    public static void main(String[] args) {

        OpenAiEmbeddingModel embeddingModel = OpenAiEmbeddingModel.builder()
                .baseUrl("http://langchain4j.dev/demo/openai/v1")
                .apiKey("demo")
                .build();

        RedisEmbeddingStore embeddingStore = RedisEmbeddingStore.builder()
                .host("127.0.0.1")
                .port(6379)
                .dimension(1536)
                .build();

        // 生成向量
        Response<Embedding> embed = embeddingModel.embed("我的名字叫司马懿");

        List<EmbeddingMatch<TextSegment>> result = embeddingStore.findRelevant(embed.content(), 4);
        for (EmbeddingMatch<TextSegment> embeddingMatch : result) {
            System.out.println(embeddingMatch.score());
        }

    }
}

代码执行结果为:

java 复制代码
0.9765566289425

这就是"我是司马懿"和"我的名字叫司马懿"两句话之间的语义相似度分数,假如我们换成"今天天气很好",得到的分数为

java 复制代码
0.8937962949275

看上去似乎分数差别不大,但是要注意小数位其实是很多的,也就是这个分数精度是比较高的,所以两个分数还是有一定距离的。

我们不妨再来演示一种场景:

java 复制代码
// 生成向量
TextSegment textSegment1 = TextSegment.textSegment("客服电话是400-8558558");
TextSegment textSegment2 = TextSegment.textSegment("客服工作时间是周一到周五");
TextSegment textSegment3 = TextSegment.textSegment("客服投诉电话是400-8668668");
Response<Embedding> embed1 = embeddingModel.embed(textSegment1);
Response<Embedding> embed2 = embeddingModel.embed(textSegment2);
Response<Embedding> embed3 = embeddingModel.embed(textSegment3);

// 存储向量
embeddingStore.add(embed1.content(), textSegment1);
embeddingStore.add(embed2.content(), textSegment2);
embeddingStore.add(embed3.content(), textSegment3);

我往向量数据库中添加了三条客户相关的知识,并且通过TextSegment把原始文本也存入了Redis,相当于Redis中现在存储了三条原始文本以及对应的向量,然后我们来查询:

java 复制代码
// 生成向量
Response<Embedding> embed = embeddingModel.embed("客服电话多少");

// 查询
List<EmbeddingMatch<TextSegment>> result = embeddingStore.findRelevant(embed.content(), 5);
for (EmbeddingMatch<TextSegment> embeddingMatch : result) {
    System.out.println(embeddingMatch.embedded().text() + ",分数为:" + embeddingMatch.score());
}

代码执行结果为:

java 复制代码
客服电话是400-8558558,分数为:0.9529553055763
客服投诉电话是400-8668668,分数为:0.9520588517189
客服工作时间是周一到周五,分数为:0.9305278658864999

从这就更容易看出向量的好处,能够基于向量快速的得到和文本相似的文本,这样就能非常适合用来做RAG,也就是检索增强生成。

本节总结

本节我们学习了什么是文本向量化、向量数据库、以及文本相似度等概念,下一节我们将通过完成一个智能客服系统来掌握向量等技术在RAG中的使用,以及掌握到底什么是RAG。

相关推荐
15年网络推广青哥1 分钟前
国际抖音TikTok矩阵运营的关键要素有哪些?
大数据·人工智能·矩阵
节点。csn1 小时前
Hadoop yarn安装
大数据·hadoop·分布式
arnold661 小时前
探索 ElasticSearch:性能优化之道
大数据·elasticsearch·性能优化
NiNg_1_2342 小时前
基于Hadoop的数据清洗
大数据·hadoop·分布式
成长的小牛2333 小时前
es使用knn向量检索中numCandidates和k应该如何配比更合适
大数据·elasticsearch·搜索引擎
goTsHgo3 小时前
在 Spark 上实现 Graph Embedding
大数据·spark·embedding
程序猿小柒3 小时前
【Spark】Spark SQL执行计划-精简版
大数据·sql·spark
隔着天花板看星星3 小时前
Spark-Streaming集成Kafka
大数据·分布式·中间件·spark·kafka
奥顺3 小时前
PHPUnit使用指南:编写高效的单元测试
大数据·mysql·开源·php
小屁孩大帅-杨一凡4 小时前
Flink 简介和简单的demo
大数据·flink