在 Day26,我們學會了如何將使用者輸入的文字轉成 Embedding 並存入 Qdrant。
今天,我們要讓 RAG 系統更進一步:「讓它自己讀懂文件」。我們將實作一個能夠自動擷取 PDF、圖片文字的功能,
讓使用者只要上傳檔案,系統就能把內容轉成純文字,
準備進入後續的 Embedding 流程。
這一篇的目標是:
1️⃣ 支援 PDF、JPG、PNG 等常見檔案上傳
2️⃣ 將文件轉成純文字
3️⃣ 自動進行 OCR(文字辨識)
4️⃣ 回傳擷取出的內容,準備給 Embedding 使用
整體流程如下:
上傳檔案
   ↓
PDFBox / Tesseract 擷取文字
   ↓
回傳純文字內容
   ↓
丟入 Ollama Embedding → 儲存 Qdrant
pom.xml)<dependencies>
    <!-- OCR -->
    <dependency>
        <groupId>net.sourceforge.tess4j</groupId>
        <artifactId>tess4j</artifactId>
        <version>5.4.0</version>
    </dependency>
    <!-- PDF 解析 -->
    <dependency>
        <groupId>org.apache.pdfbox</groupId>
        <artifactId>pdfbox</artifactId>
        <version>2.0.29</version>
    </dependency>
</dependencies>
application.yml)file:
spring:
    main:
        allow-bean-definition-overriding: true
    servlet:
        multipart:
            max-file-size: 50MB
            max-request-size: 50MB
server:
    port: 8080
file:
    upload-dir: /home/ubuntu/SpringAI_RAG/springAI_rag/temp/uploaded_file/source_file
    convert-dir: /home/ubuntu/SpringAI_RAG/springAI_rag/temp/uploaded_file/convert_file
@RestController
@RequiredArgsConstructor
@RequestMapping("/v1/rag/file")
@Slf4j
public class FileController {
    private final FileProcessService fileProcessService;
    /**
     * 檔案上傳 + OCR 擷取
     */
    @PostMapping("/upload/ocr")
    public ResponseEntity<BaseResponse> uploadFile(@RequestParam("file") MultipartFile file) {
        return ResponseEntity.ok(fileProcessService.fileOCR(file));
    }
}
@Slf4j
@Service
@RequiredArgsConstructor
public class FileProcessImp implements FileProcessService {
    private final Tesseract tesseract;
    @Value("${file.upload-dir}")
    private String uploadDir;
    @Value("${file.convert-dir}")
    private String convertDir;
    @Override
    public BaseResponse fileOCR(MultipartFile file) {
        try {
            // 建立必要目錄
            Files.createDirectories(Path.of(uploadDir));
            Files.createDirectories(Path.of(convertDir));
            // 儲存原始檔案
            String originalFilename = file.getOriginalFilename();
            if (originalFilename == null || originalFilename.isEmpty()) {
                return BaseResponse.builder().code("9999").msg("Failed").data("Uploaded file has no name").build();
            }
            String filePath = uploadDir + File.separator + originalFilename;
            file.transferTo(new File(filePath));
            // 若非 PDF,轉成 PDF
            if (!isSupportedFormat(new File(filePath))) {
                String[] command = {
                        "libreoffice", "--headless",
                        "--convert-to", "pdf",
                        filePath,
                        "--outdir", convertDir
                };
                ProcessBuilder pb = new ProcessBuilder(command);
                pb.redirectErrorStream(true);
                Process process = pb.start();
                process.waitFor();
                String fileExtension = originalFilename.substring(originalFilename.lastIndexOf("."));
                String pdfFileName = originalFilename.replace(fileExtension, ".pdf");
                filePath = convertDir + File.separator + pdfFileName;
            }
            // 使用 PDFBox 擷取文字
            File pdfFile = new File(filePath);
            PDDocument document = PDDocument.load(pdfFile);
            PDFTextStripper stripper = new PDFTextStripper();
            StringBuilder textBuilder = new StringBuilder();
            int totalPages = document.getNumberOfPages();
            for (int page = 1; page <= totalPages; page++) {
                stripper.setStartPage(page);
                stripper.setEndPage(page);
                String pageText = stripper.getText(document);
                textBuilder.append("=== Page ").append(page).append(" ===\n").append(pageText).append("\n\n");
            }
            document.close();
            log.info("Extracted text:\n{}", textBuilder);
            return BaseResponse.builder()
                    .code("0000")
                    .msg("Success")
                    .data(textBuilder.toString())
                    .build();
        } catch (IOException | InterruptedException e) {
            return BaseResponse.builder()
                    .code("9999")
                    .msg("Failed")
                    .data("Error processing file: " + e.getMessage())
                    .build();
        }
    }
    /** 檔案格式判斷 */
    public static boolean isSupportedFormat(File file) {
        String[] supported = {"png", "jpg", "jpeg", "tiff", "bmp", "pdf"};
        String name = file.getName().toLowerCase();
        for (String ext : supported) {
            if (name.endsWith(ext)) return true;
        }
        return false;
    }
}


這樣整個流程就串起來了 ,但其實不應該是分開的API應該是轉換完就直接存是比較隊的喔~~
從檔案 → 文字 → 向量 → Qdrant。
| 模組 | 功能 | 
|---|---|
| Tess4J | OCR 圖片文字辨識 | 
| PDFBox | 解析 PDF 文字內容 | 
| Spring Boot | 檔案上傳與 API 控制 | 
| Qdrant | 儲存向量資料 | 
| Ollama | 本地 Embedding 模型 | 
到這裡為止,我們的系統已能「看懂文字」與「記住內容」。
下一篇,我們要讓它「說出答案」。