基于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。

相关推荐
jack_xu39 分钟前
高频面试题:如何保证数据库和es数据一致性
后端·mysql·elasticsearch
技术项目引流1 小时前
elasticsearch查询中的特殊字符影响分析
大数据·elasticsearch·搜索引擎
EasyDSS1 小时前
视频监控从安装到优化的技术指南,视频汇聚系统EasyCVR智能安防系统构建之道
大数据·网络·网络协议·音视频
lilye661 小时前
精益数据分析(20/126):解析经典数据分析框架,助力创业增长
大数据·人工智能·数据分析
苏小夕夕2 小时前
spark-streaming(二)
大数据·spark·kafka
珈和info2 小时前
珈和科技助力“农险提效200%”!“遥感+”技术创新融合省级示范项目荣登《湖北卫视》!
大数据·科技·无人机·智慧农业
盈达科技2 小时前
盈达科技:登顶GEO优化全球制高点,以AICC定义AI时代内容智能优化新标杆
大数据·人工智能
电商数据girl3 小时前
产品经理对于电商接口的梳理||电商接口文档梳理与接入
大数据·数据库·python·自动化·产品经理
敖云岚4 小时前
【AI】SpringAI 第五弹:接入千帆大模型
java·大数据·人工智能·spring boot·后端
宅小海4 小时前
spark和Hadoop的区别和联系
大数据·hadoop·spark