Spring AI(5)——通过嵌入模型进行数据的向量化处理

发布于:2025-05-16 ⋅ 阅读:(15) ⋅ 点赞:(0)

嵌入是文本、图像或视频的数值表示,用于捕获输入之间的关系。

嵌入的工作原理是将文本、图像和视频转换为浮点数数组,称为向量。这些向量旨在捕获文本、图像和视频的含义。嵌入数组的长度称为向量的维度。

通过计算两个文本向量表示之间的数值距离,应用程序可以确定用于生成嵌入向量的对象之间的相似性。

EmbeddingModel 接口旨在轻松集成 AI 和机器学习中的嵌入模型。其主要功能是将文本转换为数值向量,通常称为嵌入。这些嵌入对于各种任务至关重要,例如语义分析和文本分类。

Spring AI支持的嵌入模型:

注意:本例使用通过Ollama安装的本地嵌入模型进行测试

通过Ollama安装嵌入模型

关于Ollama的安装,参考之前的博客DeepSeek本地化部署-CSDN博客

本例安装的嵌入模型是dmeta-embedding-zh,安装命令如下:

ollama pull shaw/dmeta-embedding-zh

嵌入模型的使用

导入jar

<dependency>
   <groupId>org.springframework.ai</groupId>
   <artifactId>spring-ai-starter-model-ollama</artifactId>
</dependency>

yml中增加嵌入模型的配置

spring:
  ai:
    ollama:
      # ollama的api路径
      base-url: http://localhost:11434
      embedding:
        options:
          # 嵌入模型名称
          model: shaw/dmeta-embedding-zh:latest
    model:
      embedding: ollama

