目录

前言

一、向量数据库

1.1 什么是向量(Embedding)?

1.2 相似度计算原理

1.3 向量索引技术 (Indexing)

1.4 为什么需要专门的向量数据库?

1.5 SpringAI支持的向量数据库

支持的向量数据库

1.6 SpringAI向量数据库相关配置

1.7 VectorStore 核心 API 详解

1.7.1 注入 VectorStore

1.7.2 添加文档 API: add()

1.7.3 相似性搜索 API: similaritySearch()

1.7.4 带过滤的搜索 API: filterExpression()

1.7.5 删除文档 API: delete()

1.7.6 手动向量化 API: EmbeddingModel

1.7.7 距离度量配置

1.7.8 向量数据库的使用实例

二、Document Processing(文档处理)模块

2.1 AI 时代的 ETL 管道

2.2 分块 (Chunking) 的核心权衡

2.3 元数据 (Metadata) 的战略价值

2.4 SpringAI的配置

2.5 Document 读取器 API 详解

2.5.1 PDF 文档读取器

2.5.2 Markdown 文档读取器

2.5.3 Tika 文档读取器(多格式支持)

2.6 文档分块 (Chunking) API 详解

2.6.1 递归字符分块器

2.6.2 按令牌分块器(Token-based)

2.7 文档转换器 API 详解

2.7.1 元数据增强转换器

2.7.2 文本清洗转换器

2.8 文档处理完整的实例

总结


前言

大家好,这里是程序员阿亮!

上一篇咱们学习了SpringAI的基本概念与ChatClient、ChatModel等API,并且给出了一些实际开发的实践,这一期,咱们就来讲解一下SpringAI中的向量数据库、文档处理等内容,由于文档处理、ChatMemory、RAG的工程化要考虑到的内容非常多,所以这里就简单讲述基本使用,对于深挖这些技术的话,我会在后面一步步讲解。

一、向量数据库

关于向量数据库的知识点讲解,我在其他的博客文章中已经有详细的介绍:

深入研究RAG:向量数据库

所以今天就进行简单的介绍就进入一个SpringAI相关代码的解释

1.1 什么是向量(Embedding)?

概念:向量是将文本、图像等非结构化数据转换为高维空间中的数值数组的过程。

  • 传统搜索:基于关键词匹配(Keyword Matching)。如果用户搜“手机”,文档里只有“移动电话”,则匹配失败。
  • 向量搜索:基于语义相似度(Semantic Similarity)。“手机”和“移动电话”在向量空间中的距离非常近。

Spring AI 视角EmbeddingModel 接口屏蔽了不同模型(OpenAI, Vertex, Local)的差异,统一输出 float[]

1.2 相似度计算原理

向量数据库的核心是计算两个向量之间的“距离”。

  • 余弦相似度 (Cosine Similarity):最常用。计算两个向量夹角的余弦值。范围 [-1, 1],1 表示完全相同。适合文本语义。
  • 欧氏距离 (Euclidean Distance):计算空间直线距离。适合物理空间或归一化后的数据。
  • 点积 (Dot Product):计算向量对应位乘积之和。适合推荐系统。

Spring AI 实现SearchRequest.similarityThreshold() 本质上是在过滤余弦相似度低于阈值的结果。

1.3 向量索引技术 (Indexing)

当数据量达到百万/千万级时,暴力计算所有向量距离(O(N))太慢。向量库使用近似最近邻搜索(ANN)算法。

  • HNSW (Hierarchical Navigable Small World):构建多层图结构,搜索速度快,内存占用高。适合实时性要求高的场景。
  • IVF (Inverted File Index):将向量聚类,搜索时只查找最近的簇。适合大规模数据,节省内存。
  • Spring AI 抽象VectorStore 接口不暴露具体索引算法,但底层实现(如 PGVector, Milvus)会利用各自的索引优化性能。

