# 03-04: Legacy Data Migration Plan (PDF 20k Docs) | description | version | | ------------------------------------------------------------------ | ------- | | legacy PDF document migration to system v1.8.0 uses n8n and Ollama | 1.8.0 | > **Note:** Category Enum system-driven, Idempotency Contract, Duplicate Handling Clarification, Storage Enforcement, Audit Log Enhancement, Review Queue Integration, Revision Drift Protection, Execution Time, Encoding Normalization, Security Hardening, Orchestrator on QNAP, AI Physical Isolation (Desktop Desk-5439), Folder Standard (/share/np-dms/n8n), **AI Tag Extraction & Auto-Tagging** --- ## 1. วัตถุประสงค์ (Objectives) - นำเข้าเอกสาร PDF 20,000 ฉบับ พร้อม Metadata จาก Excel (Legacy system export) เข้าสู่ระบบ LCBP3-DMS - ใช้ AI (Ollama Local Model) เพื่อตรวจสอบความถูกต้องของลักษณะข้อมูล (Data format, Title consistency) ก่อนการนำเข้า - **AI Tag Extraction:** ใช้ Ollama วิเคราะห์เอกสารและสกัด Tags ที่เกี่ยวข้อง (เช่น สาขางาน, ประเภทเอกสาร, องค์กร) อัตโนมัติ - รักษาโครงสร้างความสัมพันธ์ (Project / Contract / Ref No.) และระบบการทำ Revision ตาม Business Rules - **Checkpoint Support:** รองรับการหยุดและเริ่มงานต่อ (Resume) จากจุดที่ค้างอยู่ได้กรณีเกิดเหตุขัดข้อง > **Note:** เอกสารนี้ขยายความถึงวิธีปฏิบัติ (Implementation) จากการตัดสินใจทางสถาปัตยกรรมใน [ADR-017: Ollama Data Migration Architecture](../06-Decision-Records/ADR-017-ollama-data-migration.md) --- ## 2. โครงสร้างพื้นฐาน (Migration Infrastructure) - **Migration Orchestrator:** n8n (รันจาก Docker Container บน QNAP NAS) - **AI Validator:** Ollama (รันใน Internal Network บน Desktop Desk-5439, RTX 2060 SUPER 8GB) - **Target Database:** MariaDB (`correspondences` table) บน QNAP NAS - **Target Storage:** QNAP File System — **ผ่าน Backend StorageService API เท่านั้น** (ห้าม move file โดยตรง) - **Connection:** 2.5G LAN + LACP / Internal VLAN --- ## 3. ขั้นตอนการดำเนินงาน (Implementation Steps) ### Phase 1: การเตรียม Infrastructure และ Storage (สัปดาห์ที่ 1) **File Migration:** - ย้ายไฟล์ PDF ทั้งหมดจากแหล่งเก็บไปยัง Folder ชั่วคราวบน NAS (QNAP) - Target Path: `/share/np-dms/staging_ai/` **Mount Folder:** - Bind Mount `/share/np-dms/staging_ai/` เข้ากับ n8n Container แบบ **read-only** - สร้าง `/share/np-dms/n8n/migration_logs/` Volume แยกสำหรับเขียน Log แบบ **read-write** **Ollama Config:** - ติดตั้ง Ollama บน Desktop (Desk-5439, RTX 2060 SUPER 8GB) - No DB credentials, Internal network only #### 🔍 เปรียบเทียบผลลัพธ์ที่คาดหวัง | งาน | Typhoon2-4B | Qwen2.5-7B | OpenThaiGPT-7B | |-----|-------------|------------|----------------| | ความเร็ว (ток/วินาที) | ~35-45 | ~8-12 | ~10-15 | | ความเข้าใจบริบทไทย | ดีมาก | ดี | ดีมาก | | การสร้างแท็กแม่นยำ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | | ความเสถียรบน 8GB | ✅ สูง | ⚠️ ปานกลาง | ⚠️ ปานกลาง | ```bash # แนะนำ: llama3.2:3b (เร็ว, VRAM ~3GB, เหมาะ Classification) หรือ ollama run llama3.2:3b ollama pull llama3.2:3b # ทางเลือกที่ 1: เร็ว + ไทยดี (แนะนำ) ollama pull scb10x/typhoon2.1-gemma3-4b ollama run scb10x/typhoon2.1-gemma3-4b --system "คุณเป็นผู้ช่วยจัดหมวดหมู่เอกสารภาษาไทย โปรดตอบกลับในรูปแบบ JSON เท่านั้น" --option temperature=0.2 --option num_ctx=4096 # ทางเลือกที่ 2: คุณภาพสูง (โมเดลที่คุณใช้อยู่) ollama pull qwen2.5:7b-instruct-q4_K_M ollama run qwen2.5:7b-instruct-q4_K_M --system "คุณเป็นผู้ช่วยจัดหมวดหมู่เอกสารภาษาไทย โปรดตอบกลับในรูปแบบ JSON เท่านั้น" --option temperature=0.2 --option num_ctx=4096 # ถ้า Q4_K_M ยังหนักไป ลอง Q3_K_M (คุณภาพลดเล็กน้อย แต่ประหยัดแรม) ollama pull qwen2.5:7b-instruct-q3_K_M # ทางเลือกที่ 3: ไทยเฉพาะทาง ollama pull promptnow/openthaigpt1.5-7b-instruct-q4_k_m ollama run openthaigpt1.5-7b-instruct-q4_k_m --system "คุณเป็นผู้ช่วยจัดหมวดหมู่เอกสารภาษาไทย โปรดตอบกลับในรูปแบบ JSON เท่านั้น" --option temperature=0.2 --option num_ctx=4096 # เปิด terminal อีกหน้าต่างแล้วรัน watch -n 1 nvidia-smi # Fallback: mistral:7b-instruct-q4_K_M (แม่นกว่า, VRAM ~5GB) # ollama pull mistral:7b-instruct-q4_K_M ``` ใช้ ทางเลือกที่ 1 **ทดสอบ Ollama:** ```bash curl http://192.168.20.100:11434/api/generate \ -d '{"model":"llama3.2:3b","prompt":"reply: ok","stream":false}' ``` **Concurrency Configuration:** - Sequential: Batch Size = 1, Delay ≥ 2 วินาที, ปิด Parallel Execution - เพิ่ม Health Check Node ก่อนเริ่ม Batch เพื่อป้องกัน Workflow ค้างหาก Desktop Sleep หรือ Overheat --- ### Phase 2: การเตรียม Target Database และ API (สัปดาห์ที่ 1) **SQL Indexing:** ```sql ALTER TABLE correspondences ADD INDEX idx_doc_number (document_number); ALTER TABLE correspondences ADD INDEX idx_deleted_at (deleted_at); ALTER TABLE correspondences ADD INDEX idx_created_by (created_by); ``` **Checkpoint Table:** ```sql CREATE TABLE IF NOT EXISTS migration_progress ( batch_id VARCHAR(50) PRIMARY KEY, last_processed_index INT DEFAULT 0, status ENUM('RUNNING','COMPLETED','FAILED') DEFAULT 'RUNNING', updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ); ``` **Tags Table (สำหรับ AI Tag Extraction):** ```sql -- ตาราง Master เก็บ Tags (Global หรือ Project-specific) CREATE TABLE tags ( id INT PRIMARY KEY AUTO_INCREMENT, project_id INT NULL COMMENT 'NULL = Global Tag', tag_name VARCHAR(100) NOT NULL, color_code VARCHAR(30) DEFAULT 'default', description TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, created_by INT, deleted_at DATETIME NULL, UNIQUE KEY ux_tag_project (project_id, tag_name), INDEX idx_tags_deleted_at (deleted_at), FOREIGN KEY (project_id) REFERENCES projects (id) ON DELETE CASCADE, FOREIGN KEY (created_by) REFERENCES users (user_id) ON DELETE SET NULL ); -- ตารางเชื่อมระหว่าง correspondences และ tags (M:N) CREATE TABLE correspondence_tags ( correspondence_id INT, tag_id INT, PRIMARY KEY (correspondence_id, tag_id), FOREIGN KEY (correspondence_id) REFERENCES correspondences (id) ON DELETE CASCADE, FOREIGN KEY (tag_id) REFERENCES tags (id) ON DELETE CASCADE, INDEX idx_tag_lookup (tag_id) ); ``` **Idempotency Table :** ```sql CREATE TABLE IF NOT EXISTS import_transactions ( id INT AUTO_INCREMENT PRIMARY KEY, idempotency_key VARCHAR(255) UNIQUE NOT NULL, document_number VARCHAR(100), batch_id VARCHAR(100), status_code INT DEFAULT 201, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, INDEX idx_idem_key (idempotency_key) ); ``` > **Idempotency Logic:** ถ้า `idempotency_key` ซ้ำ → Backend คืน HTTP 200 ทันที (ไม่สร้าง Revision ซ้ำ) ถ้าไม่ซ้ำ → ประมวลผลปกติ **API Authentication — Migration Token:** ```sql INSERT INTO users (username, email, role, is_active) VALUES ('migration_bot', 'migration@system.internal', 'SYSTEM_ADMIN', true); ``` **Scope ของ Migration Token (Patch — คำนิยามชัดเจน):** | สิทธิ์ | ปกติ | Migration Token | หมายเหตุ | | ------------------------------------- | --- | --------------- | --------------------------------- | | Bypass File Virus Scan | ❌ | ✅ | ไฟล์ผ่าน Scan มาแล้วก่อน Import | | Bypass Duplicate **Validation Error** | ❌ | ✅ | **Revision Logic ยัง enforce ปกติ** | | Bypass Created-by User validation | ❌ | ✅ | | | Overwrite existing revision | ❌ | ❌ | **ห้ามโดยเด็ดขาด** | | Delete previous revision | ❌ | ❌ | **ห้ามโดยเด็ดขาด** | | ลบ / แก้ไข Record อื่น | ❌ | ❌ | **ห้ามโดยเด็ดขาด** | > ⚠️ **Patch Clarification:** "Bypass Duplicate Number Check" ถูกแทนด้วย "Bypass Duplicate **Validation Error**" — Revision increment logic ยังทำงานตามปกติทุกกรณี - **Token Expiry:** ไม่เกิน **7 วัน** ต้อง Revoke ทันทีหลัง Migration เสร็จ - **IP Whitelist:** ใช้ได้เฉพาะจาก `` เท่านั้น - **Audit:** ทุก Request บันทึก `created_by = 'SYSTEM_IMPORT'` --- ### Phase 3: การออกแบบ n8n Workflow (The Migration Logic) #### Node 0: Pre-flight Health Check + Fetch System Categories ตรวจสอบทุก dependency ก่อน Batch: 1. HTTP GET Ollama `/api/tags` → ต้อง HTTP 200 2. MariaDB `SELECT 1` → ต้องเชื่อมได้ 3. HTTP GET Backend `/health` → ต้อง HTTP 200 4. File Mount Check → `staging_ai` มีไฟล์, `migration_logs` เขียนได้ **Fetch System Categories (Patch — ห้าม hardcode):** ```http GET /api/meta/categories Authorization: Bearer ``` Response: ```json { "categories": ["Correspondence","RFA","Drawing","Transmittal","Report","Other"] } ``` n8n ต้องเก็บ categories นี้ไว้ใน Workflow Variable (`system_categories`) และ inject เข้า AI Prompt ทุก Request #### Node 1: Data Reader & Checkpoint #### Node 1: Data Reader & Checkpoint - อ่าน Checkpoint จาก **MariaDB Node แยก** - Batch ทีละ **50–100 แถว** ตาม `$env.MIGRATION_BATCH_SIZE` (ควรจำกัด Batch Size ป้องกัน DB Connection Overload) - ติด `original_index` ทุก Item และ Normalize Encoding (UTF-8 NFC) สำหรับ ชื่อไฟล์ และ เลขเอกสารเก่า #### Node 2: DB Lookup & Data Augmentation - **Task:** ให้ n8n นำข้อมูลจาก Excel (เช่น รหัสโปรเจ็กต์, รหัสผู้ส่ง) ยิงคำสั่ง Query ไปยัง MariaDB เพื่อแปลงเป็น `id` - **Queries:** 1. แปลง `project_code` -> `project_id` 2. แปลง `sender_code` -> `sender_organization_id` 3. แปลง `receiver_code` -> `receiver_organization_id` 4. หา Tags ที่มีอยู่ในโปรเจ็กต์: `SELECT * FROM tags WHERE project_id = {{project_id}}` - **Output:** n8n เก็บ `project_id`, `organization_ids` และ `existing_tags_json` ไว้ในแต่ละ item - *ถ้าหารหัสโปรเจ็กต์ไม่เจอ ให้ส่งเข้า Error Log ไม่ทำต่อ* #### Node 3: File Processor (Extract PDF Text & Temp Upload) - ตรวจสอบไฟล์ PDF มีอยู่จริงบน NAS `/share/np-dms/staging_ai` - **Extract PDF Text:** ใช้ Apache Tika สกัดข้อความจากเอกสาร - **Two-Phase Storage (Upload):** - n8n ยิง `POST /api/storage/upload` ส่งไฟล์ PDF เข้า Backend - Backend อัพโหลดไฟล์, กำหนด `is_temporary = TRUE` - Backend ส่งคืน `attachment_id` ให้ n8n (จะเรียกว่า `temp_attachment_id`) #### Node 4: AI Analysis (Sequential เท่านั้น) **System Prompt:** ```text You are a Document Controller for a large construction project. Your task is to validate document metadata, summarize content, and suggest relevant tags. You MUST respond ONLY with valid JSON. No explanation, no markdown. ``` **User Prompt:** ```text Validate and summarize this document. Respond in JSON. Document Number: {{$json.document_number}} Title: {{$json.title}} Extracted Text: {{$json.extracted_text}} Existing Project Tags: {{$json.existing_tags_json}} Analyze the content to provide: 1. Validation of Subject/Dates with PDF text. 2. A 4-5 sentence summary. 3. Suggest tags. Select from Existing Project Tags if applicable. If no existing tag fits, suggest a NEW one (set is_new: true). Respond ONLY with this exact JSON structure: { "is_valid": true | false, "confidence": 0.0 to 1.0, "category": "Correspondence", "summary": "<4-5 sentence summary>", "suggested_tags": [ {"name": "Structural", "description": "...", "is_new": false} ], "detected_issues": [] } ``` #### Node 5: Staging Ingestion (Insert to Review Queue) ข้อมูลทั้งหมดที่ผ่าน n8n และ AI Model **จะต้องไม่ถูกอัพเดทเข้าตารางหลักอัตโนมัติ** แต่จะถูกบังคับนำเข้าตาราง Staging `migration_review_queue` แทน เพื่อรอมนุษย์จัดการผ่าน Frontend UI **Status Routing Policy:** - `confidence >= 0.85` และ `is_valid = true` -> Status **`PENDING`** (พร้อมรับ Batch Import) - `confidence >= 0.60` และ `< 0.85` -> Status **`PENDING`** (ติด Flag ให้ระวัง) - `confidence < 0.60` หรือ `is_valid = false` -> Status **`REJECTED`** - Parse Error / AI ไม่ตอบ -> **Error Log** (Node ถัดไป) **Insert into staging:** ```sql INSERT INTO migration_review_queue ( document_number, title, project_id, sender_organization_id, receiver_organization_id, received_date, issued_date, remarks, ai_suggested_category, ai_confidence, ai_issues, ai_summary, extracted_tags, temp_attachment_id, status ) VALUES ( ... ) ON DUPLICATE KEY UPDATE status = VALUES(status), ai_summary = VALUES(ai_summary); ``` #### Node 6: Error Log & Reject Log - Parse Error → เขียนลงไฟล์ `/share/np-dms/n8n/migration_logs/error_log.csv` - ทุก 10-50 ราบการอัพเดท MariaDB `migration_progress` เพื่อเป็น Checkpoint. --- ### Phase 4: Frontend Management & Final Commit (UI -> Backend API) 1. หน้าจอ **Frontend Management UI** ดึงข้อมูลจาก `migration_review_queue` 2. Admin สามารถ Browse & Edit ข้อมูล 3. **Tag Review:** Admin สามารถพิจารณา Tags ที่เป็น `is_new: true` ว่าควรตีตก หรือเปลี่ยนไปแมตช์ของเดิม 4. Admin กดปุ่ม **Execute Import** ส่งให้ Backend รัน Final Commit. 5. Backend ยิงคำสั่งสร้าง Correspondence, นำ `temp_attachment_id` ไปผูกกับ Revision, ปรับเป็น `is_temporary = FALSE` และสร้าง/เชื่อม Tags จริง. --- ### Phase 4: แผนการทดสอบ (Testing & QA) **Dry Run Policy (Mandatory):** - All migrations MUST run with `--dry-run` - No DB commit until validation approved **Dry Run Validation (20–50 แถว):** - JSON Parse Success Rate > 95% - Category ที่ AI ตอบตรงกับ System Enum ทุกรายการ - รัน Batch เดิมซ้ำ 2 รอบ → ต้องไม่สร้าง Duplicate หรือ Revision ซ้ำ (Idempotency Test) - Storage Path ตรงตาม Core Storage Spec v1.8.0 - Revision Drift ถูก route ไป Review Queue **Integrity Check:** ```sql -- ตรวจยอด SELECT COUNT(*) FROM correspondences WHERE created_by = 'SYSTEM_IMPORT'; -- ตรวจ Revision ซ้ำ SELECT document_number, COUNT(*) as cnt FROM correspondences WHERE created_by = 'SYSTEM_IMPORT' GROUP BY document_number HAVING cnt > 1; -- ตรวจ Idempotency Key ไม่ซ้ำ SELECT idempotency_key, COUNT(*) as cnt FROM import_transactions GROUP BY idempotency_key HAVING cnt > 1; -- ตรวจ Audit Log ครบ SELECT COUNT(*) FROM audit_logs WHERE created_by = 'SYSTEM_IMPORT' AND action = 'IMPORT'; ``` --- ### Phase 5: การรันงานจริง (Execution & Monitoring) - **Scheduling:** รันอัตโนมัติ 22:00–06:00 - **Expected Runtime:** ~3 วินาที/record (2 sec delay + ~1 sec inference) → 20,000 records ≈ **60,000 วินาที (~16.6 ชั่วโมง)** → ใช้เวลาประมาณ **3–4 คืน** - **Daily Check:** Admin ตรวจ Review Queue และ Reject Log ทุกเช้าจาก Night Summary Email - **Progress Tracking:** อัปเดต `migration_progress` ทุก 10 Records --- ## 4. Rollback Plan **Step 1:** หยุด n8n และ Disable Token ```sql UPDATE users SET is_active = false WHERE username = 'migration_bot'; ``` **Step 2:** ลบ Records (Transaction) ```sql START TRANSACTION; DELETE FROM correspondence_files WHERE correspondence_id IN (SELECT id FROM correspondences WHERE created_by = 'SYSTEM_IMPORT'); DELETE FROM correspondences WHERE created_by = 'SYSTEM_IMPORT'; DELETE FROM import_transactions WHERE batch_id = 'migration_20260226'; SELECT ROW_COUNT(); COMMIT; ``` **Step 3:** ย้ายไฟล์กลับ `/share/np-dms/staging_ai/` ผ่าน Script แยก **Step 4:** Reset State ```sql UPDATE migration_progress SET status = 'FAILED', last_processed_index = 0 WHERE batch_id = 'migration_20260226'; UPDATE migration_fallback_state SET recent_error_count = 0, is_fallback_active = FALSE WHERE batch_id = 'migration_20260226'; ``` --- ## 5. แผนรับมือความเสี่ยง (Risk Management) | ลำดับที่ | ความเสี่ยง | การจัดการ (Mitigation) | | ---- | -------------------------- | -------------------------------------------------- | | 1 | AI Node หรือ GPU ค้าง | Timeout 30 วินาที, Retry 3 รอบ, Delay 60 วินาที | | 2 | Ollama ตอบไม่ใช่ JSON | JSON Pre-processor + ส่ง Human Review Queue | | 3 | Category ไม่ตรง System Enum | Fetch `/api/meta/categories` ก่อน Batch ทุกครั้ง | | 4 | Idempotency ซ้ำ | `import_transactions` table + Backend คืน HTTP 200 | | 5 | Revision Drift | ตรวจ Excel revision column → Route ไป Review Queue | | 6 | Storage bypass | ห้าม move file โดยตรง — ผ่าน Backend API เท่านั้น | | 7 | GPU VRAM Overflow | ใช้เฉพาะ Quantized Model (q4_K_M) | | 8 | ดิสก์ NAS เต็ม | ปิด "Save Successful Executions" ใน n8n | | 9 | Migration Token ถูกขโมย | Token 7 วัน, IP Whitelist `` เท่านั้น | | 11 | AI Tag Extraction ผิดพลาด | Tag confidence < 0.6 → ส่งไป Review Queue / บันทึกใน metadata | | 12 | Tag ซ้ำ/คล้ายกัน | Normalization ก่อนบันทึก (lowercase, trim, deduplicate) | --- ## 6. Post-Migration Verification ```sql -- 1. ตรวจยอดครบ 20,000 SELECT COUNT(*) FROM correspondences WHERE created_by = 'SYSTEM_IMPORT'; -- 2. ตรวจ Revision ผิดปกติ SELECT document_number, COUNT(*) FROM correspondences WHERE created_by = 'SYSTEM_IMPORT' GROUP BY document_number HAVING COUNT(*) > 5; -- 3. ตรวจ Audit Log ครบ SELECT COUNT(*) FROM audit_logs WHERE created_by = 'SYSTEM_IMPORT' AND action = 'IMPORT'; -- 4. ตรวจ Idempotency ไม่มีซ้ำ SELECT idempotency_key, COUNT(*) FROM import_transactions GROUP BY idempotency_key HAVING COUNT(*) > 1; -- 5. ตรวจ Tags ที่สร้างจาก Migration SELECT COUNT(*) as total_tags FROM tags WHERE created_by = (SELECT user_id FROM users WHERE username = 'migration_bot'); -- 6. ตรวจเอกสารที่มี Tag ผูกอยู่ SELECT COUNT(DISTINCT correspondence_id) as docs_with_tags FROM correspondence_tags ct JOIN correspondences c ON ct.correspondence_id = c.id WHERE c.created_by = (SELECT user_id FROM users WHERE username = 'migration_bot'); -- 7. ตรวจ Tag Distribution SELECT t.tag_name, COUNT(ct.correspondence_id) as doc_count FROM tags t JOIN correspondence_tags ct ON t.id = ct.tag_id JOIN correspondences c ON ct.correspondence_id = c.id WHERE c.created_by = (SELECT user_id FROM users WHERE username = 'migration_bot') GROUP BY t.id, t.tag_name ORDER BY doc_count DESC LIMIT 20; ``` --- > **ข้อแนะนำด้าน Physical Storage:** ไฟล์ PDF ทั้ง 20,000 ไฟล์จะถูก move โดย Backend StorageService ไปยัง path ที่ถูกต้องโดยอัตโนมัติ ไม่ปล่อยค้างไว้ที่ `/share/np-dms/staging_ai/`