嵌入模型支持的方法


    EmbeddingResponse call(EmbeddingRequest request);

    float[] embed(String text) {
    
    float[] embed(Document document);

    List<float[]> embed(List<String> texts)

    List<float[]> embed(List<Document> documents, EmbeddingOptions options, 

    EmbeddingResponse embedForResponse(List<String> texts)


 对字符串数据进行嵌入处理

@RestController
@RequestMapping("/embeding")
public class EmbedingController {

    @Resource
    private EmbeddingModel embeddingModel;

    @GetMapping("/embed")
    public String embed() {
        // 返回响应对象
        EmbeddingResponse embeddingResponse = embeddingModel.embedForResponse(List.of("今天天气不错"));
        // Map<String, EmbeddingResponse> embedding = Map.of("embedding", embeddingResponse);
        System.out.println(Arrays.toString(embeddingResponse.getResult().getOutput()));

        // 直接返回向量化后的数据
        float[] embed = embeddingModel.embed("挺风和日丽的");
        System.out.println(Arrays.toString(embed));
        return "success";
    }
}

输出:

Document对象进行嵌入处理

DocumentReader

通过DocumentReader实现类对象,可以读取不同来源的文档数据。支持的实现类:

  • JsonReader 处理 JSON 文档,将它们转换为 Document 对象列表
  • TextReader 处理纯文本文档,将它们转换为 Document 对象列表
  • JsoupDocumentReader 使用 JSoup 库处理 HTML 文档,将它们转换为 Document 对象列表。
  • MarkdownDocumentReader 处理 Markdown 文档,将它们转换为 Document 对象列表。
  • PagePdfDocumentReader 使用 Apache PdfBox 库解析 PDF 文档
  • ParagraphPdfDocumentReader 使用 PDF 目录(例如 TOC)信息将输入 PDF 拆分为文本段落,并为每个段落输出一个单独的 Document。注意:并非所有 PDF 文档都包含 PDF 目录
  • TikaDocumentReader 使用 Apache Tika 从各种文档格式(例如 PDF、DOC/DOCX、PPT/PPTX 和 HTML)中提取文本
TextReader读取文本文件数据

支持的构造方法

// 参数是文本文件的url路径
public TextReader(String resourceUrl)
// 参数是资源对象
public TextReader(Resource resource)

注入资源文件

    // 加载指定的资源文件
    @Value("classpath:document/医院.txt")
    private org.springframework.core.io.Resource resource;

文本文件向量化处理

    @GetMapping("/embed2")
    public String embed2() {
        // 读取文本文件
        TextReader textReader = new TextReader(this.resource);
        // 元数据中增加文件名
        textReader.getCustomMetadata().put("filename", "医院.txt");
        // 获取Document对象
        List<Document> docList = textReader.read();
        // 向量化处理
        float[] embed = embeddingModel.embed(docList.get(0));
        // 打印向量化后的数据
        System.out.println(Arrays.toString(embed));
        // 打印Document中的原始文本数据
        System.out.println(docList.get(0).getText());
        return "success";
    }

    DocumentTransformer

    对文档进行批量转换处理

    TokenTextSplitter

    上个例子,我们对文档进行了转换,但是默认一个文档转为一个Document对象,如果文档太长,以后进行检索时,那么聊天上下文占用的token就会很大,为了解决该问题,我们可以对文档进行拆分处理。

    TokenTextSplitter 是 TextSplitter 的一个实现,而TextSpliter继承了DocumentTransformer接口,它使用 CL100K_BASE 编码,根据 token 计数将文本分割成块。

    构造函数:

        public TokenTextSplitter() {
            this(800, 350, 5, 10000, true);
        }
    
        public TokenTextSplitter(boolean keepSeparator) {
            this(800, 350, 5, 10000, keepSeparator);
        }
    
        public TokenTextSplitter(int chunkSize, int minChunkSizeChars, int minChunkLengthToEmbed, int maxNumChunks, boolean keepSeparator) {
        ......
        }
          

    参数说明: 

    • defaultChunkSize: 每个文本块以 token 为单位的目标大小(默认值:800)。
    • minChunkSizeChars: 每个文本块以字符为单位的最小大小(默认值:350)。
    • minChunkLengthToEmbed: 文本块去除空白字符或者处理分隔符后,用于嵌入处理的文本的最小长度(默认值:5)。
    • maxNumChunks: 从文本生成的最大块数(默认值:10000)。
    • keepSeparator: 是否在块中保留分隔符(例如换行符)(默认值:true)。

    TokenTextSplitter拆分文档的逻辑:

    1.使用 CL100K_BASE 编码将输入文本编码为 token

    2.根据 defaultChunkSize 对编码后的token进行分块

    3.对于分块:

            (1)将块再解码为文本字符串

            (2)尝试从后向前找到一个合适的截断点(句号、问号、感叹号或换行符)。

            (3)如果找到合适的截断点,并且截断点所在的index大于minChunkSizeChars,则将在该点截断该块

            (4)对分块去除两边的空白字符,并根据 keepSeparator 设置,可选地移除换行符

            (5)如果处理后的分块长度大于 minChunkLengthToEmbed,则将其添加到分块列表中

    4.持续执行第2步和第3步,直到所有 token 都被处理完或达到 maxNumChunks

    5.如果还有剩余的token没有处理,并且剩余的token进行编码和转换处理后,长度大于 minChunkLengthToEmbed,则将其作为最终块添加

    分块逻辑的源码:

    protected List<String> doSplit(String text, int chunkSize) {
            if (text != null && !text.trim().isEmpty()) {
                List<Integer> tokens = this.getEncodedTokens(text);
                List<String> chunks = new ArrayList();
                int num_chunks = 0;
    
                while(!tokens.isEmpty() && num_chunks < this.maxNumChunks) {
                    List<Integer> chunk = tokens.subList(0, Math.min(chunkSize, tokens.size()));
                    String chunkText = this.decodeTokens(chunk);
                    if (chunkText.trim().isEmpty()) {
                        tokens = tokens.subList(chunk.size(), tokens.size());
                    } else {
                        int lastPunctuation = Math.max(chunkText.lastIndexOf(46), Math.max(chunkText.lastIndexOf(63), Math.max(chunkText.lastIndexOf(33), chunkText.lastIndexOf(10))));
                        if (lastPunctuation != -1 && lastPunctuation > this.minChunkSizeChars) {
                            chunkText = chunkText.substring(0, lastPunctuation + 1);
                        }
    
                        String chunkTextToAppend = this.keepSeparator ? chunkText.trim() : chunkText.replace(System.lineSeparator(), " ").trim();
                        if (chunkTextToAppend.length() > this.minChunkLengthToEmbed) {
                            chunks.add(chunkTextToAppend);
                        }
    
                        tokens = tokens.subList(this.getEncodedTokens(chunkText).size(), tokens.size());
                        ++num_chunks;
                    }
                }
    
                if (!tokens.isEmpty()) {
                    String remaining_text = this.decodeTokens(tokens).replace(System.lineSeparator(), " ").trim();
                    if (remaining_text.length() > this.minChunkLengthToEmbed) {
                        chunks.add(remaining_text);
                    }
                }
    
                return chunks;
            } else {
                return new ArrayList();
            }
        }

    测试代码:

        @GetMapping("/embed3")
        public String embed3() {
            // 读取文本文件
            TextReader textReader = new TextReader(this.resource);
            // 元数据中增加文件名
            textReader.getCustomMetadata().put("filename", "医院.txt");
            // 获取Document对象
            List<Document> docList = textReader.read();
            // 文档分割
            TokenTextSplitter splitter = new TokenTextSplitter();
            List<Document> splitDocuments = splitter.apply(docList);
            // 向量化处理
            float[] embed = embeddingModel.embed(splitDocuments.get(0));
            // 打印向量化后的数据
            System.out.println(Arrays.toString(embed));
            // 打印Document中的原始文本数据
            System.out.println(splitDocuments.get(0).getText());
            return "success";
        }

    通过调试,可以看到本例的文本文档被分割为5个Document: 


    网站公告

    今日签到

    点亮在社区的每一天
    去签到