1.4 为什么需要专门的向量数据库?

  • 关系型数据库 (MySQL):擅长结构化数据,但无法高效计算高维向量距离。
  • 搜索引擎 (Elasticsearch):擅长全文检索,但语义理解能力弱于专用向量库。
  • 向量数据库 (PGVector, Milvus):专为高维向量相似度搜索优化,支持混合查询(向量 + 元数据过滤)。

1.5 SpringAI支持的向量数据库

支持的向量数据库

类型

实现类

适用场景

内存/测试

SimpleVectorStore

开发测试、原型验证

PostgreSQL

PgVectorStore

生产环境、已有 PG 基础设施

Redis

RedisVectorStore

高性能、缓存场景

Milvus

MilvusVectorStore

大规模向量检索

Qdrant

QdrantVectorStore

云原生向量服务

Chroma

ChromaVectorStore

轻量级 AI 应用

Azure AI Search

AzureAiSearchVectorStore

Azure 云生态

1.6 SpringAI向量数据库相关配置

Maven:

<dependencies>
    <!-- 核心 Vector Store -->
    <dependency>
        <groupId>org.springframework.ai</groupId>
        <artifactId>spring-ai-starter-vector-store</artifactId>
    </dependency>
    
    <!-- PGVector 支持 -->
    <dependency>
        <groupId>org.springframework.ai</groupId>
        <artifactId>spring-ai-starter-vector-store-pgvector</artifactId>
    </dependency>
    
    <!-- Redis 支持 -->
    <dependency>
        <groupId>org.springframework.ai</groupId>
        <artifactId>spring-ai-starter-vector-store-redis</artifactId>
    </dependency>
    
    <!-- Embedding 模型(用于向量化) -->
    <dependency>
        <groupId>org.springframework.ai</groupId>
        <artifactId>spring-ai-starter-model-openai</artifactId>
    </dependency>
</dependencies>

Yaml:

spring:
  ai:
    # 向量存储配置
    vectorstore:
      pgvector:
        # 是否自动初始化表结构
        initialize-schema: true
        # 表名
        table-name: vector_store
        # 索引名称
        index-name: document_embeddings
        # 向量维度(需与 Embedding 模型匹配)
        dimension: 1536
      redis:
        # Redis 连接配置
        host: localhost
        port: 6379
        # 键前缀
        prefix: ai:vector:
    
    # Embedding 模型配置
    openai:
      embedding:
        options:
          model: text-embedding-3-small

1.7 VectorStore 核心 API 详解

1.7.1 注入 VectorStore

@RestController
public class VectorStoreController {

    // Spring Boot 自动配置并注入 VectorStore 实现
    @Autowired
    private VectorStore vectorStore;
    
    // 同时注入 EmbeddingModel(用于手动向量化)
    @Autowired
    private EmbeddingModel embeddingModel;
}

1.7.2 添加文档 API: add()

作用:将文档列表添加到向量库,自动进行向量化。

@PostMapping("/add")
public String addDocuments() {
    // 1. 创建 Document 对象
    // 构造函数:Document(String content, Map<String, Object> metadata)
    Document doc1 = new Document(
        "Spring AI 是 Spring 官方推出的 AI 应用框架",  // 文档内容
        Map.of(  // 元数据(用于过滤和分类)
            "category", "framework",
            "source", "official-doc",
            "version", "1.0",
            "createTime", System.currentTimeMillis()
        )
    );
    
    Document doc2 = new Document(
        "Vector Store 支持语义搜索和 RAG 应用",
        Map.of(
            "category", "vector-store",
            "source", "tutorial",
            "version", "1.0"
        )
    );
    
    // 2. 批量添加文档
    // add() 方法会自动调用 EmbeddingModel 将文本转换为向量
    vectorStore.add(List.of(doc1, doc2));
    
    return "成功添加 2 篇文档";
}

1.7.3 相似性搜索 API: similaritySearch()

作用:根据查询文本,返回最相似的文档列表。

