know-engine

文档索引构建流程——文档上传

我们需要把一份PDF最终转化成向量数据库中的向量,需要经过以下步骤: 1、pdf文件上传 2、pdf文件存储在minio 3、将文件解析成markdown 4、基于markdown做分块 5、将分块后的数据做embedding 6、…

TL;DR

我们需要把一份PDF最终转化成向量数据库中的向量,需要经过以下步骤: 1、pdf文件上传 2、pdf文件存储在minio 3、将文件解析成markdown 4、基于markdown做分块 5、将分块后的数据做embedding 6、…

我们需要把一份PDF最终转化成向量数据库中的向量,需要经过以下步骤: 1、pdf文件上传 2、pdf文件存储在minio 3、将文件解析成markdown 4、基于markdown做分块 5、将分块后的数据做embedding 6、将向量和文本块保存在向量数据库 上面整个过程其实还是比较长的,所以需要引入异步方式来处理整个流程。那么就需要有一张表来记录文档。 除了整个文档需要记录以外,我们还需要把分段也做个保存,这样做有个好处,就是在管理工作台页面上能看到一个文档下面的所有分段的列表。 knowledge_document(知识文档表) 用途:存储上传到知识库的文档元数据信息,作为文档实体。 | 字段 | 类型 | 说明 | | --- | --- | --- | | doc_id | BIGINT | 文档ID,自增主键 | | doc_title | VARCHAR(1024) | 文档标题 | | upload_user | VARCHAR(255) | 上传用户 | | doc_url | VARCHAR(2048) | 文档存储URL | | converted_doc_url | VARCHAR(2048) | 解析后文档存储URL | | status | VARCHAR(32) | 文档状态 | | accessible_by | VARCHAR(1024) | 可见范围权限控制(如角色名称) |

状态流转:

INIT(初始状态) → UPLOADED(上传成功后状态) → CONVERTED(PDF转成markdown后的状态) → CHUNKED(已经分段后的状态) → VECTOR_STORED(已经存储到向量库以后的状态)

有一些数据是不需要保存在向量数据库的,比如父子分段中的父分段,比如需要通过数据查询而不是文档搜索的数据。(这两个后面都会讲),那么状态流转就是这样的:

INIT(初始状态) → UPLOADED(上传成功后状态) → CONVERTED(PDF转成markdown后的状态) → CHUNKED(已经分段后的状态) → STORED(已经存储到关系型数据库以后的状态)

knowledge_segment(知识片段表) 用途:存储文档分片后的文本片段,是 RAG 检索的基本单元。一篇文档会被拆分为多个片段。 | 字段 | 类型 | 说明 | | --- | --- | --- | | id | BIGINT | 片段ID,自增主键 | | chunk_id | VARCHAR(255) | 分片唯一标识(用于向量化存储关联) | | text | LONGTEXT | 文本内容 | | document_id | BIGINT | 所属文档ID(外键关联 knowledge_document.doc_id) | | chunk_order | INT | 分片顺序(文档内排序) | | embedding_id | VARCHAR(255) | 嵌入向量ID(Elasticsearch 中的向量ID) | | status | VARCHAR(255) | 分片状态:INIT(初始化)、VECTOR_STORED(已向量化) | | metadata | VARCHAR(2048) | 元数据JSON(包含 parent_chunk_id、brother_chunk_id 等关联信息) | | skip_embedding | INT | 是否跳过嵌入向量生成 |

关联关系:knowledge_document一对多knowledge_segment。一个文档包含多个知识片段,片段通过 document_id 关联到文档。

文档上传

提供一个controller,传入三个参数,一个是要上传的文件、一个上传者、一个是可见范围。

    /**
     * 文件上传接口
     *
     * @param file         上传的文件
     * @param uploadUser   上传用户
     * @param accessibleBy 可见范围(可选)
     * @return 保存后的文档记录
     */
    @PostMapping("/upload")
    public KnowledgeDocument uploadFile(
            @RequestParam("file") MultipartFile file,
            @RequestParam("uploadUser") String uploadUser,
            @RequestParam(value = "accessibleBy", required = false) String accessibleBy) throws IOException {
        try {
            String fileName = file.getOriginalFilename();
            //用minio上传
            String fileUrl = fileStorageService.uploadFile(file, fileName);

            // 构建文档记录
            KnowledgeDocument document = new KnowledgeDocument();
            document.setDocTitle(fileName);
            document.setUploadUser(uploadUser);
            document.setDocUrl(fileUrl);
            document.setStatus(DocumentStatus.UPLOADED);
            //todo permission处理
            document.setAccessibleBy(accessibleBy);

            // 保存到数据库
            knowledgeDocumentService.save(document);

            // 如果是 PDF 文件(通过后缀或文件头判断),异步调用转换处理
            if (isPdfFile(fileName) || isPdfContent(file)) {
                try {
                    fileProcessService.processDocument(document, file.getInputStream());
                } catch (Exception e) {
                    // 转换失败不影响上传结果,仅记录日志
                    System.err.println("PDF 转换处理失败,documentId: " + document.getDocId() + ", error: " + e.getMessage());
                }
            } else {
                // 更新文档状态为已转换
                document.setStatus(DocumentStatus.CONVERTED);
                document.setConvertedDocUrl(fileUrl);
                knowledgeDocumentService.updateById(document);
            }
            return document;
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

这里面是先调用minio的服务上传文件,然后创建一个KnowledgeDocument,保存在数据库中,这时候的状态是UPLOADER。 然后根据文件类型判断是不是pdf,如果是的话,则需要做文档的转换。如果不是的话,直接把状态推进到CONVERTED。并记录上传地址到convertedUrl上。 文件是否PDF的判断逻辑如下:

/**
 * 通过后缀名判断是否为 PDF 文件
 *
 * @param fileName 文件名
 * @return true 如果是 PDF 文件
 */
private boolean isPdfFile(String fileName) {
    if (fileName == null || fileName.isEmpty()) {
        return false;
    }
    return fileName.toLowerCase().endsWith(".pdf");
}

/**
 * 通过 Apache Tika 检测文件内容类型判断是否为 PDF 文件
 *
 * @param file 上传的文件
 * @return true 如果是 PDF 文件
 */
private boolean isPdfContent(MultipartFile file) {
    try (InputStream is = file.getInputStream()) {
        String mimeType = tika.detect(is);
        return "application/pdf".equals(mimeType);
    } catch (IOException e) {
        System.err.println("文件类型检测失败: " + e.getMessage());
        return false;
    }
}

然后PDF的转换逻辑,下一篇文档讲。

版本提示

模型、框架与接口会持续变化。涉及版本号、参数与生产配置时,请在实践前对照对应官方文档。

LLMentor系统化学习大模型应用工程

内容来自个人课程知识库备份,并经过结构化整理。技术版本持续演进,生产使用前请结合官方文档验证。