23 KiB
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
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 (
correspondencestable) บน 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 | ✅ สูง | ⚠️ ปานกลาง | ⚠️ ปานกลาง |
# แนะนำ: 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:
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:
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:
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):
-- ตาราง 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 :
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:
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: ใช้ได้เฉพาะจาก
<NAS_IP>เท่านั้น - Audit: ทุก Request บันทึก
created_by = 'SYSTEM_IMPORT'
Phase 3: การออกแบบ n8n Workflow (The Migration Logic)
Node 0: Pre-flight Health Check + Fetch System Categories
ตรวจสอบทุก dependency ก่อน Batch:
- HTTP GET Ollama
/api/tags→ ต้อง HTTP 200 - MariaDB
SELECT 1→ ต้องเชื่อมได้ - HTTP GET Backend
/health→ ต้อง HTTP 200 - File Mount Check →
staging_aiมีไฟล์,migration_logsเขียนได้
Fetch System Categories (Patch — ห้าม hardcode):
GET /api/meta/categories
Authorization: Bearer <MIGRATION_TOKEN>
Response:
{ "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:
- แปลง
project_code->project_id - แปลง
sender_code->sender_organization_id - แปลง
receiver_code->receiver_organization_id - หา 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)
- n8n ยิง
Node 4: AI Analysis (Sequential เท่านั้น)
System Prompt:
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:
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-> StatusPENDING(พร้อมรับ Batch Import)confidence >= 0.60และ< 0.85-> StatusPENDING(ติด Flag ให้ระวัง)confidence < 0.60หรือis_valid = false-> StatusREJECTED- Parse Error / AI ไม่ตอบ -> Error Log (Node ถัดไป)
Insert into staging:
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)
- หน้าจอ Frontend Management UI ดึงข้อมูลจาก
migration_review_queue - Admin สามารถ Browse & Edit ข้อมูล
- Tag Review: Admin สามารถพิจารณา Tags ที่เป็น
is_new: trueว่าควรตีตก หรือเปลี่ยนไปแมตช์ของเดิม - Admin กดปุ่ม Execute Import ส่งให้ Backend รัน Final Commit.
- 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:
-- ตรวจยอด
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
UPDATE users SET is_active = false WHERE username = 'migration_bot';
Step 2: ลบ Records (Transaction)
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
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 <NAS_IP> เท่านั้น |
| 11 | AI Tag Extraction ผิดพลาด | Tag confidence < 0.6 → ส่งไป Review Queue / บันทึกใน metadata |
| 12 | Tag ซ้ำ/คล้ายกัน | Normalization ก่อนบันทึก (lowercase, trim, deduplicate) |
6. Post-Migration Verification
-- 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/