@PostMapping("/search")
public List<Map<String, Object>> searchDocuments(@RequestParam String query) {
    // 1. 构建搜索请求
    SearchRequest searchRequest = SearchRequest.builder()
        .query(query)                    // 查询文本
        .topK(5)                         // 返回最相似的 5 条结果
        .similarityThreshold(0.7)        // 相似度阈值(0-1,越高越严格)
        .build();
    
    // 2. 执行相似性搜索
    // 返回按相似度降序排列的 Document 列表
    List<Document> results = vectorStore.similaritySearch(searchRequest);
    
    // 3. 转换为响应格式
    return results.stream().map(doc -> Map.of(
        "content", doc.getContent(),           // 文档内容
        "metadata", doc.getMetadata(),         // 元数据
        "score", doc.getScore()                // 相似度分数
    )).collect(Collectors.toList());
}

1.7.4 带过滤的搜索 API: filterExpression()

作用:在相似性搜索基础上,添加元数据过滤条件。

@PostMapping("/search-filtered")
public List<Map<String, Object>> searchWithFilter(@RequestParam String query) {
    // 1. 构建过滤表达式
    FilterExpressionBuilder builder = new FilterExpressionBuilder();
    
    // 2. 构建复合过滤条件
    // eq: 等于,gte: 大于等于,lte: 小于等于,in: 在列表中
    Filter.Expression filter = builder.and(
        builder.eq("category", "framework"),           // 分类必须是 framework
        builder.eq("version", "1.0"),                  // 版本必须是 1.0
        builder.gte("createTime", System.currentTimeMillis() - 86400000L)  // 24 小时内
    ).build();
    
    // 3. 构建带过滤的搜索请求
    SearchRequest searchRequest = SearchRequest.builder()
        .query(query)
        .topK(10)
        .filterExpression(filter)  // 应用过滤条件
        .build();
    
    // 4. 执行搜索
    List<Document> results = vectorStore.similaritySearch(searchRequest);
    
    return results.stream()
        .map(doc -> Map.of("content", doc.getContent(), "score", doc.getScore()))
        .collect(Collectors.toList());
}

1.7.5 删除文档 API: delete()

作用:根据文档 ID 或删除条件删除文档。

@DeleteMapping("/delete")
public String deleteDocuments(@RequestParam List<String> docIds) {
    // 方式 1:按 ID 删除
    vectorStore.delete(docIds);  // docIds 是 Document 的 id 列表
    
    // 方式 2:按条件删除(部分实现支持)
    // Filter.Expression filter = new FilterExpressionBuilder()
    //     .eq("category", "temp")
    //     .build();
    // vectorStore.delete(filter);
    
    return "成功删除 " + docIds.size() + " 篇文档";
}

1.7.6 手动向量化 API: EmbeddingModel

作用:独立调用 Embedding 模型,获取文本向量表示。

@GetMapping("/embed")
public Map<String, Object> getEmbedding(@RequestParam String text) {
    // 1. 调用 Embedding 模型
    // embedding() 方法返回 float[] 向量数组
    float[] vector = embeddingModel.embed(text);
    
    // 2. 批量向量化(更高效)
    List<String> texts = List.of("文本 1", "文本 2", "文本 3");
    List<float[]> vectors = embeddingModel.embed(texts);
    
    // 3. 返回结果
    return Map.of(
        "text", text,
        "vectorLength", vector.length,
        "vectorSample", Arrays.copyOfRange(vector, 0, 5)  // 只返回前 5 个值示例
    );
}

1.7.7 距离度量配置

// 不同向量数据库支持不同的距离度量方式
SearchRequest searchRequest = SearchRequest.builder()
    .query("查询文本")
    .topK(5)
    // 部分实现支持指定距离度量
    // .distanceType(DistanceType.COSINE)  // 余弦相似度(默认)
    // .distanceType(DistanceType.EUCLIDEAN)  // 欧氏距离
    // .distanceType(DistanceType.DOT_PRODUCT)  // 点积
    .build();

1.7.8 向量数据库的使用实例

@Service
public class KnowledgeBaseService {

    @Autowired
    private VectorStore vectorStore;
    
    @Autowired
    private EmbeddingModel embeddingModel;

    /**
     * 初始化知识库
     */
    public void initKnowledgeBase() {
        // 1. 准备文档数据
        List<Document> documents = List.of(
            new Document("Spring AI 支持 OpenAI、Ollama、Azure 等模型", 
                Map.of("category", "integration", "level", "basic")),
            new Document("Vector Store 用于存储文档向量,支持语义搜索", 
                Map.of("category", "vector", "level", "intermediate")),
            new Document("RAG = 检索 + 生成,提升回答准确性", 
                Map.of("category", "rag", "level", "advanced")),
            new Document("Chat Memory 管理多轮对话上下文", 
                Map.of("category", "memory", "level", "basic")),
            new Document("Function Calling 让 AI 调用外部 API", 
                Map.of("category", "tools", "level", "intermediate"))
        );
        
        // 2. 批量添加到向量库
        vectorStore.add(documents);
        System.out.println("知识库初始化完成,共 " + documents.size() + " 篇文档");
    }

    /**
     * 语义搜索
     */
    public List<Map<String, Object>> search(String query, String category, int topK) {
        // 1. 构建搜索请求
        SearchRequest.Builder builder = SearchRequest.builder()
            .query(query)
            .topK(topK)
            .similarityThreshold(0.5);
        
        // 2. 可选:添加分类过滤
        if (category != null && !category.isEmpty()) {
            FilterExpressionBuilder fb = new FilterExpressionBuilder();
            builder.filterExpression(fb.eq("category", category).build());
        }
        
        // 3. 执行搜索
        List<Document> results = vectorStore.similaritySearch(builder.build());
        
        // 4. 格式化返回
        return results.stream().map(doc -> Map.of(
            "content", doc.getContent(),
            "category", doc.getMetadata().get("category"),
            "level", doc.getMetadata().get("level"),
            "score", doc.getScore()
        )).collect(Collectors.toList());
    }

    /**
     * 删除指定类别的文档
     */
    public int deleteByCategory(String category) {
        // 注意:部分 VectorStore 实现不支持按条件删除
        // 需要自行维护文档 ID 映射
        FilterExpressionBuilder fb = new FilterExpressionBuilder();
        Filter.Expression filter = fb.eq("category", category).build();
        
        // 先搜索出符合条件的文档
        List<Document> docs = vectorStore.similaritySearch(
            SearchRequest.builder().query("*").topK(1000).filterExpression(filter).build()
        );
        
        // 再删除
        List<String> ids = docs.stream().map(Document::getId).collect(Collectors.toList());
        vectorStore.delete(ids);
        
        return ids.size();
    }
}

二、Document Processing(文档处理)模块

对于文档处理的知识点,我之前也在其他博客有详细讲解:

深入研究RAG:总体流程与文档处理

所以这里就做简单的讲解

2.1 AI 时代的 ETL 管道

传统 ETL (Extract, Transform, Load) 是为数据仓库设计的。AI 时代的 ETL 是为大模型上下文设计的。

  • Extract (读取):从 PDF/Word 中提取纯文本,去除格式噪音。
  • Transform (转换)分块 (Chunking) 是最核心的转换。
  • Load (加载):向量化后存入 Vector Store。

2.2 分块 (Chunking) 的核心权衡

为什么不能把整本书作为一个向量?

  1. 上下文限制:LLM 的 Context Window 有限(如 128K),全量输入成本高且易丢失细节。
  2. 检索精度:用户问“第 3 章的密码是多少”,如果整本书是一个向量,检索到的可能是“第 1 章的引言”,因为整体语义相似但局部不相关。
  3. 噪声干扰:过大的 Chunk 包含过多无关信息,干扰 LLM 判断。

分块策略详解

  • 固定字符分块:简单,但可能切断句子语义。
  • 递归字符分块 (Recursive Character):Spring AI 默认推荐。按段落→换行→句子→单词优先级切分,尽量保持语义完整。
  • 语义分块 (Semantic Chunking):利用 Embedding 检测文本语义突变点进行切分(成本高,精度高)。
  • 父子分块 (Parent-Child):检索小 chunk(精准),但送给 LLM 的是包含该 chunk 的大 chunk(上下文丰富)。

2.3 元数据 (Metadata) 的战略价值

元数据不仅是标签,它是路由和过滤的关键。

  • 访问控制doc.metadata.permission = "private",检索时过滤掉无权查看的文档。
  • 时间衰减doc.metadata.year = 2020,用户问“最新政策”,可优先检索 2023-2024 的文档。
  • 来源追溯:RAG 回答中标注“据《2023 员工手册》第 5 页”,增加可信度。

2.4 SpringAI的配置

<dependencies>
    <!-- PDF 文档读取器 -->
    <dependency>
        <groupId>org.springframework.ai</groupId>
        <artifactId>spring-ai-pdf-document-reader</artifactId>
    </dependency>
    
    <!-- Apache Tika(支持 Word、Excel、PPT 等) -->
    <dependency>
        <groupId>org.springframework.ai</groupId>
        <artifactId>spring-ai-tika-document-reader</artifactId>
    </dependency>
    
    <!-- Markdown 读取器(内置) -->
    <!-- 无需额外依赖 -->
</dependencies>

2.5 Document 读取器 API 详解

2.5.1 PDF 文档读取器

@Service
public class PdfProcessor {

    /**
     * 读取 PDF 文件
     */
    public List<Document> readPdf(String filePath) {
        // 1. 创建 PDF 读取器
        // 构造函数参数:文件路径或 Resource 对象
        PdfDocumentReader reader = new PdfDocumentReader(filePath);
        
        // 2. 获取文档列表(一个 PDF 可能包含多页,每页一个 Document)
        List<Document> documents = reader.get();
        
        System.out.println("读取了 " + documents.size() + " 页 PDF");
        return documents;
    }

    /**
     * 读取 PDF 并添加元数据
     */
    public List<Document> readPdfWithMetadata(String filePath, Map<String, Object> metadata) {
        PdfDocumentReader reader = new PdfDocumentReader(filePath);
        List<Document> documents = reader.get();
        
        // 3. 为每个文档添加额外元数据
        for (Document doc : documents) {
            // getMetadata() 返回可修改的 Map
            doc.getMetadata().putAll(metadata);
        }
        
        return documents;
    }
}

2.5.2 Markdown 文档读取器

@Service
public class MarkdownProcessor {

    /**
     * 读取 Markdown 文件
     */
    public List<Document> readMarkdown(String filePath) {
        // 1. 创建 Markdown 读取器
        MarkdownDocumentReader reader = new MarkdownDocumentReader(filePath);
        
        // 2. 获取文档
        // Markdown 通常按章节或固定大小分块
        return reader.get();
    }

    /**
     * 从 URL 读取 Markdown(如 GitHub README)
     */
    public List<Document> readMarkdownFromUrl(String url) {
        // 1. 创建 Resource
        Resource resource = new UrlResource(url);
        
        // 2. 创建读取器
        MarkdownDocumentReader reader = new MarkdownDocumentReader(resource);
        
        return reader.get();
    }
}

2.5.3 Tika 文档读取器(多格式支持)

@Service
public class TikaProcessor {

    /**
     * 读取多种格式文档(Word、Excel、PPT、TXT 等)
     */
    public List<Document> readWithTika(String filePath) {
        // 1. 创建 Tika 读取器
        TikaDocumentReader reader = new TikaDocumentReader(filePath);
        
        // 2. 获取文档
        return reader.get();
    }

    /**
     * 批量读取目录下的所有文档
     */
    public List<Document> readDirectory(String dirPath) {
        List<Document> allDocuments = new ArrayList<>();
        
        // 1. 获取目录下所有文件
        File dir = new File(dirPath);
        File[] files = dir.listFiles();
        
        if (files != null) {
            for (File file : files) {
                // 2. 跳过非文件
                if (!file.isFile()) continue;
                
                // 3. 读取每个文件
                try {
                    TikaDocumentReader reader = new TikaDocumentReader(file.getPath());
                    List<Document> docs = reader.get();
                    
                    // 4. 添加文件名元数据
                    for (Document doc : docs) {
                        doc.getMetadata().put("fileName", file.getName());
                        doc.getMetadata().put("filePath", file.getAbsolutePath());
                    }
                    
                    allDocuments.addAll(docs);
                } catch (Exception e) {
                    System.err.println("读取失败:" + file.getName());
                }
            }
        }
        
        return allDocuments;
    }
}

2.6 文档分块 (Chunking) API 详解

作用:将长文档切分为合适大小的片段,避免超出模型上下文限制,同时保持语义完整性。

2.6.1 递归字符分块器

@Service
public class DocumentSplitter {

    /**
     * 使用递归字符分块器
     */
    public List<Document> splitDocuments(List<Document> documents) {
        // 1. 创建分块器
        RecursiveCharacterTextSplitter splitter = RecursiveCharacterTextSplitter.builder()
            .chunkSize(500)        // 每块最大 500 字符
            .chunkOverlap(50)      // 块之间重叠 50 字符(保持语义连贯)
            .separators(List.of(   // 分隔符优先级(从大到小)
                "\n\n",            // 段落
                "\n",              // 换行
                "。",              // 中文句号
                ".",               // 英文句号
                " ",               // 空格
                ""                 // 字符级
            ))
            .build();
        
        // 2. 应用分块
        // apply() 方法接受 Document 列表,返回分块后的 Document 列表
        List<Document> chunks = splitter.apply(documents);
        
        System.out.println("原始文档:" + documents.size() + " → 分块后:" + chunks.size());
        return chunks;
    }

    /**
     * 自定义分块策略
     */
    public List<Document> splitWithCustomStrategy(List<Document> documents) {
        // 1. 创建分块器
        RecursiveCharacterTextSplitter splitter = RecursiveCharacterTextSplitter.builder()
            .chunkSize(1000)       // 较大块,适合长上下文模型
            .chunkOverlap(200)     // 较大重叠,保证语义完整
            .lengthFunction(text -> text.length())  // 长度计算函数
            .build();
        
        return splitter.apply(documents);
    }
}

2.6.2 按令牌分块器(Token-based)

@Service
public class TokenSplitter {

    @Autowired
    private EmbeddingModel embeddingModel;

    /**
     * 按令牌数量分块(更精确控制)
     */
    public List<Document> splitByTokens(List<Document> documents) {
        // 1. 创建 Token 分块器
        TokenTextSplitter splitter = new TokenTextSplitter(
            embeddingModel,    // 使用 Embedding 模型的 tokenizer
            512,               // 每块 512 tokens
            50,                // 重叠 50 tokens
            10000,             // 最大令牌数限制
            true               // 是否保持段落完整
        );
        
        return splitter.apply(documents);
    }
}

2.7 文档转换器 API 详解

作用:对文档内容进行转换、清洗、增强。

2.7.1 元数据增强转换器

@Service
public class MetadataEnricher {

    /**
     * 为文档添加元数据
     */
    public List<Document> enrichMetadata(List<Document> documents) {
        // 1. 创建转换器
        DocumentTransformer transformer = new DocumentTransformer() {
            @Override
            public List<Document> apply(List<Document> input) {
                return input.stream().map(doc -> {
                    // 2. 复制文档并添加新元数据
                    Map<String, Object> newMetadata = new HashMap<>(doc.getMetadata());
                    newMetadata.put("processedTime", System.currentTimeMillis());
                    newMetadata.put("contentLength", doc.getContent().length());
                    newMetadata.put("wordCount", doc.getContent().split("\\s+").length);
                    
                    // 3. 创建新 Document
                    return new Document(doc.getId(), doc.getContent(), newMetadata);
                }).collect(Collectors.toList());
            }
        };
        
        return transformer.apply(documents);
    }
}

2.7.2 文本清洗转换器

@Service
public class TextCleaner {

    /**
     * 清洗文档内容(移除特殊字符、多余空格等)
     */
    public List<Document> cleanDocuments(List<Document> documents) {
        DocumentTransformer transformer = new DocumentTransformer() {
            @Override
            public List<Document> apply(List<Document> input) {
                return input.stream().map(doc -> {
                    // 1. 清洗文本
                    String cleaned = doc.getContent()
                        .replaceAll("\\s+", " ")      // 多余空格变单空格
                        .replaceAll("\\n{3,}", "\n\n") // 多余换行变双换行
                        .trim();
                    
                    // 2. 创建新 Document
                    return new Document(doc.getId(), cleaned, doc.getMetadata());
                }).collect(Collectors.toList());
            }
        };
        
        return transformer.apply(documents);
    }
}

2.8 文档处理完整的实例

@Service
public class DocumentIngestionService {

    @Autowired
    private VectorStore vectorStore;
    
    @Autowired
    private EmbeddingModel embeddingModel;

    /**
     * 完整的文档入库流程
     */
    public IngestionResult ingestDocument(String filePath, String category) {
        long startTime = System.currentTimeMillis();
        
        try {
            // ========== 阶段 1: 读取文档 ==========
            List<Document> documents = readDocument(filePath);
            System.out.println("阶段 1 完成:读取 " + documents.size() + " 篇文档");
            
            // ========== 阶段 2: 添加元数据 ==========
            Map<String, Object> metadata = Map.of(
                "category", category,
                "source", filePath,
                "ingestTime", System.currentTimeMillis()
            );
            for (Document doc : documents) {
                doc.getMetadata().putAll(metadata);
            }
            System.out.println("阶段 2 完成:元数据增强");
            
            // ========== 阶段 3: 文本清洗 ==========
            documents = cleanDocuments(documents);
            System.out.println("阶段 3 完成:文本清洗");
            
            // ========== 阶段 4: 文档分块 ==========
            RecursiveCharacterTextSplitter splitter = RecursiveCharacterTextSplitter.builder()
                .chunkSize(500)
                .chunkOverlap(50)
                .build();
            List<Document> chunks = splitter.apply(documents);
            System.out.println("阶段 4 完成:分块为 " + chunks.size() + " 个片段");
            
            // ========== 阶段 5: 向量化并存储 ==========
            vectorStore.add(chunks);
            System.out.println("阶段 5 完成:向量存储");
            
            // ========== 返回结果 ==========
            return IngestionResult.builder()
                .success(true)
                .originalDocs(documents.size())
                .chunks(chunks.size())
                .duration(System.currentTimeMillis() - startTime)
                .build();
                
        } catch (Exception e) {
            e.printStackTrace();
            return IngestionResult.builder()
                .success(false)
                .error(e.getMessage())
                .duration(System.currentTimeMillis() - startTime)
                .build();
        }
    }
    
    /**
     * 根据文件类型选择读取器
     */
    private List<Document> readDocument(String filePath) {
        if (filePath.endsWith(".pdf")) {
            return new PdfDocumentReader(filePath).get();
        } else if (filePath.endsWith(".md")) {
            return new MarkdownDocumentReader(filePath).get();
        } else {
            return new TikaDocumentReader(filePath).get();
        }
    }
    
    /**
     * 文本清洗
     */
    private List<Document> cleanDocuments(List<Document> documents) {
        return documents.stream().map(doc -> {
            String cleaned = doc.getContent()
                .replaceAll("\\s+", " ")
                .replaceAll("\\n{3,}", "\n\n")
                .trim();
            return new Document(doc.getId(), cleaned, doc.getMetadata());
        }).collect(Collectors.toList());
    }
    
    // 结果对象
    @Data
    @Builder
    public static class IngestionResult {
        private boolean success;
        private int originalDocs;
        private int chunks;
        private long duration;
        private String error;
    }
}

总结

那么今天也是讲解了Embedding和Document,接下来会是ChatMemory与RAG的使用!

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